Jackson custom serializer to use in Optional<?> [duplicate] - java

This question already has an answer here:
Is there some way to specify a custom deserializer for a type that is nested inside an Optional for Jackson?
(1 answer)
Closed 3 years ago.
Say I have a simple String custom serializer and deserializer.
public class SimpleSerializer extends StdSerializer<String> {
protected SimpleSerializer() {
super(String.class);
}
#Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value == null) {
return;
}
value = doThings(value);
gen.writeString(value);
}
}
I use this on various String fields but now I need to use it on an Optional<String> field too.
#JsonSerialize(using = SimpleSerializer.class)
#JsonDeserialize(using = SimpleDeserializer.class)
private Optional<String> item;
This doesn't work unfortunately since the class does not match. And I can't assign it like Optional<#JsonSerialize... String> item because the annotation is not marked with TYPE_USE target. I tried creating custom annotation with TYPE_USE, #JacksonAnnotationsInside and the others as a workaround but that didn't work either, which is not a surprise.
Is there any way that I can use the serializer and the deserializer without creating another one for specifically Optional<String>?

First, include the Jackson Module for Optional. It's part of
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson.version}</version>
</dependency>
Second, register it with your ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
Finally, annotate your field with with #JsonSerialize and #JsonDeserialize but use their contentUsing element to specify your custom serializer/deserializer
#JsonSerialize(contentUsing = SimpleSerializer.class)
#JsonDeserialize(contentUsing = SimpleDeserializer.class)
The JsonSerializer used to serialize Optional fields, OptionalSerializer, will know to delegate to SimpleSerializer to perform the actual serialization. The same behavior applies to deserialization.

Related

#Jacksonized deserialization doesn't work with mixins - "no String-argument constructor/factory method to deserialize from String value"

I'm trying to deserialize an array of MyObject (which uses the builder pattern via Lombok with #Jacksonized) from a csv String containing a non-standard representation of a map as one of the columns.
MyObject:
#JsonPropertyOrder({
"strField",
"mapField",
})
#Getter
#Jacksonized
#Builder(toBuilder = true)
#EqualsAndHashCode
#ToString
public class MyObject {
private final String strField;
#Builder.Default
private final Map<String, Float> mapField = new HashMap<>();
}
Example csv with non-standard mapField representation:
strField,mapField
abc,"key1=2.0;key2=3.0"
I'm using a mixin to try to achieve this without rewriting the entire object:
#JsonPropertyOrder({
"strField",
"mapField",
})
public abstract class MyObjectDeserializerMixin {
#JsonDeserialize(using = StringToMapDeserializer.class)
private Map<String, Float> mapField;
}
..which, as you can see above, points to a custom deserializer:
public class StringToMapDeserializer extends JsonDeserializer<Map<String, Float>> {
#Override
public Map<String, Float> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String csvFormattedMap = jsonParser.getText().trim();
return Arrays.stream(csvFormattedMap.split(";"))
.map(keyValue -> keyValue.split("="))
.collect(Collectors.toMap(keyValue -> keyValue[0], keyValue -> Float.parseFloat(keyValue[1])));
}
}
And, to wrap it all up, I'm configuring and using my CsvMapper like so:
CsvMapper csvMapper = new CsvMapper();
csvMapper.addMixIn(MyObject.class, MyObjectDeserializerMixin.class);
CsvSchema csvSchema = csvMapper
.schemaFor(MyObject.class)
.withHeader();
ObjectReader csvReader = csvMapper.readerFor(MyObject.class).with(csvSchema);
List<MyObject> myObjects = csvReader.<MyObject>readValues(theCsvString).readAll();
However, I'm getting the following exception:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
construct instance of java.util.LinkedHashMap (although at least one
Creator exists): no String-argument constructor/factory method to
deserialize from String value ('key1=2.0;key2=3.0') at [Source:
(StringReader); line: 2, column: 53] (through reference chain:
myPackage.MyObject$MyObjectBuilder["mapField"])
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062)
at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)
at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromString(ValueInstantiator.java:258)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:357)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:29)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeSetAndReturn(MethodProperty.java:158)
at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.vanillaDeserialize(BuilderBasedDeserializer.java:269)
at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserialize(BuilderBasedDeserializer.java:193)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1719)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1261)
...
The stack trace appears to be trying to use a BuilderBasedDeserializer which tries to use a MapDeserializer.java for the map so it doesn't appear to be aware of my custom deserializer. I've used a very similar workflow with a custom serializer to write the same csv, so I'm confused as to why this doesn't work. What is the next step to troubleshooting this?
When using a builder to deserialize, Jackson only considers the annotations on the builder class, not those on the actual class to deserialize. Lombok's #Jacksonized helps you by automatically copying all relevant annotations to the builder class and its setter methods.
However, Lombok can only do that with annotations statically present on the class. Any dynamic annotation coming from a mixin cannot be copied, because Lombok doesn't know about them.
You could put #JsonDeserialize onto the mapField of the actual class, so that Lombok is able to copy it to the builder. But that obviously runs contrary to the purpose of mixins.
Luckily, there is a better way. You can also add the mixin to the builder class as follows:
csvMapper.addMixIn(MyObjectBuilder.class, MyObjectDeserializerMixin.class);
Strictly speaking, you do not need the mixin on MyObject any more. But if you also serialize and there are annotations relevant to serialization in the mixin, you should add the mixin to both.
However, in your case, that's not sufficient, as you are using #Builder.Default. With that annotation, Lombok creates a field named mapField$value in the builder. Jackson will not match the field mapField (and its annotation) from your mixin to that field, as they are named differently.
You can work around that by defining and annotating the setter method in your mixin:
public abstract class MyObjectDeserializerMixin {
#JsonDeserialize(using = StringToMapDeserializer.class)
public abstract void mapField(Map<String, Float> mapField);
}
You can use the actual return type of the builder here, but void is also sufficient. As #JsonDeserialize is only for deserialization purposes, you can safely remove the mapField and its annotation from the mixin class.
Tested with Lombok 1.18.20 and Jackson 2.12.2.

