This is a short info on how to use unwrapping, custom Jackson Serializers. All the example code is inside the main repository: simple-meetup. That repository contains a Spring project, but the examples below work without Spring, too. The problem solved here tackles the questions asked here and especially here. This article helps you, to make your existing serializer working together with
@JsonUnwrappred
.TL;DR Don’t bother with
BeanSerializerModifier
andUnwrappingBeanSerializer
if you already have a custom and much simplerJsonSerializer
.
We use a lot of custom Jackson Serializers in the current project. A custom serializer can be used in all cases where annotations on fields and classes are not enough to customize the JSON output. They are pretty easy to use and their main contract looks like this:
static class UnwrappingRegistrationSerializer extends JsonSerializer<Registration> {
@Override
public void serialize(
final Registration value,
final JsonGenerator gen,
final SerializerProvider serializers
) throws IOException {
gen.writeStringField(nameTransformer.transform("name"), value.getName());
gen.writeStringField(nameTransformer.transform("email"), hideEmail(value.getEmail()));
}
}
The JsonGenerator
can be used to do customize all kinds of stuff: Using existing, external representation, generating fieldnames or manipulating values. In this sample project that deals with event registrations, I made up the use case of hiding an email address so that a bean like in Listing 2 renders as shown in Listing 3.
public final class Registration {
private String email;
private String name;
}
{
"email": "mic***********@innoq.com",
"name": "Michael"
}
That’s easy todo, in the body of Listing 1 I use something like this gen.writeStringField("email", hideEmail(value.getEmail()));
. And, to make it a JSON-object, I have to tell the generator to start and end one with gen.writeStartObject()
and gen.writeEndObject()
.
I don’t want to specifiy the custom serializers with annotations on my domain. Also Oliver was so nice pointing out that it might be a good idea to remove all that cruft from the domain and right he was.
By providing an instance of Module
to the Jackson ObjectMapper
custom serializers and MixIns (for replacing Jackson-Annotations in domain classes) can be registered:
public final class EventsModule extends SimpleModule {
public EventsModule() {
addSerializer(Registration.class, new RegistrationSerializer());
}
}
If you’re on Spring Boot, just instantiate a bean of such a module. If not, than add it manually to your ObjectMapper
instance as shown in Listing 5.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new EventsModule());
Now comes the interesting part: The above example works totally well in cases when a Registration
bean is serialized on it’s own or as attribute of another object. But: Not when it’s annotated with @JsonUnwrapped
. That annotation is used to pull all attributes of a given field up into the containing object. Wait, didn’t I just write that I cleaned all those annotations from my objects? Yes, I did. But I also use Spring HATEOAS respectively Spring Data REST in this project and I am building my resources like this:
@Relation(value = "registration", collectionRelation = "registrations")
public class RegistrationResource extends ResourceSupport {
@JsonUnwrapped (1)
private final Registration registration;
RegistrationResource(final Registration registration) {
this.registration = registration;
}
}
1 | This pulls the output of the registration beans JSON-representation up into the resource and directly into the embedded structure (see Listing 7) |
Together with a fitting Resource Assembler, which adds relations and stuff, I want to see a result as shown in Listing 7
{
"_embedded": {
"registrations": [
{
"email": "mic***********@innoq.com",
"name": "Michael"
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/events/2017-12-24/Xmas/registrations"
}
}
}
My customer uses a similar approach. If we would not use @JsonUnwrapped
, we’ll end up with something like:
{
"_embedded": {
"registrations": [
{
"registration": {
"email": "mic***********@innoq.com",
"name": "Michael"
}
}
]
}
}
And sadly, this is exactly what you’ll end up with using a naive custome serializer and the @JsonUnwrapped
annotation together. The solutions proposed in the StackOverFlow questions linked in the beginning are centered around using a custom UnwrappingBeanSerializer
that is registered as a beanSerializerModifier
. Those answers may work for you if your existing serializer is indeed a bean serializer, but not when working with a simple JsonSerializer
.
First: Write your serializer as shown in Listing 1. Take care not to start your serialization with opening object statements. Then override isUnwrappingSerializer
and return true:
@Override
public boolean isUnwrappingSerializer() {
return true;
}
Then, combine that unwrapping serializer with a wrapping one, that delegates its task:
static class RegistrationSerializer extends JsonSerializer<Registration> {
private final JsonSerializer<Registration> delegate
= new UnwrappingRegistrationSerializer(NameTransformer.NOP);
@Override
public void serialize(
final Registration value,
final JsonGenerator gen,
final SerializerProvider serializers
) throws IOException {
gen.writeStartObject(); (1)
this.delegate.serialize(value, gen, serializers);
gen.writeEndObject();
}
@Override
public JsonSerializer<Registration> unwrappingSerializer((2)
final NameTransformer nameTransformer
) {
return new UnwrappingRegistrationSerializer(nameTransformer);
}
}
1 | This thing writes whole objects, so we have to start and end one, in between we can use the original custom serializer |
2 | This This method is called when a serializer hits an JsonUnwrapped attribute and does exactly serve our purpose, it returns our serializer with an optional name transformer which might avoid name clashes |
Summing this up: Custom serializers can be marked as unwrapping. Non-unwrapping serializers provide a method to make them unwrapping. Both is somewhat bad documented, but works as expected. Other solutions based on instances of BeanSerializers
as proposed on StackOverFlow won’t work if you’re already have custom JsonSerializers
.
While my tips here works without any involvement of Spring or Spring Data REST, there’s another way called Projections that you might use with Spring Data REST. A very nice and clean way to reproject your data and maybe a bette fit for you. |