Logo

dev-resources.site

for different kinds of informations.

Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values

Published at
5/11/2023
Categories
java
jackson
api
springboot
Author
alaugks
Categories
4 categories in total
java
open
jackson
open
api
open
springboot
open
Author
7 person written this
alaugks
open
Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values

For my series of articles, I also wanted to see how this requirement to mapping values could be implemented with Jackson.

The first paragraph "The requirements and history" from the first article describes the requirements for Emarsys to rewrite the values for the payload.

The required packages

  • com.fasterxml.jackson.core:jackson-databin
  • com.fasterxml.jackson.datatype:jackson-datatype-jsr310

See the pom.xml in the example for the latest versions.

Minimal structure of a custom JsonSerializer and JsonDeserializer

To solve the requirements to map the values for Emarsys, a custom JsonSerializer and JsonDeserializer is needed. I call these MappingValueSerializer and MappingValueDeserializer.

Below is the minimal structure of a custom MappingValueSerializer and MappingValueDeserializer:

@JsonSerialize(using = MappingValueSerializer.class)
@JsonDeserialize(using = MappingValueDeserializer.class)
private String fieldName;

public class MappingValueSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString("serialized: " + value);
    }
}

public class MappingValueDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException  {
        String value = jsonParser.getText();
        return "deserialized: " + value;
    }
}
Enter fullscreen mode Exit fullscreen mode

In ContactDto, the fields salutation and marketingInformation for which values have to be rewritten are defined.

Fields/Direction serialize (T -> String) deserialize (String -> T)
salutation "FEMALE" -> "2" "2" -> "FEMALE"
marketingInformation true -> "1" "1" -> true

For the serialize process it is the FieldValueID (String) and for the deserialize process the type String for salutation and the type Boolean for marketingInformation.

So if you want to do the mapping, you would need a JsonSerializer to write the FieldValueID (String) for salutation and marketingInformation and a JsonDeserializer to set the value for stalutation (String) and marketingInformation (Boolean).

Custom Type

However, I only want to have a JsonDeserializer that can process String, Boolean and in the future other types. For this purpose, I create my own type MappingValue<>. Most importantly, I can transport all types with this custom generics.

package com.microservice.crm.serializer;

public class MappingValue<T> {
    T value;

    public MappingValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return this.value;
    }
}
Enter fullscreen mode Exit fullscreen mode

ContactDto

First of all the complete ContactDto with all fields and annotations. I will explain the individual annotations below.

package com.article.jackson.dto;

import java.time.LocalDate;

import com.article.jackson.annotation.MappingTable;
import com.article.jackson.serializer.MappingValue;
import com.article.jackson.serializer.MappingValueDeserializer;
import com.article.jackson.serializer.MappingValueSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonAutoDetect(
    fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE,
    setterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE
)
public class ContactDto {

    @JsonProperty("1")
    private String firstname;

    @JsonProperty("2")
    private String lastname;

    @JsonProperty("3")
    private String email;

    @JsonProperty("4")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonSerialize(using = LocalDateSerializer.class)
    @JsonDeserialize(using = LocalDateDeserializer.class)
    private LocalDate birthday;

    @JsonProperty("46")
    @MappingTable(map = Maps.SALUTATION)
    @JsonSerialize(using = MappingValueSerializer.class)
    @JsonDeserialize(using = MappingValueDeserializer.class)
    private MappingValue<String> salutation;

    @JsonProperty("100674")
    @MappingTable(map = Maps.MARKETING_INFORMATION)
    @JsonSerialize(using = MappingValueSerializer.class)
    @JsonDeserialize(using = MappingValueDeserializer.class)
    private MappingValue<Boolean> marketingInformation;

    // other getter and setter

    public String getSalutation() {
        return salutation.getValue();
    }

    public void setSalutation(String salutation) {
        this.salutation = new MappingValue<>(salutation);
    }

    public Boolean getMarketingInformation() {
        return this.isMarketingInformation();
    }

    public Boolean isMarketingInformation() {
        return marketingInformation.getValue();
    }

    public void setMarketingInformation(Boolean marketingInformation) {
        this.marketingInformation = new MappingValue<>(marketingInformation);
    }
}
Enter fullscreen mode Exit fullscreen mode

Custom Annotation @MappingTable and Enum Constants for the MappingTable

The MappingTable with the FieldValueIDs for salutation and marketingInformation must be available in the MappingValueSerializer and MappingValueDeserializer.

Annotation @MappingTable

For this I create a custom annotation @MappingTable that will be read in the MappingValueSerializer and MappingValueDeserializer.

package com.article.jackson.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.article.jackson.dto.Maps;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {
    Maps map();
}

Enter fullscreen mode Exit fullscreen mode

Enum Constants

package com.article.jackson.dto;

import java.util.Map;

public enum Maps {

    SALUTATION(Map.of("1", "MALE", "2", "FEMALE", "6", "DIVERS")),

    MARKETING_INFORMATION(Map.of("1", true, "2", false));

    private final Map<String, Object> map;

    Maps(Map<String, Object> map) {
        this.map = map;
    }

