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 and UnwrappingBeanSerializer if you already have a custom and much simpler JsonSerializer.

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:

Listing 1. JsonSerializer.java
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.

Listing 2. Registration.java
public final class Registration {
    private String email;

    private String name;
}
Listing 3. Desired rendering of the bean in Listing 2
{
    "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:

Listing 4. EventsModule.java
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.

Listing 5. Manual registration of a Jackson Module
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:

Listing 6. RegistrationResource.java
@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

Listing 7. Desired rendering a registrations resource
{
    "_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:

Listing 8. Wrapped embedded content, not desired
{
    "_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:

Listing 9. Make your serializer unwrapping
@Override
public boolean isUnwrappingSerializer() {
    return true;
}

Then, combine that unwrapping serializer with a wrapping one, that delegates its task:

Listing 10. Start and end an object and delegate the real serialization
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.