I have a Java class with an Optional field. I am serializing the class to JSON using Jackson 2.8.3 (called from Spring web 4.3.3).
I am hoping to get the serializer to skip the field if the Optional is empty, and serialize the contained string if it is present. An example of the result I'm looking for with a list of two objects:
[
{
"id": 1,
"foo": "bar"
},
{
"id": 2,
}
]
Here the foo Optional is empty for the object with id 2.
Instead, what I get is:
[
{
"id": 1,
"foo": {
"present": true
}
},
{
"id": 2,
"foo": {
"present": false
}
}
]
This is the result even if I annotate the "bar" field in the class like
#JsonInclude(JsonInclude.Include.NON_ABSENT)
public Optional<String> getFoo() { ...
Is there any way I can achieve a result like the first list using the Jackson annotations or a custom serializer?
No need to write custom serializer. Annotate your class with #JsonInclude(JsonInclude.Include.NON_ABSENT).
You also need to:
include com.fasterxml.jackson.datatype:jackson-datatype-jdk8 as your dependency
and to register the corresponding module with your object mapper: objectMapper.registerModule(new Jdk8Module());
You can use objectMapper.registerModule(new Jdk8Module()); but it serializes with null values.
But still you want to remove null values also from JSON, please use the following code:
objectMapper.registerModule(new Jdk8Module().configureAbsentsAsNulls(true));
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Use a JsonSerializer to your needs.
Something like this (semi-pseudo):
public class MySer extends JsonSerializer<Opional<?>> {
#Override
public void serialize(Optional<?> optString, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
//Check Optional here...
generator.writeString(/* DO SOMETHING HERE WHATEVER */);
}
//Then in your model:
public class ClassWhatever {
#JsonSerialize(using = MySer .class)
public Optional<String> getFoo() { ...
}
To avoid annotating every field with #JsonSerialize you may register your custom serializer to object mapper using
ObjectMapper mapper = new ObjectMapper();
SimpleModule testModule = new SimpleModule("MyModule", new Version(1, 0, 0, null));
testModule.addSerializer(new MyCustomSerializer()); // assuming serializer declares correct class to bind to
mapper.registerModule(testModule);
Also, given solution works only for serialization. Deserialization will fail unless you write your own deserializer. Then you need to annotate every field with #JsonDeserialize or register your custom deserializer.
Related
I have a class like this.
class MyClass {
String config;
// ... other fields, getters, setters ...
}
The config string will be coming as JSON from the REST endpoint in the request body as follows..
"config": {
"field1": "value1",
"field2": 2,
"field3": true
}
// other fields of MyClass
I need to deserialize MyClass in such a way that the above JSON string is put in quotes as follows.
"config": "{
\"field1\": \"value1\",
\"field2\": 2,
\"field3\": true
}"
I cannot modify this class as it is being used by other projects. So, I cannot use #JsonDeserialize or any annotations on the class.
I tried setting the following properties to ObjectMapper
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
Is there any other way, perhaps using ObjectMapper to deserialize it that way. I am using Spring Boot also, so would welcome a Spring boot Jackson way of doing this.
You can use JsonNode, the Jackson JsonNode class, com.fasterxml.jackson.databind.JsonNode is Jackson's tree model (object graph model) for JSON, use like that:
String content = jsonNode.get("data").textValue();
I'mu using JsonPatch (JSR-374) with implementation from Apache org.apache.johnzon:johnzon-core:1.2.4 in my Spring project PATCH endpoint:
#Bean
#Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.registerModule(new Jdk8Module());
objectMapper.registerModule(new JSR353Module());
return objectMapper;
}
Controller
#PatchMapping("/settings")
public ResponseEntity<SettingsResponse> patchSettings(#RequestBody JsonPatch patchDocument, Locale locale) {...}
With json request of a simple atomic values
[
{ "op": "replace", "path": "/currency", "value": "EUR" },
{ "op": "test", "path": "/version", "value": 10 }
]
JsonPatch instance is deserialised correctly by Jackson
But with complex value type (object):
[
{ "op": "replace", "path": "/currency", "value": {"code": "USD", "label": "US Dollar"} },
{ "op": "test", "path": "/version", "value": 10 }
]
Exception is thrown
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of javax.json.JsonPatch (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (PushbackInputStream); line: 1, column: 1]
I recon JsonPatch (and its Apache JsonPatchImpl) is capable of working with complex types as JsonValue mentions JsonObject and ValueType.OBJECT, but I don't know how to instruct Jackson to deserialise correctly
Thanks in advance for any suggestions or help!
I went through this by using the JSR-364 Implementation Json.createPatch:
#PatchMapping("/settings")
public ResponseEntity<SettingsResponse> patchDefaultSettingsJsonP3(#RequestBody String patchString, Locale locale) {
try (JsonReader jsonReader = Json.createReader(new StringReader(patchString))) {
JsonPatch patch = Json.createPatch(jsonReader.readArray());
...
}
}
EDIT:
I found wiser solution by registering the converter as a bean. Spring then takes care of the deserialisation internally
#Component
public class JsonPatchHttpMessageConverter extends AbstractHttpMessageConverter<JsonPatch> {
public JsonPatchHttpMessageConverter() {
super(MediaType.valueOf("application/json-patch+json"), MediaType.APPLICATION_JSON);
}
#Override
protected boolean supports(Class<?> clazz) {
return JsonPatch.class.isAssignableFrom(clazz);
}
#Override
protected JsonPatch readInternal(Class<? extends JsonPatch> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try (JsonReader reader = Json.createReader(inputMessage.getBody())) {
return Json.createPatch(reader.readArray());
} catch (Exception e) {
throw new HttpMessageNotReadableException(e.getMessage(), inputMessage);
}
}
#Override
protected void writeInternal(JsonPatch jsonPatch, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
throw new NotImplementedException("The write Json patch is not implemented");
}
}
In addition to the mentioned above by Tomáš Mika I would add my comment / use-case.
Pre-conditions
Spring-Boot + Lombok + Jackson + JsonPatch usage
JsonPatchHttpMessageConverter - as defined above
At some point after colleagues cleanup commit all PATCH APIs have stopped working, by throwing the InvalidDefinitionException with error message:
Cannot construct instance of `javax.json.JsonValue` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: UNKNOWN; byte offset: #UNKNOWN]
After some time & investigation it was found that:
Exception has thrown by ObjectMapper deserializer, specifically in method:
protected Object _convert(Object fromValue, JavaType toValueType)
line #4388:
result = deser.deserialize(p, ctxt);
The reason was missing ObjectMapper bean, previously created to enable JsonPatch and lately removed by mistake in scope of some cleanup:
#Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.findAndRegisterModules();
}
This ObjectMapper bean has been explicitly defined, in addition to the extensive configuration:
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
#Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String dateFormatPattern;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.forEach(converter -> {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter;
ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
jacksonConverter.setPrettyPrint(true);
configureObjectMapper(objectMapper);
}
});
}
private void configureObjectMapper(ObjectMapper mapper) {
mapper.setTimeZone(TimeZone.getTimeZone("UTC"));
mapper.setDateFormat(new SimpleDateFormat(dateFormatPattern));
// https://programming.vip/docs/61b9674a30f8c.html
mapper.setVisibility(
mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
);
// Serializers
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
mapper.disable(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS);
// Deserializers
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
mapper.disable(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS);
mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
mapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
mapper.registerModule(hibernate5Module());
mapper.registerModule(new JavaTimeModule());
}
#Bean
public Module hibernate5Module() {
Hibernate5Module hibernate5Module = new Hibernate5Module();
hibernate5Module.enable(Hibernate5Module.Feature.REPLACE_PERSISTENT_COLLECTIONS);
return hibernate5Module;
}
}
I was trying to implement a custom serializer for one of the properties of my object to get a different JSON structure when I return it from my REST controller.
My constraints are I cannot change the interface of the REST controller or the model classes (so I cannot add extra annotation etc, that would maybe make this easier). The only thing I could think of, making it render different than described in the model is a custom serializer, if there are any better approaches for this, please don't hesitate to tell me a different approach that is within the constraints.
My models look something like this:
public class WrapperModel {
// a lot of autogenerated fields
List<Property> properties;
// getters/setters
}
public class Property {
private String name;
private String value;
// getters / setters
}
So when this is rendered is looks like so:
{ ....
"properties": [
{"key1": "value1"}, {"key2": "value2"},...
]
}
What I would want is this:
{ ....
"properties": {
"key1": "value1",
"key2": "value2",
...
}
}
The serializer for this is easy enough:
public class PropertyListJSONSerializer extends StdSerializer<List<Property>> {
//....
#Override
public void serialize(List<Property> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
for(Property p: value){
gen.writeStringField(p.getName(), p.getValue());
}
gen.writeEndObject();
}
}
Now when I try to register this serializer inside a #Configuration file:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new SimpleModule();
module.addSerializer(List<Property>.class, new PropertyListJSONSerializer());
mapper.registerModule(module);
return mapper;
}
this doesn't work, because List<Property>.class cannot be used for addSerializer since it's a template class. Is there any other way to add this serializer or something that does something similar?
I do not want to add a custom serializer for WrapperModel since this class is autogenerated and fields can be added and removed. This should be possible without modifying the application code (if I had a custom serializer you would need to add/remove the fields from the serializer also(?)). Or is there a way to just use the Standard serializer for the class and just manually handle this one List<> field.
The model classes are generated by the Spring Boot openapi code generator, so there is a very limited set of JSON annotations I can put on top of the model fields (if there's an annotation way, please dont hesitate to post as I can check in the openapi sourcecode if that particular annotation is supported). But I would rather go with either a custom serializer for List<Property> if that is at all possible or writing a serializer for WrapperModel that uses StdSerializer for everything and only handle the List property myself.
MixIn
In that case we need to use MixIn feature. Create interface like below:
interface WrapperModelMixIn {
#JsonSerialize(using = PropertyListJSONSerializer.class)
List<Property> getProperties();
}
and register it like below:
ObjectMapper mapper = new ObjectMapper();
mapper.addMixInAnnotations(WrapperModel.class, WrapperModelMixIn.class);
Older proposal
You need to use Jackson types which allow to register serialiser for generic type. Your serialiser after change could look like below:
class PropertyListJSONSerializer extends StdSerializer<List<Property>> {
public PropertyListJSONSerializer(JavaType type) {
super(type);
}
#Override
public void serialize(List<Property> value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
for (Property p : value) {
gen.writeStringField(p.getName(), p.getValue());
}
gen.writeEndObject();
}
}
And you can register it as below:
ObjectMapper mapper = new ObjectMapper();
CollectionType propertiesListType = mapper.getTypeFactory().constructCollectionType(List.class, Property.class);
SimpleModule module = new SimpleModule();
module.addSerializer(new PropertyListJSONSerializer(propertiesListType));
mapper.registerModule(module);
I am using fasterxml json with object mapper and below is my code:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
String jsonInString = mapper.writeValueAsString(myClassObjectHere);
return new ResponseEntity<String>(jsonInString, HttpStatus.OK);
} catch (JsonProcessingException e) {
e.printStackTrace();
return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
}
The way I have implemented and am using the code, i get desired output but json has 2 random strange values in certain json objects as follows:
{
"listing": {
"listingId": 1,
"name": "Business",
"handler": {},
"hibernateLazyInitializer": {}
},
"handler": {},
"hibernateLazyInitializer": {}
},
How to configure objectmappper to ignore "handler": {}, "hibernateLazyInitializer": {} values from outputted json ?
I tried solutions below:
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
But its not working and the output is still the same as i posted above.
Also, I know I can ignore these handler and hibernateLazyInitializer in json by annotating classes with #JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) but is there any way to globally configure json jackson object mapper so that it never adds these values in my outputted json?
You could try adding a mixin to Object.class:
public ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
return mapper;
}
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private abstract class IgnoreHibernatePropertiesInJackson{ }
I just ran into the same problem and found a way to globally get hibernate entities mapped correctly by adding the Hibernate4Module to the mapper:
objectMapper.registerModule(
new Hibernate4Module().configure(
Hibernate4Module.Feature.FORCE_LAZY_LOADING, true));
Seems like "handler": {}, "hibernateLazyInitializer": {} properties seems to be empty or null. You can use the following configuration to ignore empty or null properties.
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Or if you have access to the Class annotate handler and hibernateLazyInitializer with #JsonIgnore this will prevent it from being Serialized globally.
Adding Annotation over the class #JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler"}) worked for me.
I need to configure Jackson in a specific way which I'll describe below.
Requirements
Annotated fields are serialized with only their id:
If the field is a normal object, serialize its id
If the field is a collection of objects, serialize an array of id
Annotated fields get their property names serialized differently:
If the field is a normal object, add "_id" suffix to property name
If the field is a collection of objects, add "_ids" suffix to property name
For the annotation I was thinking something like a custom #JsonId, ideally with an optional value to override the name just like #JsonProperty does
The id property should be defined by the user, either using:
The already existing Jackson's #JsonIdentityInfo
Or by creating another class or field annotation
Or by deciding which annotation to inspect for id property discoverability (useful for JPA scenarios, for example)
Objects should be serialized with a wrapped root value
Camel case naming should be converted to lower case with underscores
All of this should be deserializable (by constructing an instance with just the id setted)
An example
Considering these POJO's:
//Inform Jackson which property is the id
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public abstract class BaseResource{
protected Long id;
//getters and setters
}
public class Resource extends BaseResource{
private String name;
#JsonId
private SubResource subResource;
#JsonId
private List<SubResource> subResources;
//getters and setters
}
public class SubResource extends BaseResource{
private String value;
//getters and setters
}
A possible serialization of a Resource instance could be:
{
"resource":{
"id": 1,
"name": "bla",
"sub_resource_id": 2,
"sub_resource_ids": [
1,
2,
3
]
}
}
So far...
Requirement #5 can be accomplished by configuring ObjectMapper in the following way:
objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
And then using #JsonRootName("example_root_name_here") in my POJO's.
Requirement #6 can be accomplished by configuring ObjectMapper in the following way:
objectMapper.setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
As you can see there are still lots of requirements to fulfill. For those wondering why I need such a configuration, it's because I'm developing a REST webservice for ember.js (more specifically Ember Data).
You would appreciate very much if you could help with any of the requirements.
Thanks!
Most (all?) of your requirements can be accomplished through the use of a contextual serializer. Taking one answer from ContextualDeserializer for mapping JSON to different types of maps with Jackson and Jackson's wiki (http://wiki.fasterxml.com/JacksonFeatureContextualHandlers) I was able to come up with the following.
You need to start with the #JsonId annotation, which is the key indicating a property needs to only use the Id property.
import com.fasterxml.jackson.annotation.*;
import java.lang.annotation.*;
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotation // important so that it will get included!
public #interface JsonId {
}
Next is the actual ContextualSerializer, which does the heavy lifting.
import com.fasterxml.jackson.databind.ser.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.core.*;
import java.io.*;
public class ContextualJsonIdSerializer
extends JsonSerializer<BaseResource>
implements ContextualSerializer/*<BaseResource>*/
{
private ObjectMapper mapper;
private boolean useJsonId;
public ContextualJsonIdSerializer(ObjectMapper mapper) { this(mapper, false); }
public ContextualJsonIdSerializer(ObjectMapper mapper, boolean useJsonId) {
this.mapper = mapper;
this.useJsonId = useJsonId;
}
#Override
public void serialize(BaseResource br, JsonGenerator jgen, SerializerProvider provider) throws IOException
{
if ( useJsonId ) {
jgen.writeString(br.getId().toString());
} else {
mapper.writeValue(jgen, br);
}
}
#Override
public JsonSerializer<BaseResource> createContextual(SerializerProvider config, BeanProperty property)
throws JsonMappingException
{
// First find annotation used for getter or field:
System.out.println("Finding annotations for "+property);
if ( null == property ) {
return new ContextualJsonIdSerializer(mapper, false);
}
JsonId ann = property.getAnnotation(JsonId.class);
if (ann == null) { // but if missing, default one from class
ann = property.getContextAnnotation(JsonId.class);
}
if (ann == null ) {//|| ann.length() == 0) {
return this;//new ContextualJsonIdSerializer(false);
}
return new ContextualJsonIdSerializer(mapper, true);
}
}
This class looks at BaseResource properties and inspects them to see if the #JsonId annotation is present. If it is then only the Id property is used, otherwise a passed in ObjectMapper is used to serialize the value. This is important because if you try to use the mapper that is (basically) in the context of the ContextualSerializer then you will get a stack overflow since it will eventually call these methods over and over.
You're resource should look something like the following. I used the #JsonProperty annotation instead of wrapping the functionality in the ContextualSerializer because it seemed silly to reinvent the wheel.
import java.util.*;
import com.fasterxml.jackson.annotation.*;
public class Resource extends BaseResource{
private String name;
#JsonProperty("sub_resource_id")
#JsonId
private SubResource subResource;
#JsonProperty("sub_resource_ids")
#JsonId
private List<SubResource> subResources;
//getters and setters
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public SubResource getSubResource() {return subResource;}
public void setSubResource(SubResource subResource) {this.subResource = subResource;}
public List<SubResource> getSubResources() {return subResources;}
public void setSubResources(List<SubResource> subResources) {this.subResources = subResources;}
}
Finally the method that performs the serialization just creates an additional ObjectMapper and registers a module in the original ObjectMapper.
// Create the original ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
// Create a clone of the original ObjectMapper
ObjectMapper objectMapper2 = new ObjectMapper();
objectMapper2.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
objectMapper2.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
objectMapper2.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
// Create a module that references the Contextual Serializer
SimpleModule module = new SimpleModule("JsonId", new Version(1, 0, 0, null));
// All references to SubResource should be run through this serializer
module.addSerializer(SubResource.class, new ContextualJsonIdSerializer(objectMapper2));
objectMapper.registerModule(module);
// Now just use the original objectMapper to serialize