Hiep Doan

How to register a custom list serializer with Spring Boot / Jackson

April 20, 2018

Recently, I ran into a situation where I wanted to customize the Json serialization of a list in an API response.

TL/DR: you can find the full source code on my github: https://github.com/alexthered/spring-jackson-custom-list-serializer

Concretely, I have a list of language strings:

public class LanguageString {

    Locale locale;
    String value;
}

and it should be serialized to: (a map-like format instead of a Json list)

{
  "de": "hallo",
  "en": "hello",
  "fr": "bonjour"
}

To change the default json serialization with Jackson, you can implement a custom serializer and then you can either:

  1. Annotate every field that you want to use that serializer with @JsonSerializer(use = YourCustomSerializerClass.class). This is not a choice for us as we simply have the List everywhere and we want to set a default behaviour for it.
  2. Register your custom serializer with Jackson config and the data type. This normally works fine without collection type. After quite a bit of struggling to make it work and still failed, I came across the comment in Jackson’s SimpleModule.java:
/**
 * Method for adding serializer to handle values of specific type.

 * WARNING! Type matching only uses type-erased {@code Class} and should NOT
 * be used when registering serializers for generic types like

 * {@link java.util.Collection} and {@link java.util.Map}.
 */

So, it turns out that you can not add a custom serializer with generic type. I tried to google quite a bit but people just recommend to add annotation in the filed. Time to dig in the source code!!

Finally, after a bit more struggling, I found out that there is a class name SimpleSerializers where Jackson uses to define which serializer it should use for a collection type. So easy job now, you just need to override the method to point to your own custom serializer.

public class CollectionTypeJsonSerializer extends SimpleSerializers {
  
@Override
public JsonSerializer<?> findCollectionSerializer(SerializationConfig config, CollectionType type, BeanDescription beanDesc, 
TypeSerializer elementTypeSerializer,
JsonSerializer<Object> elementValueSerializer) {

    //if the collection is of type LanguageString, then use custom collection serializer

    if (isLanguageStringListType(type)) {

      return new LanguageStringListSerializer();

    }

    return findSerializer(config, type, beanDesc);

  }


  private boolean isLanguageStringListType(CollectionType type) {

    CollectionType languageStringArrayListType = TypeFactory.defaultInstance()

        .constructCollectionType(ArrayList.class, LanguageString.class);

    CollectionType languageStringListType = TypeFactory.defaultInstance()

        .constructCollectionType(List.class, LanguageString.class);

    return (type.equals(languageStringListType) || type.equals(languageStringArrayListType));

  }

}

And then, you can register your new SimpleSerializers by setting it as the serializer for a module in Jackson config:

@Configuration
public class JacksonConfig {

    @Bean

    public ObjectMapper jsonObjectMapper() {
        ArrayList<Module> modules = new ArrayList<>();
        //CollectionType Serialization
        SimpleModule collectionTypeSerializerModule = new SimpleModule();
        collectionTypeSerializerModule.setSerializers(new CollectionTypeJsonSerializer());
        modules.add(collectionTypeSerializerModule);
        return Jackson2ObjectMapperBuilder.json()
                .modules(modules)
                .build();
    }

}

Then that’s it.

I guess the take-away lesson here is that as a developer, sometimes it’s worthwhile (and fruitful) to dig into the source code, instead of just googling around.


Hiep Doan

Written by Hiep Doan, a software engineer living in the beautiful Switzerland. I am passionate about building things which matters and sharing my experience along the journey. I also selectively post my articles in Medium. Check it out!