Issue
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.
Solution
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;
Answered By - Bishares