I have the following system:
- I am sending
MediaType.APPLICATION_JSON_VALUE
s from spring controllers to my client and vice versa. - I also have an export/import feature of my to-be-serialized classes. The JSON File is created by using an
ObjectMapper
and utilizing thewriteValueAsString
andreadValue
methods. I am reading from and writing into the json file. - Both of those serialization paths currently utilize the same serializers/deserializers.
I use the @JsonSerialize
and @JsonDeserialize
annotations to define custom serialization for some of my objects.
I want to serialize those objects differently for export/import.
So I want to swap the serializer / deserializer for the export/import task. Something like this:
If I understand the docs correctly, those two annotations only allow one using
class. But I want to register multiple serializers/deserializers and use them based on some conditional logic.
Advertisement
Answer
This is my solution
It’s not pretty but does its job.
I left my old jackson config untouched, so the client<->server serialization stays the same. I then added this custom ObjectMapper to take care of my server<->file.
My custom ObjectMapper does the following things:
- It registers a new custom JacksonAnnotationIntrospector, which I configured to ignore certain annotations. I also configured it to use my selfmade annotation
@TransferJsonTypeInfo
whenever a property has both the@TransferJsonTypeInfo
as well as the@JsonTypeInfo
annotation. - I registered my
CustomerFileSerializer
andCustomerFileDeserializer
for this ObjectMapper.
@Service public class ImportExportMapper { protected final ObjectMapper customObjectMapper; private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector(); public ImportExportMapper(){ customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false); // emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION); customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule module = new SimpleModule(); module.addSerializer(Customer.class, new CustomerFileSerializer()); module.addDeserializer(Customer.class, new CustomerFileDeserializer()); customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO); customObjectMapper.registerModule(module); } public String writeValueAsString(Object data) { try { return customObjectMapper.writeValueAsString(data); } catch (JsonProcessingException e) { e.printStackTrace(); throw new IllegalArgumentException(); } } public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException { return customObjectMapper.readValue(fileContent, clazz); } private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() { return new JacksonAnnotationIntrospector() { @Override protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) { if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) { FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class); if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) { return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class } } if (ignoreJsonAnnotations(annoClass)) return null; return super._findAnnotation(annotated, annoClass); } }; } private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) { if (annoClass == JsonSerialize.class) { return true; } if(annoClass == JsonDeserialize.class){ return true; } if(annoClass == JsonIdentityReference.class){ return true; } return annoClass == JsonIdentityInfo.class; } }
My custom annotation is defined and described like this:
/** * This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize * objects that already have a wrongly defined @JsonTypeInfo annotation (wrongly defined for the importExportMapper). * * Idea is taken from here: https://stackoverflow.com/questions/58495480/how-to-properly-override-jacksonannotationintrospector-findannotation-to-replac */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface FileJsonTypeInfo { JsonTypeInfo jsonTypeInfo(); }
And it is used like this:
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") @JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE) @TransferJsonTypeInfo(jsonTypeInfo = @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier")) @JsonIdentityReference(alwaysAsId = true) @JsonDeserialize(using = CustomerClientDeserializer.class) @JsonSerialize(using = CustomerClientSerializer.class) private Customer customer;