0

I am trying to create an immutable DTO.

Therefore I have added the @Builder and @Getter Lombok annotation for creating immutable objects from Pizza.class. To prevent the ingredients field to be initialized with a mutable List, I have added the @Singular Lombok annotation.

DTO

@Builder
@Getter
public class Pizza {

    private final String name;
    @Singular
    private final List<String> ingredients;

}

Now if I create an API endpoint and try to send a pizza JSON to that endpoint, it somehow gets unmarshalled by Spring, but the result of that process is a mutable ingredient list.

Controller

@RestController
@RequestMapping("/api/v1/demo")
public class DemoController {

    @PostMapping("/pizza")
    Pizza addPizza(@RequestBody Pizza pizza) {
        pizza.getIngredients().add("Honey");
        return pizza;
    }

}

Request/ Response

Request body:
{
    "name": "Hawaii",
    "ingredients": ["Pineapple"]
}

Response body:
{
    "name": "Hawaii",
    "ingredients": [
        "Pineapple",
        "Honey"
    ]
}

The below code snippet is throwing a java.lang.UnsupportedOperationException, which indicates to me that the ingredients field is an unmodifiable list.

Code snippet

var ingredients = new ArrayList<String>();
ingredients.add("Pinapple");
var pizza2 = Pizza.builder().name("Hawaii").ingredients(ingredients).build();
pizza2.getIngredients().add("Honey");

My questions:

  • How is Spring Boot doing the marshalling/ unmarshalling of the request body/ response body?
  • How can I prevent Spring Boot from initializing the ingredients field with a modifiable list?

1 Answers1

1

Your list gets passed to a constructor in the builder, so it's overriding what @Singular is doing here. You can drop the Singular and Builder annotations, create your own builder, and deserialize through it. In the Pizza constructor, the list is made immutable.

@Getter
@JsonDeserialize(builder = Pizza.PizzaBuilder.class)
public static class Pizza {
    private final String name;
    private final List<String> ingredients;

    private Pizza(String name, List<String> ingredients) {
        this.name = name;
        this.ingredients = Collections.unmodifiableList(ingredients);
    }

    @JsonPOJOBuilder
    @Setter
    @Getter
    static class PizzaBuilder {
        List<String> ingredients;
        String name;

        PizzaBuilder name(String name) {
            this.name = name;
            return this;
        }

        PizzaBuilder ingredients(List<String> ingredients) {
            this.ingredients = ingredients;
            return this;
        }

        public Pizza build() {
            return new Pizza(name, ingredients);
        }
    }
}
lane.maxwell
  • 5,002
  • 1
  • 20
  • 30
  • Thank you for your fast answer. This was pointing me to the right direction. So what I think you mean is the following: a) Spring Boot default library for JSON mapping is Jackson b) Jackson is doing some reflection magic to use the private constructor for creating the pizza object, instead of the builder pattern. I would love to accept your answer since it helped me a lot, but my first question wasn't really answered. If I have been unclear with my question, please let me know. – Moritz Kampfinger Sep 30 '22 at 08:51