How to ignore only #JsonSerialize annotations in combination with DefaultTyping

I have seen the many questions and answers about ignoring certain annotations, or even disabling all annotations:
.configure(MapperFeature.USE_ANNOTATIONS, false)
But
the first solution defeats DefaultTyping (types don't end up in the serialized JSON and
the second solution defeats many useful annotations, of which the most critical to us: java.beans.ConstructorProperties.
How can I ignore #JsonSerialize and still have typeinfo in my resulting JSON while still supporting other annotations such as ConstructorProperties?
Here's what I have so far:
private static ObjectMapper configureObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, JsonTypeInfo.As.PROPERTY);
mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
protected TypeResolverBuilder<?> _findTypeResolver(MapperConfig<?> config, Annotated ann, JavaType baseType) {
if (ann.hasAnnotation(JsonSerialize.class) || ann.hasAnnotation(JsonDeserialize.class)) {
return StdTypeResolverBuilder.noTypeInfoBuilder(); // or null
}
return super._findTypeResolver(config, ann, baseType);
}
});
return mapper;
}
// or the same config using a JsonMapper builder
But this still processes #JsonSerialize for some reason. I'm on jackson 2.10.0.pr3.
The real problem I'm facing is that I'm serializing 3rd party objects, which contain provided #JsonSerialize for unrelated purposes, but without defining #JsonDeserialize. Even though they're perfectly serializable without these annotations, they end up blocking our deserialization. At the same time I don't know upfront which classes they are, so these should be encoded in the resulting JSON. Furthermore, some of these objects are generated with Lombok resulting in no-arg constructors annotated with java.beans.ConstructorProperties which Jackson can deal with fine under normal circumstances.
It's the above combination of configuration I'm not able to solve.
Have you tried to override JacksonAnnotationIntrospector#findSerializer method:
class SkipSerializersJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
private final List<Class> classesToSkip = new ArrayList<>();
public SkipSerializersJacksonAnnotationIntrospector() {
classesToSkip.add(YourClass.class);
}
#Override
public Object findSerializer(Annotated a) {
if (classesToSkip.contains(a.getRawType())) {
return null;
}
return super.findSerializer(a);
}
}
It should allow you to skip JsonSerialize annotation and keep type.

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

