«1. Введение

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

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

В этом кратком руководстве мы рассмотрим пользовательские сериализаторы Jackson и покажем, как получить доступ к сериализаторам по умолчанию внутри пользовательского сериализатора.

2. Пример модели данных

Прежде чем мы углубимся в настройку Джексона, давайте взглянем на наш пример класса Folder, который мы хотим сериализовать:

public class Folder {
    private Long id;
    private String name;
    private String owner;
    private Date created;
    private Date modified;
    private Date lastAccess;
    private List<File> files = new ArrayList<>();

    // standard getters and setters
}

И класс File, который определен как Список внутри нашего класса Folder:

public class File {
    private Long id;
    private String name;

    // standard getters and setters
}

3. Пользовательские сериализаторы в Джексоне

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

Итак, давайте представим, что нам нужно уменьшенное представление нашего класса Folder:

{
    "name": "Root Folder",
    "files": [
        {"id": 1, "name": "File 1"},
        {"id": 2, "name": "File 2"}
    ]
}

Как мы увидим в следующих разделах, есть несколько способов добиться желаемого результата в Джексоне.

3.1. Подход грубой силы

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

Давайте создадим собственный сериализатор для нашего класса Folder, чтобы добиться этого:

public class FolderJsonSerializer extends StdSerializer<Folder> {

    public FolderJsonSerializer() {
        super(Folder.class);
    }

    @Override
    public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider)
      throws IOException {
        gen.writeStartObject();
        gen.writeStringField("name", value.getName());

        gen.writeArrayFieldStart("files");
        for (File file : value.getFiles()) {
            gen.writeStartObject();
            gen.writeNumberField("id", file.getId());
            gen.writeStringField("name", file.getName());
            gen.writeEndObject();
        }
        gen.writeEndArray();

        gen.writeEndObject();
    }
}

Таким образом, мы можем сериализовать наш класс Folder в сокращенное представление, содержащее только те поля, которые нам нужны.

3.2. Использование Internal ObjectMapper

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

Одним из способов использования сериализаторов по умолчанию является доступ к внутреннему классу ObjectMapper:

@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    gen.writeStringField("name", value.getName());

    ObjectMapper mapper = (ObjectMapper) gen.getCodec();
    gen.writeFieldName("files");
    String stringValue = mapper.writeValueAsString(value.getFiles());
    gen.writeRawValue(stringValue);

    gen.writeEndObject();
}

Итак, Джексон просто выполняет тяжелую работу, сериализуя объекты List of File, и тогда наш вывод будет таким же.

3.3. Использование SerializerProvider

Другой способ вызвать сериализаторы по умолчанию — использовать SerializerProvider. Поэтому мы делегируем процесс сериализатору по умолчанию типа File.

Теперь давайте немного упростим наш код с помощью SerializerProvider:

@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    gen.writeStringField("name", value.getName());

    provider.defaultSerializeField("files", value.getFiles(), gen);

    gen.writeEndObject();
}

И, как и прежде, мы получим тот же результат.

4. Возможная проблема с рекурсией

В зависимости от варианта использования нам может потребоваться расширить наши сериализованные данные, включив дополнительные сведения о папке. Это может быть связано с интеграцией устаревшей системы или внешнего приложения, которое у нас нет возможности изменить.

Давайте изменим наш сериализатор, чтобы создать поле сведений для наших сериализованных данных, чтобы просто отображать все поля класса Folder:

@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    gen.writeStringField("name", value.getName());

    provider.defaultSerializeField("files", value.getFiles(), gen);

    // this line causes exception
    provider.defaultSerializeField("details", value, gen);

    gen.writeEndObject();
}

На этот раз мы получаем исключение StackOverflowError.

Когда мы определяем собственный сериализатор, Джексон внутренне переопределяет исходный экземпляр BeanSerializer, созданный для типа Folder. Следовательно, наш SerializerProvider каждый раз находит настроенный сериализатор вместо стандартного, и это вызывает бесконечный цикл.

Итак, как решить эту проблему? Мы увидим одно полезное решение для этого сценария в следующем разделе.

5. Использование BeanSerializerModifier

Возможным обходным решением является использование BeanSerializerModifier для сохранения сериализатора по умолчанию для типа Folder до того, как Джексон внутренне переопределит его.

Давайте изменим наш сериализатор и добавим дополнительное поле — defaultSerializer:

private final JsonSerializer<Object> defaultSerializer;

public FolderJsonSerializer(JsonSerializer<Object> defaultSerializer) {
    super(Folder.class);
    this.defaultSerializer = defaultSerializer;
}

Далее мы создадим реализацию BeanSerializerModifier для передачи сериализатора по умолчанию:

public class FolderBeanSerializerModifier extends BeanSerializerModifier {

    @Override
    public JsonSerializer<?> modifySerializer(
      SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {

        if (beanDesc.getBeanClass().equals(Folder.class)) {
            return new FolderJsonSerializer((JsonSerializer<Object>) serializer);
        }

        return serializer;
    }
}

Теперь нам нужно зарегистрируйте наш BeanSerializerModifier как модуль, чтобы заставить его работать:

ObjectMapper mapper = new ObjectMapper();

SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FolderBeanSerializerModifier());

mapper.registerModule(module);

Затем мы используем defaultSerializer для поля сведений:

@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    gen.writeStringField("name", value.getName());

    provider.defaultSerializeField("files", value.getFiles(), gen);

    gen.writeFieldName("details");
    defaultSerializer.serialize(value, gen, provider);

    gen.writeEndObject();
}

Наконец, мы можем захотеть удалить поле файлов из сведений, поскольку мы уже запишите его в сериализованные данные отдельно.

«Итак, мы просто игнорируем поле файлов в нашем классе Folder:

@JsonIgnore
private List<File> files = new ArrayList<>();

Наконец, проблема решена, и мы также получаем ожидаемый результат:

{
    "name": "Root Folder",
    "files": [
        {"id": 1, "name": "File 1"},
        {"id": 2, "name": "File 2"}
    ],
    "details": {
        "id":1,
        "name": "Root Folder",
        "owner": "root",
        "created": 1565203657164,
        "modified": 1565203657164,
        "lastAccess": 1565203657164
    }
}

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

В этом уроке мы узнали, как вызывать сериализаторы по умолчанию внутри пользовательского сериализатора в библиотеке Джексона.

Как всегда, все примеры кода, используемые в этом руководстве, доступны на GitHub.