    public Map<String, Object> getMap() {
        return this.map;
    }
}
Enter fullscreen mode Exit fullscreen mode

Field definitions

The enum constants Maps.SALUTATION and Maps.MARKETING_INFORMATION are referenced in the @MappingTable annotation. The HashMaps are used in the JsonSerializer and JsonDeserializer for bi-directional mapping.

@MappingTable(map = Maps.SALUTATION)
private MappingValue<String> salutation;

@MappingTable(map = Maps.MARKETING_INFORMATION)
private MappingValue<Boolean> marketingInformation;
Enter fullscreen mode Exit fullscreen mode

Reading and writing on the fields

The ObjectManager of Jackson writes and reads on the mutators (setter) and accessor (getter, isser) by default.

For the mutator and accessor of salutation and marketingInformation, however, I would like to define the type String or Boolean.

You can use an annotation to instruct Jackson to read and write only on the fields, so we can use the custom type MappingValue<> internally. The reading and writing process thus takes place on the fields and we can define String and Boolean for the mutator and accessor of salutation and marketingInformation.

@JsonAutoDetect(
    fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE,
    setterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE
)
Enter fullscreen mode Exit fullscreen mode

FieldIDs

The FieldIDs can be defined very easy with @JsonProperty.

@JsonProperty("123")
Enter fullscreen mode Exit fullscreen mode

Define custom JsonSerializer and JsonDeserializer

The custom JsonSerializer (MappingValueSerializer) and JsonDeserializer (MappingValueDeserializer) can be defined with @JsonSerialize and @JsonDeserialize on the field.

@JsonSerialize(using = MappingValueSerializer.class)
@JsonDeserialize(using = MappingValueDeserializer.class)
Enter fullscreen mode Exit fullscreen mode

Skip null values

Fields with null as value should not be serialized. This is because the fields that are sent are also updated. The annotation @JsonInclude can be used for this.

@JsonInclude(JsonInclude.Include.NON_NULL)
Enter fullscreen mode Exit fullscreen mode

Ignore unknown properties

Emarsys always returns all fields for a contact in the response. I want only the fields defined in the ContactDto to be mapped so that no exceptions are thrown. The annotation @JsonIgnoreProperties can be used for this:

@JsonIgnoreProperties(ignoreUnknown = true)
Enter fullscreen mode Exit fullscreen mode

MappingValueSerializer and MappingValueDeserializer

In order for the MappingTable, which is defined at the field, to be read, the interface ContextualSerializer must be implemented for the MappingValueSerializer and the interface ContextualDeserializer for the MappingValueDeserializer.

With createContextual(), access to the property is possible and via BeanProperty the annotation can be fetched and the MappingTable can be read out.

MappingValueSerializer

In the MappingValueSerializer, for example, for salutation "FEMALE" is mapped to "2" and marketingInformation true to "1", which is why the FieldValueID is written with jsonGenerator.writeString().

package com.article.jackson.serializer;

import java.io.IOException;
import java.util.Map;

import com.article.jackson.annotation.MappingTableMapReader;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

public class MappingValueSerializer extends JsonSerializer<MappingValue<?>> implements ContextualSerializer {

    private final Map<String, Object> map;

    public MappingValueSerializer() {
        this(null);
    }

    public MappingValueSerializer(Map<String, Object> map) {
        this.map = map;
    }

    @Override
    public void serialize(MappingValue<?> field, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        String fieldValueId = this.map.entrySet()
                .stream()
                .filter(e -> e.getValue().equals(field.getValue()))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);

        jsonGenerator.writeString(fieldValueId);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        return new MappingValueSerializer(
                new MappingTableMapReader(property).getMap()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

MappingValueDeserializer

In the MappingValueDeserializer the mapping takes place backwards. Here the FieldValueID for salutation and marketingInformation must be mapped accordingly. For salutation "2" to "FEMALE" (String) and for marketingInformation "1" to true (Boolean).

package com.article.jackson.serializer;

import java.io.IOException;
import java.util.Map;

import com.article.jackson.annotation.MappingTableMapReader;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;

public class MappingValueDeserializer extends JsonDeserializer<MappingValue<?>> implements ContextualDeserializer {

    private final Map<String, Object> map;

    public MappingValueDeserializer() {
        this(null);
    }

    public MappingValueDeserializer(Map<String, Object> map) {
        this.map = map;
    }

    @Override
    public MappingValue<?> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        String fieldValue = jsonParser.getText();

        return new MappingValue<>(this.map.entrySet()
                .stream()
                .filter(e -> e.getKey().equals(fieldValue))
                .map(Map.Entry::getValue)
                .findFirst()
                .orElse(null));
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        return new MappingValueDeserializer(
                new MappingTableMapReader(property).getMap()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

MappingTableMapReader

In the MappingTableMapReader class, the enum constant is retrieved from the annotation and is available in the JsonSerializer and JsonDeserializer.

ackage com.article.jackson.annotation;

import java.util.Map;

import com.article.jackson.exception.MappingTableRuntimeException;
import com.fasterxml.jackson.databind.BeanProperty;

public class MappingTableMapReader {

    private final BeanProperty property;

    public MappingTableMapReader(BeanProperty property) {
        this.property = property;
    }

    public Map<String, Object> getMap() {

        MappingTable annotation = property.getAnnotation(MappingTable.class);

        if (annotation == null) {
            throw new MappingTableRuntimeException(
                String.format(
                    "Annotation @MappingTable not set at property %s",
                    this.property.getMember().getFullName()
                )
            );
        }

        return annotation.map().getMap();
    }
}
Enter fullscreen mode Exit fullscreen mode

Functional Test

To check the implementation, we still need a test. To compare the JSON, I use assertThatJson() from the package json-unit-assertj.

package com.article.jackson.serializer;

import java.io.IOException;
import java.time.LocalDate;

import com.article.jackson.dto.ContactDto;
import com.article.jackson.exception.MappingTableRuntimeException;
import com.article.jackson.fixtures.ContactDtoAnnotationNotSet;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.api.Test;

import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MappingTableSerializerDeserializerTest {

    String emarsysPayload = """
            {
                "1": "Jane",
                "2": "Doe",
                "3": "[email protected]",
                "4": "1989-11-09",
                "46": "2",
                "100674": "1"
            }
            """;

    @Test
    void serialize() throws IOException {
        ContactDto contact = new ContactDto();
        contact.setSalutation("FEMALE");
        contact.setFirstname("Jane");
        contact.setLastname("Doe");
        contact.setEmail("[email protected]");
        contact.setBirthday(LocalDate.of(1989, 11, 9));
        contact.setMarketingInformation(true);
        String json = new ObjectMapper().writeValueAsString(contact);
        assertThatJson(this.emarsysPayload)
                .when(Option.IGNORING_ARRAY_ORDER)
                .isEqualTo(json);
    }

    @Test
    void deserialize() throws IOException {
        ContactDto contact = new ObjectMapper().readValue(this.emarsysPayload, ContactDto.class);
        assertEquals("FEMALE", contact.getSalutation());
        assertEquals("Jane", contact.getFirstname());
        assertEquals("Doe", contact.getLastname());
        assertEquals("[email protected]", contact.getEmail());
        assertEquals(LocalDate.of(1989, 11, 9), contact.getBirthday());
        assertTrue(contact.getMarketingInformation());
        assertTrue(contact.isMarketingInformation());
    }
}
Enter fullscreen mode Exit fullscreen mode

Full example on GitHub

https://github.com/alaugks/article-jackson-serializer

Updates

  • Change GitHub Repository URL (Sep 6th 2024)
  • Replace JSON with HashMaps (Dec 19th 2024)
jackson Article's
30 articles in total
Favicon
Why Do We Still Need Jackson or Gson in Java?
Favicon
A simple GeoJSON serializer for Jackson
Favicon
[Java Spring Boot] Como Criar Serializador Personalizado para seus Responses ou Json de saรญda
Favicon
[Java Spring Boot] How to implement a Custom Serializer for your Responses or Json
Favicon
[Java SpringBoot] Como Criar Deserializador Personalizado para seus Requests
Favicon
[Java SpringBoot] How to implement a Custom Deserializer for your Requests
Favicon
Java Jackson JSON: How to Handle Custom Keys?
Favicon
Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values
Favicon
Anotaciรณn @JsonUnwrapped
Favicon
A tale of fixing a tiny OpenAPI bug
Favicon
Kotlin Springboot -- Part 21 ไปปๆ„ใฎ key value ใฎ json ใ‚’ POST ใ™ใ‚‹ API E2E ใ‚’ๆ›ธใ
Favicon
Formatting json Date/LocalDateTime/LocalDate in Spring Boot
Favicon
Jackson's @JsonView with SpringBoot Tutorial
Favicon
Jackson JSON parsing top-level map into records
Favicon
Using Jackson Subtypes to Write Better Code
Favicon
Java โ€“ Convert Excel File to/from JSON (String/File) โ€“ using Apache Poi + Jackson
Favicon
How to resolve Json Infinite Recursion problem when working with Jackson
Favicon
Java โ€“ Convert Excel File to/from JSON (String/File) โ€“ using Apache Poi + Jackson
Favicon
Practical Java 16 - Using Jackson to serialize Records
Favicon
Kotlin โ€“ Convert Object to/from JSON with Jackson 2.x
Favicon
๐Ÿ’พ Java Records ๐Ÿ’ฟ with Jackson 2.12
Favicon
Jackson, JSON and the Proper Handling of Unknown Fields in APIs
Favicon
Polymorphic deserialization with Jackson and no annotations
Favicon
Playing around with Kotlin Sealed Classes
Favicon
Moonwlker: JSON without annotation
Favicon
Jackson Readonly properties and swagger UI
Favicon
Registering Jackson sub-types at runtime in Kotlin
Favicon
Parsing JSON in Spring Boot, part 1
Favicon
Customize how Jackson does LocalDate Parsing
Favicon
Painless JSON with Kotlin and jackson

Featured ones: