Adam Bronfin
Adam Bronfin

Reputation: 1279

Java Jackson deserialize null field to default empty list in POJO

I have a POJO with lombok annotations which my JSON deserializes to through Jackson like so:

@Getter
@ToString
@NoArgsConstructor
@Builder
@AllArgsConstructor
@EqualsAndHashCode
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResponsePOJO {
    @JsonProperty("list-one")
    private List<Object> list1 = Lists.newArrayList;
    @JsonProperty("list-two")
    private List<Object> list2 = Lists.newArrayList;
    @JsonProperty("list-three")
    private List<Object> list3 = Lists.newArrayList;
    @JsonProperty("list-four")
    private List<Object> list4 = Lists.newArrayList;
}

When jackson attempts to deserialize a response where only list-one & list-two are present, I expect the resulting POJO to have properties list3 and list4 as an empty list which is the default value, but instead they are deserialized as null.

What is missing to insure all properties will either contain the corresponding value from the deserialized JSON, or empty list which is the default assigned value?

---Update---- This was not an issue until I upgraded from Spring 1.3.5 to 1.4.2, which also upgraded my Jackson version from 2.6.6 to 2.8.4

Upvotes: 2

Views: 9691

Answers (3)

Justin Wrobel
Justin Wrobel

Reputation: 2019

I ran into a similar issue deserializing a JSON object using Jackson (com.fasterxml.jackson) and Lombok's @Builder annotation. I was able to resolve this issue by creating an empty Builder class annotated with @JsonPOJOBuilder inside the class and then adding a @JsonDeserialize with a reference to the empty class. You can find more info here

For example,

@Getter
@ToString
@NoArgsConstructor
@Builder
@AllArgsConstructor
@EqualsAndHashCode
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(builder = ResponsePOJO.TypeBuilder.class)
public class ResponsePOJO {
    @JsonPOJOBuilder(withPrefix = "")
    @JsonIgnoreProperties(ModelUtils.TYPE_INFO_PROPERTY)

   public static class TypeBuilder{}//Empty builder class
                                    //Needed just to apply annotations
                                    //Lombok will fill it in later.

    @JsonProperty("list-one")
    private List<Object> list1 = Lists.newArrayList;
    @JsonProperty("list-two")
    private List<Object> list2 = Lists.newArrayList;
    @JsonProperty("list-three")
    private List<Object> list3 = Lists.newArrayList;
    @JsonProperty("list-four")
    private List<Object> list4 = Lists.newArrayList;
}

Upvotes: 0

mattis
mattis

Reputation: 211

Lombok's @Builder is not a direct problem here, cause Jackson does not use it for deserialization. However, Jackson is using non-default constructor with args, if it is available, for object instantiation. Otherwise it is using non-arg constructor. When you use @AllArgsConstructor on ResponsePOJO, Jackson takes created constructor which looks like this:

public ResponsePOJO(List<Object> list1, List<Object> list2, List<Object> list3, List<Object> list4) {
    this.list1 = list1;
    this.list2 = list2;
    this.list3 = list3;
    this.list4 = list4;
}

And when this constructor is invoked, your default value is discarded and null is assigned for non-existent fields in JSON.

While @Builder requires all arguments constructor, you have few ways to fix the issue:

  1. Not use @Builder and remove @AllArgsConstructor - sometimes you may not need any of these, but it depends on your case
  2. Remove @AllArgsConstructor and create custom builder, which is not using all arguments constructor inside, but the no-arg one.
  3. Use @Builder and create custom all arguments constructor similar to this one:

    public ResponsePOJO(List<Object> list1, List<Object> list2, List<Object> list3, List<Object> list4) {
        this.list1 = list1 == null ? Lists.newArrayList() : list1;
        this.list2 = list2 == null ? Lists.newArrayList() : list2;
        this.list3 = list3 == null ? Lists.newArrayList() : list3;
        this.list4 = list4 == null ? Lists.newArrayList() : list4;
    }
    

    Here Jackson would still invoke all arguments constructor above, but if some field does not exist OR exists and is null (this may have downstream impact on your business logic if in some cases you may expect null values, e.g. validation against nullity), it will always assign empty list.

Upvotes: 3

Roel Spilker
Roel Spilker

Reputation: 34452

The problem is that you're also adding @Builder. The initializer is moved to the builder, as you don't want to run the code twice.

Recent versions (1.16.16+) of lombok have the @Builder.Default that you can use to influence that behavior.

You can instruct Jackson to use the builder instead, and make your objects immutable. Builder is meant for immutable objects.

Disclosure: I am a lombok developer.

Upvotes: 1

Related Questions