Practical Java 16 - Using Jackson to serialize Records
Tags: java, jackson, serialization, records
Published: 2021-04-24 14:21:01

**Introduction**

With JDK 16 being in General Availability and JDK 17 already in early access, I think it's finally time to explore what is arguably one of the coolest features offered by the "bleeding edge" Java: records, and how to work with them from a practical perspective.

One of the most common workflows for modern applications, relies on serializing what is commonly referred to as a "Data Transfer Object", shortened to DTO, that is an object used to represent a domain concept that we want to serialize to a common format that can be consumed by clients, usually JSON. 

A common library to do this in the Java stack, is called Jackson that offers core functionalities and databinding capabilities to serialize Java classes to JSON and to deserialize from JSON into Java classes.

We will see how Jackson works and how records are so much more compact and easier to use.

**Current status and usage before Java 16 and Jackson 2.12.x**

Currently, using Java < 16 and Jackson, since records are not supported, the only way we have to serialize a Java object into JSON, is to use the Jackson object mapper, as follows:

Let's create a simple `PersonDTO` class which has the following information:

```java
public class PersonDTO {
    private final String name;
    private final int age;
    private final double weight;
    @JsonSerialize(using = LocalDateSerializer.class)
    private final LocalDate collegeApplicationDate;

    public PersonDTO(String name, int age, double weight,
        @JsonSerialize(using = LocalDateSerializer.class) LocalDate collegeApplicationDate) {
        this.name = name;
        this.age = age;
        this.weight = weight;
        this.collegeApplicationDate = collegeApplicationDate;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public double getWeight() {
        return weight;
    }

    @JsonSerialize(using = LocalDateSerializer.class)
    public LocalDate getCollegeApplicationDate() {
        return collegeApplicationDate;
    }
```

As we can see, this is quite verbose, and arguably, most of this boilerplate could be avoided, and the information conveyed by the class would be exactly the same: it's a person with some personal information and an application to a college date, nothing else.

The constructor, the getters and the attributes, add a lot of clutter, but, it's the only way to have a DTO to be serialized by Jackson, as the getters are required so that the library can write them to JSON internally, if we are below Java 16.

Let's use the below service as a kind of placeholder to exercise Jackson:

```java
public class PersonListingService {

    public PersonListingService() {
    }

    public List<PersonDTO> getPersonList() {
        return Arrays.asList(
            new PersonDTO("John",30,60.7, LocalDate.now()),
            new PersonDTO("Shaw", 33, 70.5, LocalDate.now().minusDays(20L)),
            new PersonDTO("Harold Finch", 50, 70, null));
    }
}
```

We can write a quick test and verify that this works as expected, using Jackson 2.11.4, that comes bundled by default with Spring Initializr in IntelliJ:

```java
 @Test
    void dto_list_can_be_serialized_via_jackson() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));

        String serializedList = mapper.writeValueAsString(personListingService.getPersonList());

        Assert.assertEquals("[{\"name\":\"John\",\"age\":30,\"weight\":60.7,\"collegeApplicationDate\":\"2021-04-24\"}"
                + ",{\"name\":\"Shaw\",\"age\":33,\"weight\":70.5,\"collegeApplicationDate\":\"2021-04-04\"},{\"name\":\"Harold "
                + "Finch\",\"age\":50,\"weight\":70.0,\"collegeApplicationDate\":null}]", serializedList);
}
```
This works in Java < 16 and with Jackson 2.11.4, because, the getters in our DTO are used internally by Jackson to map the fields that need to be translated to the JSON: it knows to write an `age` field because our DTO has a `getAge` method, and this correspondence is done based on the getters and attributes names, by default.

Let's see now how to make records work with the Jackson version that comes bundled with the Spring Initializr.

**Serializing a record using Jackson 2.11.4**

We can now "convert" our `PersonDTO` into a Record, very easily:

```java
public record PersonDTO(String name, int age, double weight,
                        LocalDate collegeApplicationDate) {
}
```

A Java Record can be thought of as a data class that has specific fields, and can be created as a normal class, using the standard constructor based on the attributes defined above. This means that our original service works as it is declared above, no changes needed.

However, if we try to serialize this as it is above, our test will fail:

```
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.example.demo.dto.PersonDTO and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.Arrays$ArrayList[0])
```

Let's try to add a `@JsonSerialize` annotation above the record:

```java
@JsonSerialize
public record PersonDTO(String name, int age, double weight,
                        LocalDate collegeApplicationDate) {
}
```

and we get:

```java
org.junit.ComparisonFailure: expected:<[{["name":"John","age":30,"weight":60.7,"collegeApplicationDate":"2021-04-24"},{"name":"Shaw","age":33,"weight":70.5,"collegeApplicationDate":"2021-04-04"},{"name":"Harold Finch","age":50,"weight":70.0,"collegeApplicationDate":null]}]> but was:<[{[},{},{]}]>
Expected :[{"name":"John","age":30,"weight":60.7,"collegeApplicationDate":"2021-04-24"},{"name":"Shaw","age":33,"weight":70.5,"collegeApplicationDate":"2021-04-04"},{"name":"Harold Finch","age":50,"weight":70.0,"collegeApplicationDate":null}]
Actual   :[{},{},{}]
```

So now, we did manage to "serialize" something, but, we get an empty array, meaning that each individual record couldn't be serialized correctly.

Without any real "insight" on how records truly work under the hood, it can be hard to debug this, but, it's worth having a deeper look.

**"Debugging" records serialization with "old" Jackson**

IntelliJ offers us the possibility to convert a record to a class, so, let's do it and see exactly how the JVM "sees" a record as a true data class under the hood:

```java
@JsonSerialize
public final class PersonDTO {
    private final String name;
    private final int age;
    private final double weight;
    @JsonSerialize(using = LocalDateSerializer.class)
    private final LocalDate collegeApplicationDate;

    public PersonDTO(String name, int age, double weight,
        @JsonSerialize(using = LocalDateSerializer.class) LocalDate collegeApplicationDate) {
        this.name = name;
        this.age = age;
        this.weight = weight;
        this.collegeApplicationDate = collegeApplicationDate;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    public double weight() {
        return weight;
    }

    @JsonSerialize(using = LocalDateSerializer.class)
    public LocalDate collegeApplicationDate() {
        return collegeApplicationDate;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (obj == null || obj.getClass() != this.getClass())
            return false;
        var that = (PersonDTO)obj;
        return Objects.equals(this.name, that.name) &&
            this.age == that.age &&
            Double.doubleToLongBits(this.weight) == Double.doubleToLongBits(that.weight) &&
            Objects.equals(this.collegeApplicationDate, that.collegeApplicationDate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, weight, collegeApplicationDate);
    }

    @Override
    public String toString() {
        return "PersonDTO[" +
            "name=" + name + ", " +
            "age=" + age + ", " +
            "weight=" + weight + ", " +
            "collegeApplicationDate=" + collegeApplicationDate + ']';
    }
}
```

So a record is nothing more than a "final class" with final fields, and an auto-generated hashcode and equals methods. Interesting.

It's especially interesting from a debugging perspective, because, as we noted earlier, Jackson 2.11.4 requires the getters to be defined explicitely in order to be able to serialize the class to JSON, and, from this representation, it seems like the field names are being used directly to generate the getters, which opens up the road to the next logical step.....

.....

```java
@JsonSerialize
public record PersonDTO(String getName, int getAge, double getWeight,
                        @JsonSerialize(using = LocalDateSerializer.class) LocalDate collegeApplicationDate) {
}
```

Don't do this at home!! :D

We can actually trick Jackson's reflection mechanism as well as the JVM record conversion into generating "getters" in the underlying data class, which CAN be picked up by Jackson...

```java
Expected :[{"name":"John","age":30,"weight":60.7,"collegeApplicationDate":"2021-04-24"},{"name":"Shaw","age":33,"weight":70.5,"collegeApplicationDate":"2021-04-04"},{"name":"Harold Finch","age":50,"weight":70.0,"collegeApplicationDate":null}]
Actual   :[{"collegeApplicationDate":"2021-04-24","name":"John","age":30,"weight":60.7},{"collegeApplicationDate":"2021-04-04","name":"Shaw","age":33,"weight":70.5},{"collegeApplicationDate":null,"name":"Harold Finch","age":50,"weight":70.0}]
```

While the fields of the objects are out of order, we see that we do have the data there, and actually it works because, the values in the fields are exactly the same. We left the custom serializer to handle the date, but, using the "artificial getter" actually was a funny way to leverage record's internal construction structure.

The correct way is to use Jackson's annotation `AutoDetect`:

```java
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonSerialize
public record PersonDTO(String name, int age, double weight,
                        @JsonSerialize(using = LocalDateSerializer.class) LocalDate collegeApplicationDate) {
}
```

this will let Jackson autodetect the fields based on the record's attributes and the test will pass, since the restriction of the getter field will be lifted.

Since records generate the getters directly from the attributes of the record, this is a good way to bypass it, that is perfectly valid and allows us to use records today with Jackson 2.11.4.

**How to do it the right way**

By upgrading Jackson to it's latest release 2.12.3:

```xml
  <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.12.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.12.3</version>
        </dependency>
```

We can make use of the great interoperability between Jackson and records, thanks to the added support for record serialization (more info [here](https://github.com/FasterXML/jackson-future-ideas/issues/46) ) which means that simply declaring a record in its simplest form:

```java
public record PersonDTO(String name, int age, double weight,
                        @JsonSerialize(using = LocalDateSerializer.class) LocalDate collegeApplicationDate) {
}
```

will work, and the record will be able to be serialized correctly, leveraging the local date serializer as well.

**Conclusion**

As Java keeps improving and maturing, some of its features are extremely useful and versatile, and it's always nice to explore them in some detail to understand its nuances and how it works under the hood. Our example of leveraging the knowledge of Jackson together with the way JVM generates a record is a good example of that.

With the right configuration, records can work almost hassle free with Jackson 2.11.4 and it's as seamlessly integrated as normal workflows of today in Jackson's latest releases. Are you using records already? :)