Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

This article covers some additional annotations that were not covered in the previous article, A Guide to Jackson Annotations – we will go through seven of these.

2. @JsonIdentityReference

@JsonIdentityReference is used for customization of references to objects that will be serialized as object identities instead of full POJOs. It works in collaboration with @JsonIdentityInfo to force usage of object identities in every serialization, different from all-but-the-first-time when @JsonIdentityReference is absent. This couple of annotations is most helpful when dealing with circular dependencies among objects. Please refer to section 4 of the Jackson – Bidirectional Relationship article for more information.

In order to demonstrate the use @JsonIdentityReference, we will define two different bean classes, without and with this annotation.

The bean without @JsonIdentityReference:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class BeanWithoutIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

For the bean using @JsonIdentityReference, we choose the id property to be the object identity:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
public class BeanWithIdentityReference {
    private int id;
    private String name;
    
    // constructor, getters and setters
}

In the first case, where @JsonIdentityReference is absent, that bean is serialized with full details on its properties:

BeanWithoutIdentityReference bean 
  = new BeanWithoutIdentityReference(1, "Bean Without Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);

The output of the serialization above:

{
    "id": 1,
    "name": "Bean Without Identity Reference Annotation"
}

When @JsonIdentityReference is used, the bean is serialized as a simple identity instead:

BeanWithIdentityReference bean 
  = new BeanWithIdentityReference(1, "Bean With Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);
assertEquals("1", jsonString);

3. @JsonAppend

The @JsonAppend annotation is used to add virtual properties to an object in addition to regular ones when that object is serialized. This is necessary when we want to add supplementary information directly into a JSON string, rather than changing the class definition. For instance, it might be more convenient to insert the version metadata of a bean to the corresponding JSON document than to provide it with an additional property.

Assume we have a bean without @JsonAppend as follows:

public class BeanWithoutAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

A test will confirm that in the absence of the @JsonAppend annotation, the serialization output does not contain information on the supplementary version property, despite the fact that we attempt to add to the ObjectWriter object:

BeanWithoutAppend bean = new BeanWithoutAppend(2, "Bean Without Append Annotation");
ObjectWriter writer 
  = mapper.writerFor(BeanWithoutAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

The serialization output:

{
    "id": 2,
    "name": "Bean Without Append Annotation"
}

Now, let’s say we have a bean annotated with @JsonAppend:

@JsonAppend(attrs = { 
  @JsonAppend.Attr(value = "version") 
})
public class BeanWithAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

A similar test to the previous one will verify that when the @JsonAppend annotation is applied, the supplementary property is included after serialization:

BeanWithAppend bean = new BeanWithAppend(2, "Bean With Append Annotation");
ObjectWriter writer 
  = mapper.writerFor(BeanWithAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

The output of that serialization shows that the version property has been added:

{
    "id": 2,
    "name": "Bean With Append Annotation",
    "version": "1.0"
}

4. @JsonNaming

The @JsonNaming annotation is used to choose the naming strategies for properties in serialization, overriding the default. Using the value element, we can specify any strategy, including custom ones.

In addition to the default, which is LOWER_CAMEL_CASE (e.g. lowerCamelCase), Jackson library provides us with four other built-in property naming strategies for convenience:

  • KEBAB_CASE: Name elements are separated by hyphens, e.g. kebab-case.
  • LOWER_CASE: All letters are lowercase with no separators, e.g. lowercase.
  • SNAKE_CASE: All letters are lowercase with underscores as separators between name elements, e.g. snake_case.
  • UPPER_CAMEL_CASE: All name elements, including the first one, start with a capitalized letter, followed by lowercase ones and there are no separators, e.g. UpperCamelCase.

This example will illustrate the way to serialize properties using snake case names, where a property named beanName is serialized as bean_name.

Given a bean definition:

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class NamingBean {
    private int id;
    private String beanName;

    // constructor, getters and setters
}

The test below demonstrates that the specified naming rule works as required:

NamingBean bean = new NamingBean(3, "Naming Bean");
String jsonString = mapper.writeValueAsString(bean);        
assertThat(jsonString, containsString("bean_name"));

The jsonString variable contains following data:

{
    "id": 3,
    "bean_name": "Naming Bean"
}

5. @JsonPropertyDescription

The Jackson library is able to create JSON schemas for Java types with the help of a separate module called JSON Schema. The schema is useful when we want to specify expected output when serializing Java objects, or to validate a JSON document before deserialization.

The @JsonPropertyDescription annotation allows a human readable description to be added to the created JSON schema by providing the description field.

This section makes use of the bean declared below to demonstrate the capabilities of @JsonPropertyDescription:

public class PropertyDescriptionBean {
    private int id;
    @JsonPropertyDescription("This is a description of the name property")
    private String name;

    // getters and setters
}

The method for generating a JSON schema with the addition of the description field is shown below:

SchemaFactoryWrapper wrapper = new SchemaFactoryWrapper();
mapper.acceptJsonFormatVisitor(PropertyDescriptionBean.class, wrapper);
JsonSchema jsonSchema = wrapper.finalSchema();
String jsonString = mapper.writeValueAsString(jsonSchema);
assertThat(jsonString, containsString("This is a description of the name property"));

As we can see, the generation of JSON schema was successful:

{
    "type": "object",
    "id": "urn:jsonschema:com:baeldung:jackson:annotation:extra:PropertyDescriptionBean",
    "properties": 
    {
        "name": 
        {
            "type": "string",
            "description": "This is a description of the name property"
        },

        "id": 
        {
            "type": "integer"
        }
    }
}

6. @JsonPOJOBuilder

The @JsonPOJOBuilder annotation is used to configure a builder class to customize deserialization of a JSON document to recover POJOs when the naming convention is different from the default.

Suppose we need to deserialize the following JSON string:

{
    "id": 5,
    "name": "POJO Builder Bean"
}

That JSON source will be used to create an instance of the POJOBuilderBean:

@JsonDeserialize(builder = BeanBuilder.class)
public class POJOBuilderBean {
    private int identity;
    private String beanName;

    // constructor, getters and setters
}

The names of the bean’s properties are different from those of the fields in JSON string. This is where @JsonPOJOBuilder comes to the rescue.

The @JsonPOJOBuilder annotation is accompanied by two properties:

  • buildMethodName: The name of the no-arg method used to instantiate the expected bean after binding JSON fields to that bean’s properties. The default name is build.
  • withPrefix: The name prefix for auto-detection of matching between the JSON and bean’s properties. The default prefix is with.

This example makes use of the BeanBuilder class below, which is used on POJOBuilderBean:

@JsonPOJOBuilder(buildMethodName = "createBean", withPrefix = "construct")
public class BeanBuilder {
    private int idValue;
    private String nameValue;

    public BeanBuilder constructId(int id) {
        idValue = id;
        return this;
    }

    public BeanBuilder constructName(String name) {
        nameValue = name;
        return this;
    }

    public POJOBuilderBean createBean() {
        return new POJOBuilderBean(idValue, nameValue);
    }
}

In the code above, we have configured the @JsonPOJOBuilder to use a build method called createBean and the construct prefix for matching properties.

The application of @JsonPOJOBuilder to a bean is described and tested as follows:

String jsonString = "{\"id\":5,\"name\":\"POJO Builder Bean\"}";
POJOBuilderBean bean = mapper.readValue(jsonString, POJOBuilderBean.class);

assertEquals(5, bean.getIdentity());
assertEquals("POJO Builder Bean", bean.getBeanName());

The result shows that a new data object has been successfully re-created from a JSON source in despite a mismatch in properties’ names.

7. @JsonTypeId

The @JsonTypeId annotation is used to indicate that the annotated property should be serialized as the type id when including polymorphic type information, rather than as a regular property. That polymorphic metadata is used during deserialization to recreate objects of the same subtypes as they were before serialization, rather than of the declared supertypes.

For more information on Jackson’s handling of inheritance, see section 2 of the Inheritance in Jackson.

Let’s say we have a bean class definition as follows:

public class TypeIdBean {
    private int id;
    @JsonTypeId
    private String name;

    // constructor, getters and setters
}

The following test validates that @JsonTypeId works as it is meant to:

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
TypeIdBean bean = new TypeIdBean(6, "Type Id Bean");
String jsonString = mapper.writeValueAsString(bean);
        
assertThat(jsonString, containsString("Type Id Bean"));

The serialization process’ output:

[
    "Type Id Bean",
    {
        "id": 6
    }
]

8. @JsonTypeIdResolver

The @JsonTypeIdResolver annotation is used to signify a custom type identity handler in serialization and deserialization. That handler is responsible for conversion between Java types and type id included in a JSON document.

Suppose that we want to embed type information in a JSON string when dealing with the following class hierarchy.

The AbstractBean superclass:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "@type"
)
@JsonTypeIdResolver(BeanIdResolver.class)
public class AbstractBean {
    private int id;

    protected AbstractBean(int id) {
        this.id = id;
    }

    // no-arg constructor, getter and setter
}

The FirstBean subclass:

public class FirstBean extends AbstractBean {
    String firstName;

    public FirstBean(int id, String name) {
        super(id);
        setFirstName(name);
    }

    // no-arg constructor, getter and setter
}

The LastBean subclass:

public class LastBean extends AbstractBean {
    String lastName;

    public LastBean(int id, String name) {
        super(id);
        setLastName(name);
    }

    // no-arg constructor, getter and setter
}

Instances of those classes are used to populate a BeanContainer object:

public class BeanContainer {
    private List<AbstractBean> beans;

    // getter and setter
}

We can see that the AbstractBean class is annotated with @JsonTypeIdResolver, indicating that it uses a custom TypeIdResolver to decide how to include subtype information in serialization and how to make use of that metadata the other way round.

Here is the resolver class to handle inclusion of type information:

public class BeanIdResolver extends TypeIdResolverBase {
    
    private JavaType superType;

    @Override
    public void init(JavaType baseType) {
        superType = baseType;
    }

    @Override
    public Id getMechanism() {
        return Id.NAME;
    }

    @Override
    public String idFromValue(Object obj) {
        return idFromValueAndType(obj, obj.getClass());
    }

    @Override
    public String idFromValueAndType(Object obj, Class<?> subType) {
        String typeId = null;
        switch (subType.getSimpleName()) {
        case "FirstBean":
            typeId = "bean1";
            break;
        case "LastBean":
            typeId = "bean2";
        }
        return typeId;
    }

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class<?> subType = null;
        switch (id) {
        case "bean1":
            subType = FirstBean.class;
            break;
        case "bean2":
            subType = LastBean.class;
        }
        return context.constructSpecializedType(superType, subType);
    }
}

The two most notable methods are idFromValueAndType and typeFromId, with the former telling the way to include type information when serializing POJOs and the latter determining the subtypes of re-created objects using that metadata.

In order to make sure that both serialization and deserialization work well, let’s write a test to validate the complete progress.

First, we need to instantiate a bean container and bean classes, then populate that container with bean instances:

FirstBean bean1 = new FirstBean(1, "Bean 1");
LastBean bean2 = new LastBean(2, "Bean 2");

List<AbstractBean> beans = new ArrayList<>();
beans.add(bean1);
beans.add(bean2);

BeanContainer serializedContainer = new BeanContainer();
serializedContainer.setBeans(beans);

Next, the BeanContainer object is serialized and we confirm that the resulting string contains type information:

String jsonString = mapper.writeValueAsString(serializedContainer);
assertThat(jsonString, containsString("bean1"));
assertThat(jsonString, containsString("bean2"));

The output of serialization is shown below:

{
    "beans": 
    [
        {
            "@type": "bean1",
            "id": 1,
            "firstName": "Bean 1"
        },

        {
            "@type": "bean2",
            "id": 2,
            "lastName": "Bean 2"
        }
    ]
}

That JSON structure will be used to re-create objects of the same subtypes as before serialization. Here are the implementation steps for deserialization:

BeanContainer deserializedContainer = mapper.readValue(jsonString, BeanContainer.class);
List<AbstractBean> beanList = deserializedContainer.getBeans();
assertThat(beanList.get(0), instanceOf(FirstBean.class));
assertThat(beanList.get(1), instanceOf(LastBean.class));

9. Conclusion

This tutorial has explained several less-common Jackson annotations in detail. The implementation of these examples and code snippets can be found in a GitHub project.

Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – Jackson (eBook) (cat=Jackson)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.