«1. Обзор

В этом кратком руководстве мы покажем два разных способа десериализации неизменяемых объектов Java с помощью библиотеки обработки JSON Jackson.

2. Почему мы используем неизменяемые объекты?

Неизменяемый объект — это объект, сохраняющий свое состояние неизменным с момента создания. Это означает, что независимо от того, какие методы объекта вызывает конечный пользователь, объект ведет себя одинаково.

Неизменяемые объекты пригодятся, когда мы проектируем систему, которая должна работать в многопоточной среде, поскольку неизменность обычно гарантирует безопасность потоков.

С другой стороны, неизменяемые объекты полезны, когда нам нужно обрабатывать ввод из внешних источников. Например, это может быть пользовательский ввод или какие-то данные из хранилища. В этом случае может оказаться критически важным сохранить полученные данные и защитить их от случайных или непреднамеренных изменений.

Давайте посмотрим, как мы можем десериализовать неизменяемый объект.

3. Публичный конструктор

Рассмотрим структуру класса Employee. У него есть два обязательных поля: id и name, поэтому мы определяем общедоступный конструктор со всеми аргументами, набор аргументов которого соответствует набору полей объекта:

public class Employee {

    private final long id;
    private final String name;

    public Employee(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getters
}

Таким образом, мы получим все поля объекта инициализируются в момент создания. Окончательные модификаторы в объявлении полей не позволят нам изменить их значения в будущем. Чтобы сделать этот объект десериализуемым, нам просто нужно добавить пару аннотаций к этому конструктору:

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public Employee(@JsonProperty("id") long id, @JsonProperty("name") String name) {
    this.id = id;
    this.name = name;
}

Давайте подробнее рассмотрим только что добавленные аннотации.

Прежде всего, @JsonCreator сообщает десериализатору Jackson использовать назначенный конструктор для десериализации.

Есть два режима, которые можно использовать в качестве параметра для этой аннотации — СВОЙСТВА и ДЕЛЕГИРОВАНИЕ.

PROPERTIES лучше всего подходит, когда мы объявляем конструктор со всеми аргументами, тогда как DELEGATING может быть полезен для конструкторов с одним аргументом.

После этого нам нужно аннотировать каждый из аргументов конструктора с помощью @JsonProperty, указав имя соответствующего свойства в качестве значения аннотации. Мы должны быть очень осторожны на этом шаге, так как все имена свойств должны совпадать с теми, которые мы использовали при сериализации.

Давайте рассмотрим простой модульный тест, который охватывает десериализацию объекта Employee:

String json = "{\"name\":\"Frank\",\"id\":5000}";
Employee employee = new ObjectMapper().readValue(json, Employee.class);

assertEquals("Frank", employee.getName());
assertEquals(5000, employee.getId());

4. Private Constructor и Builder

Иногда бывает, что объект имеет набор необязательных полей. Давайте рассмотрим другую структуру класса, Person, которая имеет необязательное поле age:

public class Person {
    private final String name;
    private final Integer age;

    // getters
}

Когда у нас есть значительное количество таких полей, создание общедоступного конструктора может стать громоздким. Другими словами, нам нужно будет объявить множество аргументов для конструктора и аннотировать каждый из них аннотациями @JsonProperty. В результате множество повторяющихся объявлений сделают наш код раздутым и трудным для чтения.

Это тот случай, когда на помощь приходит классический паттерн Builder. Давайте посмотрим, как мы можем использовать его мощь в десериализации. Прежде всего, давайте объявим частный конструктор со всеми аргументами и класс Builder:

private Person(String name, Integer age) {
    this.name = name;
    this.age = age;
}

static class Builder {
    String name;
    Integer age;
    
    Builder withName(String name) {
        this.name = name;
        return this;
    }
    
    Builder withAge(Integer age) {
        this.age = age;
        return this;
    }
    
    public Person build() {
        return new Person(name, age);
    } 
}

Чтобы заставить десериализатор Джексона использовать этот Builder, нам просто нужно добавить две аннотации к нашему коду. Прежде всего, нам нужно пометить наш класс аннотацией @JsonDeserialize, передав параметр билдера с полным доменным именем класса билдера.

После этого нам нужно аннотировать сам класс билдера как @JsonPOJOBuilder:

@JsonDeserialize(builder = Person.Builder.class)
public class Person {
    //...
    
    @JsonPOJOBuilder
    static class Builder {
        //...
    }
}

Обратите внимание, что мы можем настроить имена методов, используемых во время сборки.

Параметр buildMethodName по умолчанию имеет значение «build» и обозначает имя метода, который мы вызываем, когда билдер готов сгенерировать новый объект.

Другой параметр, withPrefix, обозначает префикс, который мы добавляем к методам компоновщика, отвечающим за установку свойств. Значение по умолчанию для этого параметра — with. Вот почему мы не указали ни один из этих параметров в примере.

Давайте рассмотрим простой модульный тест, который охватывает десериализацию объекта Person:

String json = "{\"name\":\"Frank\",\"age\":50}";
Person person = new ObjectMapper().readValue(json, Person.class);

assertEquals("Frank", person.getName());
assertEquals(50, person.getAge().intValue());

5. Заключение

«В этой короткой статье мы увидели, как десериализовать неизменяемые объекты с помощью библиотеки Джексона.

Весь код, относящийся к этой статье, можно найти на GitHub.