Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values
Tags: java, jackson, api, springboot
Published: 2023-05-11 11:28:08

---
title: Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values
published: true
description: 
tags: java, jackson, api, springboot
series: Mapping FieldValueIDs for the payload of the Emarsys API
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-05-08 19:07 +0000
---

For my series of articles, I also wanted to see how this requirement to mapping values could be implemented with [Jackson](https://github.com/FasterXML/jackson).

The first paragraph "[The requirements and history](https://dev.to/elevado/create-a-custom-symfony-normalizer-for-mapping-values-4nc2#the_requirements_and_history)" from the first article describes the requirements for [Emarsys](https://emarsys.com/) to rewrite the values for the payload.

## The required packages

* Jackson
    * com.fasterxml.jackson.core:jackson-databin
    * com.fasterxml.jackson.datatype:jackson-datatype-jsr310
* JUnit
    * org.junit.jupiter:junit-jupiter-api
    * org.junit.jupiter:junit-jupiter-engine
* JsonUnit
    * net.javacrumbs.json-unit:json-unit-assertj

```xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.14.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.14.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.9.2</version>
</dependency>
<dependency>
<dependency>    
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.9.2</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.json-unit</groupId>
    <artifactId>json-unit-assertj</artifactId>
    <version>2.37.0</version>
    <scope>test</scope>
</dependency>
```
## 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`:

```java
@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;
    }
}
```

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.

```java
package com.microservice.crm.serializer;

public class MappingValue<T> {
    T value;

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

    public T getValue() {
        return this.value;
    }
}
```

## ContactDto

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

```java
package com.microservice.crm.fixtures;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonFormat;
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;
import com.microservice.crm.annotation.*;
import com.microservice.crm.serializer.MappingValue;
import com.microservice.crm.serializer.MappingValueDeserializer;
import com.microservice.crm.serializer.MappingValueSerializer;

import java.time.LocalDate;

@JsonAutoDetect(
    fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE,
    setterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
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 = "{\"1\": \"MALE\", \"2\": \"FEMALE\", \"6\": \"DIVERS\"}")
    @JsonSerialize(using = MappingValueSerializer.class)
    @JsonDeserialize(using = MappingValueDeserializer.class)
    private MappingValue<String> salutation;

    @JsonProperty("100674")
    @MappingTable(map = "{\"1\": true, \"2\": false}")
    @JsonSerialize(using = MappingValueSerializer.class)
    @JsonDeserialize(using = MappingValueDeserializer.class)
    private MappingValue<Boolean> marketingInformation;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public void setBirthday(LocalDate birthday) {
        this.birthday = birthday;
    }

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

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

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

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

```

### 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`.

```java
@JsonAutoDetect(
    fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE,
    setterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE
)
```

### FieldIDs

The FieldIDs can be defined very easy with `@JsonProperty`.

```java
@JsonProperty("123")
```

### Define custom JsonSerializer and JsonDeserializer

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

```java
@JsonSerialize(using = MappingValueSerializer.class)
@JsonDeserialize(using = MappingValueDeserializer.class)
```

### 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.

```java
@JsonInclude(JsonInclude.Include.NON_NULL)
```

### 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:

```java
@JsonIgnoreProperties(ignoreUnknown = true)
```

### Custom Annotation @MappingTable for the MappingTable

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

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

```java
package com.microservice.crm.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface MappingTable {
    String map() default "{}";
}
```

The `@MappingTable` is defined as masked JSON (String) at the annotation. 

Only the following types are possible for annotations:

* Primitive types
* String
* Enum
* Class (Class \<?\>, Class\<? extends/super T\>)
* Array of the above (array[] of primitives, enums, String, or Class)
* Another annotation.

See this [conversation](https://stackoverflow.com/questions/1458535/which-types-can-be-used-for-java-annotation-members) on Stackoverflow.

## 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.

The [MappingTableDataReader](https://github.com/elevado/jackson-article/blob/master/src/main/java/com/article/jackson/annotation/MappingTableMapReader.java) converts the JSON into a HashMap.

### 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()`. 

```java
package com.microservice.crm.serializer;

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;
import com.microservice.crm.annotation.MappingTableDataReader;

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

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

    private final HashMap<String, ?> data;

    public MappingValueSerializer() {
        this(null);
    }

    public MappingValueSerializer(HashMap<String, ?> data) {
        this.data = data;
    }

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

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        return new MappingValueSerializer(
                new MappingTableDataReader().getMap(property)
        );
    }
}
```

### 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).

```java
package com.microservice.crm.serializer;

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;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.microservice.crm.annotation.MappingTableDataReader;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

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

    private final String[] supportedTypes = {"String", "Boolean"};
    
    private final HashMap<String, ?> data;

    private final Type type;

    public MappingValueDeserializer() {
        this(null, null);
    }

    public MappingValueDeserializer(HashMap<String, ?> data, Type type) {
        this.data = data;
        this.type = type;
    }

    @Override
    public MappingValue<?> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        String value = jsonParser.getText();
        String simpleName = ((SimpleType) this.type).getBindings().getTypeParameters().get(0).getRawClass().getSimpleName();

        if (Arrays.stream(supportedTypes).noneMatch(simpleName::equalsIgnoreCase)) {
            throw new IOException(String.format("Type \"%s\" is currently not supported", simpleName));
        }
        
        return new MappingValue<>(this.data.entrySet().stream()
                .filter(e -> e.getKey().equals(value))
                .map(Map.Entry::getValue)
                .findFirst()
                .orElse(null));
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        return new MappingValueDeserializer(
                new MappingTableDataReader().getMap(property),
                property.getType()
        );
    }
}
```

## Test

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

```java
package com.microservice.crm.serializer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservice.crm.fixtures.ContactDto;
import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.api.Test;

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

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

public class MappingTableSerializerDeserializerTest {

    String emarsysPayload = """
            {
                "1": "Jane",
                "2": "Doe",
                "3": "jane.doe@example.com",
                "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("jane.doe@example.com");
        contact.setBirthday(LocalDate.of(1989, 11, 9));
        contact.setMarketingInformation(true);
        String json = new ObjectMapper().writeValueAsString(contact);
        assertThatJson(this.emarsysPayload.trim())
                .when(Option.IGNORING_ARRAY_ORDER)
                .isEqualTo(json);
    }

    @Test
    void deserialize() throws IOException {
        ContactDto contact = new ObjectMapper().readValue(this.emarsysPayload.trim(), ContactDto.class);
        assertEquals("FEMALE", contact.getSalutation());
        assertEquals("Jane", contact.getFirstname());
        assertEquals("Doe", contact.getLastname());
        assertEquals("jane.doe@example.com", contact.getEmail());
        assertEquals(LocalDate.of(1989, 11, 9), contact.getBirthday());
        assertTrue(contact.getMarketingInformation());
    }
}

```

## ToDo

The MappingTable is defined as masked JSON to the annotation `@MappingTable`. This means we cannot use the data in any other context. A HashMap cannot be defined because Annotation does not support HashMap. For example, the solution would be to use ENUM class. 

How this can be solved I will look at in a future article.

## Links
[Full example on github](https://github.com/elevado/jackson-article)