Skip to content

How to correctly configure JsonMapper to deserialize Optional.empty() instead of null in case of missing property #5335

@wendigo

Description

@wendigo

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Optional handling differs between Jackson 2.x and 3.0 despite the information that Jdk8 module was pulled in to core.

Version Information

3.0

Reproduction

I have a record class with an Optional field. When I use JsonMapper from Jackson 3.0, I'm unable to configure it in a way that will inject Optional.empty if either value is null or absent in the JSON.

Works in Jackson 2.x:

public class TestOptional
{
    public static void main(String[] args) throws JsonProcessingException {
        JsonMapper.Builder builder = JsonMapper.builder();
        builder.addModule(new Jdk8Module());
        builder.defaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.ALWAYS));

        Person person = new Person("Alice", Optional.empty());
        ObjectMapper mapper = builder.build();

        System.out.println(mapper.writeValueAsString(person));
        System.out.println(mapper.readValue(mapper.writeValueAsString(person), Person.class));
        System.out.println(mapper.readValue("{\"name\": \"Alice\"}}", Person.class));
        System.out.println(mapper.readValue("{\"name\": \"Alice\", \"nickname\": null}}", Person.class));
    }

    record Person(String name, Optional<String> nickname)
    {
        public Person {
            requireNonNull(name, "name is null");
            requireNonNull(nickname, "nickname is null");
        }
    }
}

Doesn't work in Jackson 3:

public class TestOptional
{
    public static void main(String[] args)
    {
        JsonMapper.Builder builder = JsonMapper.builder();
        builder.changeDefaultPropertyInclusion(_ -> JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.ALWAYS));

        Person person = new Person("Alice", Optional.empty());
        ObjectMapper mapper = builder.build();

        System.out.println(mapper.writeValueAsString(person));
        System.out.println(mapper.readValue(mapper.writeValueAsString(person), Person.class));
        System.out.println(mapper.readValue("{\"name\": \"Alice\"}}", Person.class));
        System.out.println(mapper.readValue("{\"name\": \"Alice\", \"nickname\": null}}", Person.class));
    }

    record Person(String name, Optional<String> nickname)
    {
        public Person {
            requireNonNull(name, "name is null");
            requireNonNull(nickname, "nickname is null");
        }
    }
}

Result:

{"name":"Alice"}
Exception in thread "main" tools.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `io.airlift.json.TestOptional$Person`, problem: nickname is null
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN]
	at tools.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.java:44)
	at tools.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:2076)
	at tools.jackson.databind.deser.std.StdValueInstantiator.wrapAsDatabindException(StdValueInstantiator.java:581)
	at tools.jackson.databind.deser.std.StdValueInstantiator.rewrapCtorProblem(StdValueInstantiator.java:602)
	at tools.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:289)
	at tools.jackson.databind.deser.ValueInstantiator.createFromObjectWith(ValueInstantiator.java:270)
	at tools.jackson.databind.deser.bean.PropertyBasedCreator.build(PropertyBasedCreator.java:252)
	at tools.jackson.databind.deser.bean.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:697)
	at tools.jackson.databind.deser.bean.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1417)
	at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:480)
	at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:200)
	at tools.jackson.databind.deser.DeserializationContextExt.readRootValue(DeserializationContextExt.java:265)
	at tools.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2610)
	at tools.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1522)
	at io.airlift.json.TestOptional.main(TestOptional.java:22)
Caused by: java.lang.NullPointerException: nickname is null
	at java.base/java.util.Objects.requireNonNull(Objects.java:246)
	at io.airlift.json.TestOptional$Person.<init>(TestOptional.java:31)
	at java.base/java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:735)
	at tools.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:113)
	at tools.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:287)
	... 10 more

Process finished with exit code 1

Expected behavior

Optionals should work :)

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    to-evaluateIssue that has been received but not yet evaluated

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions