Jackson without annotations

Jackson is an efficient and simple to use Java library for data-binding of JSON documents and POJOs. It supports other documents types e.g. YAML, XML and other modes e.g. tree-model but this post is about JSON to/from POJO. When I first used Jackson following various examples, I was slightly annoyed by having to annotate stuff or having to manually create a no-args constructor just for Jackson’s sake. These issues also came up when using Lombok with my POJOs. I describe here a usage pattern I ended up with to avoid Jackson annotations and no-args constructors. Hopefully someone else will find this useful!

The no-args constructor issue

Final fields are desirable whenever possible (Effective Java item 15). Here is an incomplete class definition with private final fields.

class Person {
    private final String name;
    private final String surname;
    private final int age;
}

This doesn’t compile as the default constructor doesn’t initialize the fields.
Obviously we can’t have setters for final fields. A final field can be initialized at declaration e.g.

private final String name = null;

or at an initialization block. But then we couldn’t have a constructor with arguments as that would be modifying the final field. We would also be setting null and zero values that are never actually used and that’s confusing. We can manually create a no-args constructor and initialize all final fields to 0 or null values. This compiles just fine, doesn’t prevent another constructor with arguments and Jackson will happily use this constructor without constructor annotations even if it is private. But such a constructor adds clutter if not otherwise needed and is not auto-generated by Intellij or Lombok annotations. If we want Jackson to use a constructor with arguments, we normally have to annotate both the constructor and each argument as in the example above. Otherwise, Jackson wouldn’t know the order of the constructor arguments and to which field they correspond.

Annotation clutter

Apart from the annotations on constructor with arguments Jackson requires other annotations to discover non-public fields and methods. The most common scenario for POJOs is to make all fields private. Public setters/getters may not necessarily exist for each/any field. Also, not all classes and their constructors should be public. We may want a class and its constructor to be visible only within a package. Jackson typically requires a class level annotation to set the field and/or method visibility.

Annotations add clutter to the source code and require imports. This is what a simple class looks like with all the annotations required by Jackson.

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonAutoDetect(creatorVisibility = ANY, fieldVisibility = ANY)
class Person {

    private final String name;
    private final String surname;
    private final int age;

    @Override
    public String toString() {
        return "Person{" +
               "name='" + name + '\'' +
               ", surname='" + surname + '\'' +
               ", age=" + age +
               '}';
    }

    @JsonCreator
    Person(@JsonProperty("name") String name,
           @JsonProperty("surname") String surname,
           @JsonProperty("age") int age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }
}

Another issue with annotations is that the source code where a class is defined may not be under our control. Jackson offers mixins or custom (de)serializers for that purpose but it would be better if we can avoid them altogether.

Usage Pattern
It has recently become possible to have Jackson use a constructor with arguments without annotations. The compiler can generate and save metadata with the argument names. A jackson module can then use this metadata to know how to use the constructor to initialize all fields. Although I haven’t checked the bytecode that sounds more efficient than reflection. The requirements for that are :

  1. JDK 1.8
  2. compile with -parameters argument
  3. use and register jackson-module-parameter-names

Regarding the visibility annotations, they can be set outside the POJO by calling setVisibility method on the ObjectMapper. That a single line of code in the same file as the other Jackson related code and imports. This is what the POJO code looks like (toString and constructor as autogenerated by Intellij)

class Person {
    private final String name;
    private final String surname;
    private final int age;

    @Override
    public String toString() {
        return "Person{" +
               "name='" + name + '\'' +
               ", surname='" + surname + '\'' +
               ", age=" + age +
               '}';
    }

    Person(String name, String surname, int age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }
}

and here is the main function that (de)serialises a Person object

import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import java.io.File;
import java.io.IOException;

class Main {

    public static void main(String[] args) throws IOException {
        Person person = new Person("John", "Smith", 40);

        ObjectMapper mapper = new ObjectMapper();

        // Avoid having to annotate the Person class
        // Requires Java 8, pass -parameters to javac
        // and jackson-module-parameter-names as a dependency
        mapper.registerModule(new ParameterNamesModule());

        // make private fields of Person visible to Jackson
        mapper.setVisibility(FIELD, ANY);

        File file = new File("person.json");
        mapper.writeValue(file, person);

        Person anotherPerson = mapper.readValue(file, Person.class);
        System.out.println(anotherPerson);
    }
}

You can see the source of the standard and the lombok-builder version. Both versions come with the necessary maven dependencies, javac arguments and Intellij project files.

Cheers
Manos

Links
jackson for jdk8
jackson module for using parameter names
java api for parameter name reflection