ObjectMapper can't handle mapping object with legacy enums (classes)

I'm having an issue with building REST architecture for some legacy code.
Jackson ObjectMapper is unable to map my custom object to legacy object, because of 'enums' that really are classes with static final fields.
I tried implementing custom converters/deserializers with no success
In the legacy system there are enums that look like this:
public final class LegacyEnum extends LegacyEnumSuperclass {
public static final LegacyEnum VALUE = new LegacyEnum("1");
I'm receiving values of these 'enums' as Strings, that I convert to legacy enum values (custom deserializer) and set them in my custom class (I need it because I'm using jackson annotations, and I have no access or permission to modify legacy code) and this part works nicely. When I try to map my custom object to legacy object with
objectMapper.convertValue(myCustomObject, LegacyObjectContainingEnums.class);
I get an exception:
Can not construct instance of LegacyEnum: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
The LegacyEnum class has a private constructor, and LegacyEnumSuperclass has a similar protected constructor so I cannot access them (neither can ObjectMapper). I have tried implementing a custom converter, that would skip the 'create new object' part of ObjectMapper mapping, and I also tried to reuse my custom deserializer. I ran into multiple issues and achieved no success.
The most annoying part is, that when I use ModelMapper library it works like a charm (it probably just sets a value in the legacy object, no need to create new LegacyEnum instance like ObjectMapper!) but I'm trying to resolve that issue without adding new dependencies.
Any ideas?
I resolved the issue by using MixIn and custom deserializer, like this:
public abstract class LegacyClassMixIn
#JsonDeserialize(using = LegacyEnumDeserializer.class)
abstract LegacyEnum getLegacyEnum();
Deserializer:
public class LegacyEnumDeserializer extends JsonDeserializer<LegacyEnumSuperclass> implements ContextualDeserializer {
private JavaType valueType;
#Override
public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) {
JavaType wrapperType = property.getType();
LegacyEnumDeserializer deserializer = new LegacyEnumDeserializer();
deserializer.valueType = wrapperType;
return deserializer;
}
#Override
public LegacyEnumSuperclass deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return LegacyEnumSuperclass.getEnum(valueType.getRawClass(), parser.readValueAs(String.class));
}
valueType.getRawClass() returns LegacyEnum.class, that way I can use one deserializer for all of the 'enums' that inherit LegacyEnumSuperclass class. getEnum is a custom method, from the legacy code.
Registering MixIn in ObjectMapper Spring configuration:
#Configuration
public class ObjectMapperConfig {
public ObjectMapperConfig(ObjectMapper objectMapper) {
objectMapper.addMixIn(LegacyClass.class, LegacyClassMixIn.class);
}
}
That way I can use LegacyClass as a parameter in a Controller method.
Thanks for clues.

POJO deserialization property of interface type withoud impementation class with Jackson

I have a problem with deserialization json into POJO class which looks like this:
#Data
public class Foo {
private String fieldA;
private String fieldB;
private IBar fieldC;
}
IBar is an interface which defines getters for some classes. One of the solutions what I found is to use #JsonDeserialize(as = BarImpl.class) where BarImpl will implement IBar interface. Problem is classes which implement that interface (for instance BarImpl) are in another maven module where I don't have access from current module so I cannot use one of this impl classes in that annotation. Can you tell me if there is another solution?
Thank you in advice.
Are you sure you mean deserialization? You'll need a concrete implementation of your interface if Jackson's going to be able to create Java objects for you.
deserialization = Json String -> Java object
serialization = Java object -> Json String
When serializing Jackson will use the runtime class of the object, so it will use the actual implementations rather than attempt to use the interface. If you want to customize this you can add a serializer for your interface. You'll need to decide exactly what you want to write out.
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(IBar.class, new JsonSerializer<IBar>() {
#Override
public void serialize(IBar value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("fieldName", value.getFieldName());
gen.writeEndObject();
}
});
objectMapper.registerModule(module);

Categories