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 .