Search code examples
javajsonspringjackson-databind

Swap Jackson custom serializer / deserializer during runtime


I have the following system:

enter image description here

  • I am sending MediaType.APPLICATION_JSON_VALUEs 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 the writeValueAsString and readValue 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:

enter image description here

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:

    1. 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.
    2. I registered my CustomerFileSerializer and CustomerFileDeserializer 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;