Извикване на сериализатор по подразбиране от персонализиран сериализатор в Джаксън

1. Въведение

Сериализирането на пълната ни структура от данни към JSON, използвайки точно едно-на-едно представяне на всички полета, може да не е подходящо понякога или просто да не е това, което искаме. Вместо това може да поискаме да създадем разширен или опростен изглед на нашите данни. Тук влизат в сила персонализирани сериализатори на Jackson.

Внедряването на персонализиран сериализатор обаче може да бъде досадно, особено ако нашите обекти на модел имат много полета, колекции или вложени обекти. За щастие библиотеката на Джаксън има няколко разпоредби, които могат да направят тази работа много по-опростена.

В този кратък урок ще разгледаме персонализираните сериализатори на Jackson и ще покажем как да осъществим достъп до сериализаторите по подразбиране в персонализиран сериализатор .

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

Преди да се потопим в персонализирането на Джаксън, нека да разгледаме нашия примерен клас папка , който искаме да сериализираме:

public class Folder { private Long id; private String name; private String owner; private Date created; private Date modified; private Date lastAccess; private List 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. Подход на грубата сила

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

Нека създадем персонализиран сериализатор за нашия клас папки, за да постигнем това:

public class FolderJsonSerializer extends StdSerializer { 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(); } 

Така че, Джаксън просто се справя с тежкото вдигане, като сериализира списъка с обекти на файла и тогава изходът ни ще бъде същият.

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 .

When we define a custom serializer, Jackson internally overrides the original BeanSerializer instance that is created for the type Folder. Consequently, our SerializerProvider finds the customized serializer every time, instead of the default one, and this causes an infinite loop.

So, how do we solve this problem? We'll see one usable solution for this scenario in the next section.

5. Using BeanSerializerModifier

A possible workaround is using BeanSerializerModifierto store the default serializer for the type Folderbefore Jackson internally overrides it.

Let's modify our serializer and add an extra field — defaultSerializer:

private final JsonSerializer defaultSerializer; public FolderJsonSerializer(JsonSerializer defaultSerializer) { super(Folder.class); this.defaultSerializer = defaultSerializer; } 

Next, we'll create an implementation of BeanSerializerModifier to pass the default serializer:

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) serializer); } return serializer; } } 

Now, we need to register our BeanSerializerModifier as a module to make it work:

ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.setSerializerModifier(new FolderBeanSerializerModifier()); mapper.registerModule(module); 

Then, we use the defaultSerializer for the details field:

@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(); } 

Lastly, we may want to remove the files field from the details since we already write it into the serialized data separately.

So, we simply ignore the files field in our Folder class:

@JsonIgnore private List files = new ArrayList(); 

Finally, the problem is solved and we get our expected output as well:

{ "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. Conclusion

In this tutorial, we learned how to call default serializers inside a custom serializer in Jackson Library.

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