Jackson deserialise JsonPatch with object value - java

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;
}
}

Related

Spring Boot custom serializer for Collection class

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);

Jackson deserialize different strings to the same enum constant

Let's suppose I have an enum Status that looks like this.
public enum Status {
SUCCESS,
FAIL,
RETRY,
UNKNOWN
}
I am receiving status property from a JSON that could look like following examples.
{"status":"success"} // valid case, deserialize to Status.SUCCESS
{"status":"fail"} // valid case, deserialize to Status.FAIL
{"status":"retry"} // valid case, deserialize to Status.RETRY
But any other value should be deserialized to Status.UNKNOWN. Examples.
{"status":"blabla"} // invalid case, deserialize to Status.UNKNOWN
{"status":"John"} // invalid case, deserialize to Status.UNKNOWN
I know I could do it by writing a custom deserializer, but I'd try to avoid that because i have many, many enums in my program, and requiring a custom deserializer for each of them would be an overkill.
Ideally, some kind of constructor from regex that matches any string (except for the "success", "fail" and "retry").
Is there a way to do it with Jackson without writing custom deserializer?
If all of your enums have UNKNOWN value, you can write one custom deserializer like this:
class EnumDeserializer extends JsonDeserializer<Enum> {
private final Class<? extends Enum> enumType;
public EnumDeserializer(Class<? extends Enum> enumType) {
this.enumType = enumType;
}
#Override
public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
try {
String stringValue = jp.getValueAsString().toUpperCase();
return Enum.valueOf(enumType, stringValue.toUpperCase());
} catch (IllegalArgumentException e) {
return Enum.valueOf(enumType, "UNKNOWN");
}
}
}
And configure your mapper to user it:
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<Enum> modifyEnumDeserializer(DeserializationConfig config,
final JavaType type,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new EnumDeserializer((Class<Enum<?>>) type.getRawClass());
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Alternatively you can use jackson deserialization feature for setting default value for unknown enum properties:
enum MyEnum { A, B, #JsonEnumDefaultValue UNKNOWN }
...
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
MyEnum value = mapper.readValue("\"foo\"", MyEnum.class);
assertSame(MyEnum.UNKNOWN, value);
But with such approach you'll need to change all your enums to use #JsonEnumDefaultValue annotation for default value, plus by default it doesn't handle lowercase enum values.

How to have the Jackson serializer fail on required attributes that are null

I would like the Jackson JSON serializer to fail if it encounters required attributes that are null.
I know I can tell it to omit null fields (mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)). However, in addition I'd like to declare
#NotNull // BeanValidation
private String foo;
or
#JsonProperty(required = true) // Jackson databinding
private String foo;
and have the serializer fail if such fields are null.
I don't see any annotation or configuration option for that. You can use hibernate-validator to validate an object before serializing. Since you don't want to add custom annotations you can modify the default serializer by having your objects validated before serialization.
First create a custom serializer that takes another one as constructor argument and uses hibernate validator to validate objects and throw an exception.
class ValidatingSerializer extends JsonSerializer<Object> {
private final JsonSerializer<Object> defaultSerializer;
private final Validator validator;
ValidatingSerializer(final JsonSerializer<Object> defaultSerializer) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
this.validator = factory.getValidator();
this.defaultSerializer = defaultSerializer;
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (!validator.validate(value).isEmpty()) {
throw new IOException("Null value encountered");
}
defaultSerializer.serialize(value, gen, provider);
}
}
Then create serializer modifier that will use this serializer:
class ValidatingSerializerModifier extends BeanSerializerModifier {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config,
BeanDescription beanDesc, JsonSerializer<?> serializer) {
return new ValidatingSerializer((JsonSerializer<Object>) serializer);
}
}
Finally register this on you ObjectMapper with a SimpleModule:
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new ValidatingSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
This will now be used and exceptions will be thrown whenever validation fails for fields annotated with standard validation annotations:
#NotNull // BeanValidation
private String foo;

How to skip Optional.empty fields during Jackson serialization?

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.

Jackson - Disallow null for object-properties and array-elements during deserialization

Is there an option in Jackson to let the deserialization fail when it encounters a null for any (non-primitive) object property or for any (non-primitive) array element?
It should work similarly to the Deserialization Feature - FAIL_ON_NULL_FOR_PRIMITIVES).
Example #1
So deserializing {"name": null} should throw an exception when deserializing into a POJO
class User {
private String name = "Default name";
//+setters/getters
}
However, it should work fine and throw no exception when deserializing {} into that POJO, as the default value for the name field is not overwritten (see comments).
Example #2
I would like to avoid null elements in arrays also, so deserializing ["A", "B", null] should throw an exception when deserializing into List<String>.
There is no easy way to do this as far as I know (jackson-databind 2.4.2).
I suggest you take a look at using custom constructors / factory methods for creating objects out of Json. That allows you to do more advanced validation of incoming Json strings.
Solution to example #1
You can add this feature by registering a SimpleModule with an added BeanDeserializerModifier in order to alter the deserialization functionality.
By overriding the appropriate method you can use the default JsonDeserializer to deserialize the object easily and throw a mapping exception if a null property occurs.
You can find details in the answers of a similar SO question.
Extend the existing deserialization:
//instantiate an objectMapper and alter the deserialization functionality
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
return new DisallowNullDeserializer(beanDesc.getBeanClass(), deserializer);
}
});
mapper.registerModule(simpleModule);
The actual deserialization and exception throwing is happening in this utility class:
public class DisallowNullDeserializer<T> extends StdDeserializer<T> implements ResolvableDeserializer {
private final JsonDeserializer<?> delegateDeserializer;
public DisallowNullDeserializer(Class<T> clazz, JsonDeserializer<?> delegateDeserializer) {
super(clazz);
this.delegateDeserializer = delegateDeserializer;
}
#Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// delegate actual deserialization to default deserializer
T out = (T) delegateDeserializer.deserialize(jp, ctxt);
// check for null properties & throw exception
// -> there may be a better, more performant way to find null properties
Map<String, Object> propertyMap = mapper.convertValue(out, Map.class);
for (Object property: propertyMap.values())
if (property == null)
throw ctxt.mappingException("Can not map JSON null values.");
return out;
}
// there is no obvious reason why this is needed; see linked SO answers
#Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) delegateDeserializer).resolve(ctxt);
}
}

Categories