35

I have an entity that contains collection as attribute:

public class Entity {

    @JsonProperty(value="homes")
    @JsonDeserialize(as=HashSet.class, contentAs=HomeImpl.class)
    private Collection<Home> homes = new ArrayList<Home>();

}

If request contains null as request property:

{
  "homes": null
}

then homes is set to null. What I want to do is to set homes to empty list. Do I need to write special deserializer for this or is there one for collections? What I tried is this deserializer but it looks ugly (it's not generic and uses implementation instead of interface).

public class NotNullCollectionDeserializer extends JsonDeserializer<Collection<HomeImpl>> {

  @Override
  public Collection<HomeImpl> deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
    return jsonParser.readValueAs(new TypeReference<Collection<HomeImpl>>(){});
  }

  @Override
  public Collection<HomeImpl> getNullValue() {
    return Collections.emptyList();
  }
}

So few questions:

  1. Is there some jackson property that changes null to empty collection during deserialization?
  2. If no for the first point - do I need to write deserializer for this? If yes, can I write generic one?
Manos Nikolaidis
  • 21,608
  • 12
  • 74
  • 82
pepuch
  • 6,346
  • 7
  • 51
  • 84

4 Answers4

55

As of Jackson 2.9, it looks like null-handling for specific properties can be configured with @JsonSetter, for example:

@JsonSetter(nulls = Nulls.AS_EMPTY)
public void setStrings(List<String> strings) {
    this.strings = strings;
}

Similar configuration can also be applied globally for collections:

ObjectMapper mapper = objectMapperBuilder()
    .changeDefaultNullHandling(n -> n.withContentNulls(Nulls.AS_EMPTY))
    .build();

Or by type:

ObjectMapper mapper = objectMapperBuilder()
    .withConfigOverride(List.class,
        o -> o.setNullHandling(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)))
    .build();

I haven't been able to try the feature out, so this is based on the feature discussion and examination of unit tests. YMMV.

Mike Partridge
  • 5,128
  • 8
  • 35
  • 47
  • 24
    To be clear, there is not method `objectMapperBuilder()` in Jackson 2.9. There is `jsonMapperBuilder()` in 2.10, which is just helper method in Jackson tests and it just returns `JsonMapper.builder()`, where `JsonMapper` is a new helper class introduced in 2.10. In 2.9 the defaults can be configured using `ObjectMapper`, like this `mapper.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY));` or for specific type: `mapper.configOverride(List.class).setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY));`. – Ruslan Stelmachenko Jul 10 '19 at 09:58
  • 8
    + @RuslanStelmachenko `forContentNulls` is for null array elements, the `forValueNulls` is needed for entire null array: `mapper.configOverride(List.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));` – Roman Sinyakov Oct 09 '20 at 08:56
  • 6
    Note that per the Javadoc, this only works if the value is explicitly set to null, rather than omitted from the JSON. `{"homes": null}` vs. `{}`. – M. Justin Feb 02 '21 at 00:43
  • @M.Justin You are right. So question is how to get empty collection after deserialization if the attribute is completely omitted in json (reqeust). – sasynkamil Dec 22 '22 at 08:07
  • 2
    @sasynkamil initialize it with new keyword on field level, it won't be overridden by jackson – Conrad Jan 05 '23 at 13:44
15

I also couldn't find a Jackson property or annotation for this. So I'll have to answer no to the first question. But I would recommend a simple setter instead of the special deserializer :

public class Entity {

    @JsonDeserialize(contentAs = HomeImpl.class)
    private Collection<Home> homes = new ArrayList<>();

    public void setHomes(List<Home> homes) {
        if (homes != null)
            this.homes = homes;
    }
}

This is generic as it only uses the Home interface instead of HomeImpl. You don't need @JsonProperty as Jackson will associate setHomes and homes.

Manos Nikolaidis
  • 21,608
  • 12
  • 74
  • 82
  • 2
    One thing that drove me crazy: You cannot combine `@JsonDeserialize` and `@JsonSetter(nulls=Nulls.AS_EMPTY)`. You'll have to implement `JsonDeserializer#getNullValue()` and remove `@JsonSetter` – Ich Nov 18 '20 at 20:45
3

What worked for me was simply to remove the setter and make the attribute final. jackson 2 will then use the getter to modify the list.

public class Entity {

  @JsonProperty(value="homes")
  @JsonDeserialize(as=HashSet.class, contentAs=HomeImpl.class)
  private final Collection<Home> homes = new ArrayList<Home>();

  public List<Home> getHomes() {
     return homes;
  }
}

The responsible feature is USE_GETTERS_AS_SETTERS which is turned on by default: https://github.com/FasterXML/jackson-databind/wiki/Mapper-Features

Malik Atalla
  • 193
  • 8
0

I think the cleanest solution in my case which is working as expected (get empty list instead of null after deserialization) in both cases:

  1. property is omitted in the json (request)
  2. property is explicitly set to null in the json (request)

is:

@Valid // javax.validation
@Schema(required = false) // swagger.v3 
@JsonProperty(required = false, defaultValue = "") // jackson 2.13
private List<@NotEmpty @Size(max = 100) String> actions = new ArrayList<>();

public List<String> getActions() {
    return actions;
}

@JsonSetter(nulls = Nulls.AS_EMPTY)
public void setActions(List<String> actions) {
    this.actions = actions;
}

Notes:

  • when property explicitly set to null: used default annotations
  • when property omitted: added initialization = new ArrayList<>();
  • when used e.g. java validation, then this solution is needed just for optional (required = false) lists
sasynkamil
  • 859
  • 2
  • 12
  • 23