Created 2024/11/03 at 01:06PM

Last Modified 2025/01/09 at 09:08PM

So, few years ago I came across a piece of code which was not working as it is supposed to, and it was something that could be very easily overlooked.

We are going to use 2 libraries - lombok and jackson. Both are very popular and widely used. Lombok provides a way to use a lot of annotations to reduce boilerplate and repetitive code, and Jackson provides a way for JSON serialization / deserialization.

// Main.java
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class Main {
    public static void main(String[] args) {
        ObjectMapper objectMapper = new ObjectMapper();

        List<String> hobbies = new ArrayList<>(Arrays.asList("swimming", "soccer"));
        Person person = new Person(hobbies);

        try {
            String json = objectMapper.writeValueAsString(person);
            System.out.println("Serialized JSON: " + json);
            Person deserializedPerson = objectMapper.readValue(json, Person.class);
            System.out.println("Deserialized Person: " + deserializedPerson);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
// Person.java
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

import lombok.AllArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    public List<String> hobbies;

    public List<String> getPersonalHobbies() {
        return this.hobbies;
    }
}

So, first of all ,let me explain the three annotations that we are using, which are provided by lombok.

  1. Data - Generates all the boilerplate that is normally associated with simple POJOs (Plain Old Java Objects) and beans: getters for all fields, setters for all non-final fields, and appropriate toString, equals and hashCode implementations that involve the fields of the class, and a constructor that initializes all final fields, as well as all non-final fields with no initializer that have been marked with NonNull annotation, in order to ensure the field is never null.

  2. AllArgsConstructor - Generates a constructor with 1 parameter for each field in your class. Fields marked with NonNull annotation result in null checks on those parameters.

  3. NoArgsConstructor - Generates a constructor with no parameters. This is something needed by Jackson to instantiate the object during deserialization.

When we run this, we see something strange

$ mvn exec:java -q -Dexec.mainClass="Main"
Serialized JSON: {"hobbies":["swimming","soccer"],"personalHobbies":["swimming","soccer"]}
Deserialized Person: Person(hobbies=[swimming, soccer, swimming, soccer])

Two issues

  1. Two keys in JSON - Data annotation injects getters and setters. Since getPersonalHobbies would look like a getter generated by lombok for personalHobbies, jackson would treat it as a separate property and dump it as well in JSON.

  2. Duplicated hobbies in deserialized Person - For the "hobbies" key, Jackson finds the hobbies field and assigns the value ["swimming", "soccer"] to it. When Jackson encounters a getter method like getPersonalHobbies(), it treats it as a property to be populated with the deserialized data. This means that Jackson adds the list from "personalHobbies" to whatever list was already in the hobbies field. Since the hobbies and personalHobbies lists are referring to the same object (the list of strings), when Jackson deserializes both keys ("hobbies" and "personalHobbies"), the list is duplicated.

Fixes

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    public List<String> hobbies;

    @JsonIgnore
    public List<String> getPersonalHobbies() {
        return this.hobbies;
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    public List<String> hobbies;

    public List<String> getPersonalHobbies() {
        return new ArrayList<>(this.hobbies);
    }
}