Перейти к основному содержимому

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

· 5 мин. чтения

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. Использование внутреннего 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();
}

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

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 . Это может быть связано с интеграцией устаревшей системы или внешнего приложения, которое у нас нет возможности модифицировать .

Давайте изменим наш сериализатор, чтобы создать поле сведений для наших сериализованных данных, чтобы просто отображать все поля класса 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 .