Jsonb: how to customize enum names
Jsonb is the standard JSON-Java mapper API. It is very simple and efficient. However some few details stay surprising. Let's detail the enum case in this post.
To illustrate our post let's take a very simple model. First a normal POJO:
public class Result {
private final State state;
public Result(final State state) {
this.state = state;
}
public State getState() {
return state;
}
}
Then our State enum:
public enum State {
ok,
ko
}
Nothing crazy, right? Now let's take the case where the code enum name does not match the serialization name. Most of the time you will just refactor the enum and that's it, however in several cases you can't:
- You wrote a library and part of the contract (either serialization or java) is already out since some version and you don't want to break the compatibility with the new version,
- The serialization name is not a value java name.
Immediately you think JSON-B is awesome and already have the right API for that so you convert it to:
public enum State {
@JsonbProperty("well")
ok,
@JsonbProperty("bad")
ko
}
Sounds good no? If you serialize a Result instance, you will still get ok and ko names instead of well and bad.
To solve that, the simplest is to write an adapter. The adapter will build a mapping Java-JSON in its constructor and then just use these two mappings to load the values. The mapping will indeed be maps. An HashMap is good enough in both cases but the one having the enum as key can also be an IdentityHashMap which should be a little bit faster if you are a purist. So here is the skeleton of our adapter:
public abstract class EnumMapperBase<A extends Enum<?>> implements JsonbAdapter<A, String> {
private final Map<String, A> jsonToJavaMapping = new HashMap<>();
private final Map<A, String> javaToJsonMapping = new IdentityHashMap<>();
protected EnumMapperBase() {
// todo
}
private Class<A> getEnumType() {
return Class.class.cast(ParameterizedType.class.cast(
getClass().getGenericSuperclass())
.getActualTypeArguments()[0]);
}
@Override
public String adaptToJson(final A obj) {
return javaToJsonMapping.get(obj);
}
@Override
public A adaptFromJson(final String obj) {
return ofNullable(jsonToJavaMapping.get(obj))
.orElseThrow(() -> new IllegalArgumentException("Unknown enum value: '" + obj + "'"));
}
}
The implementation is super simple: two maps and each one matching a mapper method. The only trick can be to throw - or not - an exception when an unknown value enters the mapper methods. Finally, this is a generic implementation which must be contextualize with an enum type to work, it is done just extending this class and passing the right generic and be able to extract the mapping:
public class EnumMapper extends EnumMapperBase<State> {
}
At that point we just miss the mapping. The simplest is to extract the enum type - this is ready in the abstract class we just created - and iterate over its constants and map each of them. Here is a potential implementation you can put in the constructor of the base class:
Stream.of(getEnumType().getEnumConstants()).forEach(constant -> {
final String asString;
try {
asString = ofNullable(
constant.getClass()
.getDeclaredField(constant.name())
.getAnnotation(JsonbProperty.class))
.map(JsonbProperty::value)
.orElseGet(constant::name);
} catch (final NoSuchFieldException e) {
throw new IllegalArgumentException(e);
}
jsonToJavaMapping.put(asString, constant);
javaToJsonMapping.put(constant, asString);
});
The value considered the JSON value - asString in previous snippet - follows default implementation in JSON-B, i.e. it uses the enum constant name but if you decorated the enum constant/value with @JsonbProperty then it will take the property name as we desired at the beginning of this post.
One you put it altogether your adapter is ready to use. To register it you have mainly two options:
- Register it for your enum globally,
- Register it for your enum only for one attribute of a particular model.
Last case is the simplest, you just add @JsonbTypeAdapter on the field you want to map:
public class Result {
@JsonbTypeAdapter(StateMapper.class)
private final State state;
// ... as before
public static class StateMapper extends EnumMapperBase<State> {}
}
However, if you prefer to register this mapping globally you must do it in your Jsonb instance:
try (final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
.withAdapters(new StateMapper()))) {
// ....
}
The withAdapters is the simplest way to register globally an adapter.
This way to customize an enum serialization - or more generally any object - is very very powerful. This post was just about the name but you can also duplicate the entries in the mapping to manage older versions (parameter renaming) or even tolerate multiple input types for a single field (String and List<String> mapping the same attribute if the key is the same or using singular/plural form).
Adapters are really powerful and generally easy to abstract to reuse accross a whole application so don't hesitate to write one!
From the same author:
In the same category: