Jackson Tips: custom List serialization

@cowtowncoder
5 min readMay 31, 2021

--

(how to serialize contents of List<String> in alphabetic order, using “helper” List subtype, custom JsonSerializer)

On Customizing Jackson value Serialization

Jackson-databind allows a wide range of customization options for formatting output of many types, both using configuration settings and annotations. But there are sometimes cases where more customization is necessary.

In case of non-structured (scalar) types and user-defined POJOs, it is quite easy to alternative just implement custom JsonSerializer if bigger changes to output are required. Such serializers may then be easily added using SimpleModule like so:

ObjectMapper mapper = JsonMapper.builder()
.addModule(new SimpleModule()
.addSerializer(new MyValueSerializer()
).build();

or using @JsonSerialize(using = MyValueSerializer.class) either on property, or value class (which we assume here to be MyValue).

However… Overriding serialization of structured/container types like Map and Collection are trickier: since they need to handle serialization of container as well as delegating serialization of contents, their registration is more complicated and is not supported by SimpleModule.
It is certainly possible to register serializers and deserializers for structured types, too, using Jackson Modules, but you do need to implement Serializers provider if taking that route.

But how about using @JsonSerialize annotation? Turns out that works pretty well for our hypothetical use case.

Use Case: Alphabetically ordering List<String> on serialization

So, let’s consider a case that I know has been requested as a feature (but not yet implemented as such): ability to make contents of a List to be serialized in their natural (for Strings, lexicographic (~= alphabetic)) order:

public class Values {
// Ids should be sorted alphabetically when serialized:
public List<String> ids;
public Values(String... ids) {
this.ids = Arrays.asList(ids);
}
}
ObjectMapper mapper = new JsonMapper();
assertEquals("{\"ids\":[\"a\",\"a",\"b\",\"c\"]}",
mapper.writeValueAsString(new Values("a", "c", "a", "b"))
);

Serializer itself would be relatively easy if we know that:

  1. It only needs to work for Strings (that is, List<String>)
  2. We do not need any general Jackson configuration to be applied
  3. No polymorphism (String is not polymorphic and List typically neither, specifically not here)

And we could write it something like so:

static class SortedListSerializer
extends JsonSerializer<List<String>>
{
public SortedListSerializer() { super(List.class, false); }
@Override
public void serialize(List<String> value, JsonGenerator g, SerializerProvider provider)
throws IOException
{
List<String> sorted = ...; // sort the entries into new list
g.writeStartArray();
for (String value : sorted) {
g.writeString(value);
}
g.writeEndArray();
}
}

But how do we register it?

Registering Custom List<String> serializer

How to register our custom serializer depends a little bit on exactly how we want it to apply. A simple mechanism if this is needed only in one or at most a few places; just attach it to property like so:

public class Values {
// note: for Lists, "using" serializes "List" itself,
// "contentUsing" would be used for value elements
@JsonSerialize(using = SortedListSerializer.class)
public List<String> ids;
}

and allows you to exactly specify where to use it. But it does require application in every place. It also would not work for “root values”; cases where you want to serialize such a List<String> as the full JSON value.

But we can actually add @JsonSerialize on Classes too, not just properties.
Maybe that could work?

Using @JsonSerialize as Class annotation for Fun & Profit

So, yes: @JsonSerialize is applicable to Classes. But wait a minute — we are talking about List type, which is part of JDK!
How could we annotate that class?

Actually, we could — this is what Jackson Mix-In annotations allow — but that would still be problematic for one big reason: if we attach it on List type, it would apply to EACH AND EVERY List<?> type! So if you had a List<MyPOJO>, voila, it would try to use our serializer and promptly fail. And since there is no way to attach annotations on specific generic type (at least not the way we’d need it), this does not seem like a way to go.

Except… what if we define a “helper type”? A specific sub-type like:

@JsonSerialize(using = SortedListSerializer.class)
public class SortedStringList extends ArrayList<String> {
public SortedStringList(String... ids) {
super(Arrays.asList(ids));
}
}

and with that, we can handle both Root values, and dependent ones:

String jsonForRoot = mapper.writeValueAsString(
new SortedStringList("foo", "bar"));
// -> ["bar","foo"]
// Note: SortedValues uses type "SortedStringList" for "values":
String jsonForValues = mapper.writeValueAsString(
new SortedValues("b", "d", "a"));
// -> {"value":["a","b","d"]}

which is what we set out to do.

This approach definitely has its limits, and its a bit intrusive (since we need to expose our new type — fortunately with default constructor, it should work just fine for deserialization too) — but it works wonders for a relatively common case of List (or Sets, Collections and Maps) with simple scalar values, but with some processing requirements. For example:

  • We may want to mask password/credential Strings
  • We may want to remove duplicates
  • We may want to produce a JSON String as concatenation of a List (serializers are not limited to “natural” mapping)
  • Perhaps we just wanted to write out number of Items in List as a count (size)

Helper Types For the Win!

Bit more generally, this technique of using “helper” (or “throw-away” or “one-off”) types is applicable more generally. It is especially powerful in avoiding dreaded “Generic Types as Root Values” problem where, for example:

@JsonTypeInfo(...) // we have a polymorphic type!
abstract class Animal {
}
public class Dog extends Animal { ... }List<Animal> animals = ...
String json = mapper.writeValueAsString(animals);
// PROBLEM! Why did my Animal values NOT get serialized as polymorphic?!?!// Solution ->public class AnimalList extends ArrayList<Animal> { }

Which is probably the most often reported issue that is due to limitations imposed by Java Type Erasure. What happens when attempting to serialize a List<Animal> instance is that JDK at runtime has no real knowledge of intended generic type: all you can see from instance is that it is a List<?> ; which is about same as List<Object> as far as Jackson knows — and java.lang.Object is not a polymorphic type.

To solve this problem, you can select one of 2 approaches:

  1. Use a helper type list AnimalList — this retains generic type information (since there is now a non-generic type with super type bindings)
  2. Construct a TypeReference (or JavaType), construct an ObjectWriter with it (ObjectMapper.writerFor(new TypeReference<List<Animal>>() { }), use that

both of which work. But while roughly equivalent, (1) can be much less work and even be bit more expressive (although opinions here vary).

Still, it is good to keep in mind the possibility of subtypes that can

  1. Provide type binding (turn generic into non-generic!)
  2. Add/override (Jackson) annotations
  3. Be configured separately from base types for settings not controlled by annotations

--

--

@cowtowncoder

Open Source developer, most known for Jackson data processor (nee “JSON library”), author of many, many other OSS libraries for Java, from ClassMate to Woodstox