Skip to content

Latest commit

 

History

172 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jackson-databind-nullable

Build Status

This module provides a JsonNullable wrapper class and a Jackson module to serialize/deserialize it. The JsonNullable wrapper shall be used to wrap Java bean fields for which it is important to distinguish between an explicit "null" and the field not being present. A typical usage is when implementing Json Merge Patch where an explicit "null" has the meaning "set this field to null / remove this field" whereas a non-present field has the meaning "don't change the value of this field".

The module comes with an integrated ValueExtractor that automatically unwraps the contained value of the JsonNullable if used together with javax.validation Bean validation (JSR 380).

Note: a lot of people use Optional to bring this behavior. Although it kinda works, it's not a good idea because:

  • Beans shouldn't have Optional fields. Optional was designed to be used only as method return value.
  • Optional should never be null. The goal of Optional is to wrap the null and prevent NPE so the code should be designed to never assign null to an Optional. A code invoking a method returning an Optional should be confident that this Optional is not null.

Installation

The module is compatible with JDK8+

./mvnw clean install

Usage

JsonNullable shall primarily be used in bean fields.

If we have the following class

public static class Pet {
    
    @Size(max = 10)   
    public JsonNullable<String> name = JsonNullable.undefined();
    
    public Pet name(JsonNullable<String> name) {
        this.name = name;
        return this;
    }
}

And we instantiate the mapper either for JSON

import com.fasterxml.jackson.databind.ObjectMapper;

// ...

ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.registerModule(new JsonNullableModule());

or for XML

import com.fasterxml.jackson.dataformat.xml.XmlMapper;

// ...

XmlMapper xmlMapper = new XmlMapper();
xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
xmlMapper.registerModule(new JsonNullableModule());

Then we can serialize

assertEquals("{}", mapper.writeValueAsString(new Pet().name(JsonNullable.<String>undefined())));
assertEquals("{\"name\":null}", mapper.writeValueAsString(new Pet().name(JsonNullable.<String>of(null))));
assertEquals("{\"name\":\"Rex\"}", mapper.writeValueAsString(new Pet().name(JsonNullable.of("Rex"))));

and deserialize

assertEquals(JsonNullable.of("Rex"), mapper.readValue("{\"name\":\"Rex\"}", Pet.class).name);
assertEquals(JsonNullable.<String>of(null), mapper.readValue("{\"name\":null}", Pet.class).name);
assertEquals(JsonNullable.<String>undefined(), mapper.readValue("{}", Pet.class).name);

Blank strings

By default a blank string ("" or whitespace only) sent for a non-String JsonNullable deserializes to JsonNullable.undefined(), as if the property were absent. If your clients send a blank string to mean "clear this value" (common with PATCH requests), enable mapBlankStringToNull so it deserializes to JsonNullable.of(null) instead:

mapper.registerModule(new JsonNullableModule().mapBlankStringToNull(true));
// Jackson 3: JsonMapper mapper = JsonMapper.builder().addModule(new JsonNullableJackson3Module().mapBlankStringToNull(true)).build();

// given a bean with a JsonNullable<Integer> age property:
assertEquals(JsonNullable.<Integer>of(null), mapper.readValue("{\"age\":\"\"}", Person.class).age);

JsonNullable<String> properties are never affected: a blank string is a valid string value.

JsonNullable can also be used as a @JsonCreator constructor parameter. An absent property is passed to the constructor as JsonNullable.undefined() rather than as null, so it stays distinguishable from an explicit null.

On the class path, the ValueExtractor is registered automatically via the Java Service loader mechanism (see Limitations for the module path). The example class above will validate as follows

// instantiate javax.validation.Validator
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Pet myPet = new Pet().name(JsonNullable.of("My Pet's really long name"));
Set<ConstraintViolation<Pet>> validationResult = validator.validate(myPet);
assertEquals(1, validationResult.size());

Limitations

  • Doesn't work with @JsonUnwrapped.
  • On the module path with Java 17 or newer, nothing is registered through the Java Service loader: ObjectMapper.findModules() / findAndRegisterModules() won't find JsonNullableModule, Jackson 3 won't discover JsonNullableJackson3Module, and the ValueExtractor isn't picked up either. The Java 17 module descriptor declares Jackson 2, Jackson 3 and both validation APIs as optional, and service declarations for optional dependencies would fail module resolution when they are absent (see #100). Register the module explicitly instead:
    mapper.registerModule(new JsonNullableModule());                             // Jackson 2
    JsonMapper mapper = JsonMapper.builder().addModule(new JsonNullableJackson3Module()).build();  // Jackson 3
    
    Validator validator = Validation.byDefaultProvider().configure()
            .addValueExtractor(new JsonNullableJakartaValueExtractor())    // or JsonNullableValueExtractor for javax.validation
            .buildValidatorFactory().getValidator();
    On the class path all of these are registered automatically. On the module path with Java 9 to 16, JsonNullableModule is still discovered, but the ValueExtractor has to be registered with your validator explicitly on the module path regardless of Java version.

About

JsonNullable wrapper class and Jackson module to support meaningful null values

Topics

Resources

Stars

153 stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages