0

Assuming I have a POJO like this:

public static class Test {
  private Optional<String> something;
}

And a Jackson mapper like this:

var OBJECT_MAPPER = JsonMapper.builder()
  .addModule(new Jdk8Module())
  .serializationInclusion(JsonInclude.Include.NON_ABSENT)
  .build();

If I deserialize the JSON string {"something": null}, I get a Test object with something=null. Instead of that, I want to get a Test object with something=Optional.EMPTY.

My desired deserialization strategy for Optional is:

  • null → not provided
  • Optional.empty() → provided null
  • Optional.of() → provided not null

The above can be achieved if I keep the default serializationInclusion (remove the serializationInclusion(JsonInclude.Include.NON_ABSENT) setting). This results in serializing everything and including nulls in the JSON string output, which is not what I want.

Test test = new Test();
// This should serialize to {}, not {"something": null}

Test test2 = new Test();
test2.setSomething(Optional.EMPTY);
// This should also serialize to {}, not {"something": null}

TLTR: is there a way to separate the serializationInclusion separately for serialization and deserialization?

k.liakos
  • 639
  • 6
  • 11
  • Did you try to use Include.NON_NULL. That way Optional logic would still work and null values wont be included into the serialized json. But if you want to separate options you can define 2 ObjectMapper`s, one for each purpose or change the config every time you use it – Yury Mar 25 '23 at 21:57
  • you seem to be doing something wrong: that is definitely bad idea to use `Optional` as object's field. – Andrey B. Panfilov Mar 25 '23 at 23:47
  • @Yury I tried the Include.NON_NULL as well, doesn't make any difference vs Include.NON_ABSENT. It looks like the OptionalDeserializer is never invoked if a field has null literal value. – k.liakos Mar 26 '23 at 07:29
  • @AndreyB.Panfilov Oracle seems to have different opinion: https://www.oracle.com/technical-resources/articles/java/java8-optional.html – k.liakos Mar 26 '23 at 07:30
  • @Yury btw yes, the 2 ObjectMappers is a solution I thought of and could work, however I want to avoid it if possible. – k.liakos Mar 26 '23 at 07:31
  • @k.liakos that is not an "Oracle's opinion", you just found someone's misleading blogpost under oracle.com domain. [You should almost never use it as a field of something or a method parameter](https://stackoverflow.com/a/26328555/3426309) – Andrey B. Panfilov Mar 26 '23 at 07:56
  • @AndreyB.Panfilov Well you seem to be right. However they say "almost" never. I want to have a tri-state value for PATCH update, in order to differentiate between a value not been provided (=don't update the field) vs provided null (=clear the field's value). And Optional seems like a nice feat for the case (Javascript has undefined+null to separate this). I could have added a custom wrapper class, but I would have to rewrite all Serializers/Deserializers and handling for JSON. – k.liakos Mar 26 '23 at 08:07
  • another way to deal with it is to use objectMapper.readerForUpdating (https://www.logicbig.com/tutorials/misc/jackson/reader-for-updating.html). That will only update/deserialize values coming in json, and you dont have to use Optional for fields. – Yury Mar 26 '23 at 10:26

2 Answers2

1

You can do this with a custom serializer:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;

import java.io.IOException;
import java.util.Optional;

public class Main {

    public static void main(String[] args) throws JsonProcessingException {
        var OBJECT_MAPPER = JsonMapper.builder()
                .addModule(new Jdk8Module())
                .serializationInclusion(JsonInclude.Include.NON_ABSENT)
                .build();
        System.out.println(OBJECT_MAPPER.writeValueAsString(new Test(null)));
        System.out.println(OBJECT_MAPPER.writeValueAsString(new Test(Optional.empty())));
        System.out.println(OBJECT_MAPPER.writeValueAsString(new Test(Optional.of("jim"))));

        System.out.println(OBJECT_MAPPER.readValue("{}", Test.class).toString());
        System.out.println(OBJECT_MAPPER.readValue("{\"something\":null}", Test.class).toString());
        System.out.println(OBJECT_MAPPER.readValue("{\"something\":\"jim\"}", Test.class).toString());
    }
}

class Test {
    @JsonSerialize(using = MyOptionalSerializer.class)
    private Optional<String> something;

    public Test() {}

    Test(Optional<String> something) {
        this.something = something;
    }

    public Optional<String> getSomething() {
        return something;
    }

    public void setSomething(Optional<String> something) {
        this.something = something;
    }

    @Override
    public String toString() {
        return "Test{" +
                "something=" + something +
                '}';
    }
}

class MyOptionalSerializer extends JsonSerializer<Optional<?>> {

    @Override
    public void serialize(Optional<?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) {
            if (value.isPresent()) {
                gen.writeObject(value.get());
            } else {
                gen.writeObject(null);
            }
        }
    }
}
tgdavies
  • 10,307
  • 4
  • 35
  • 40
0

I figured out what I was doing wrong, and it was very simple, I just got stuck: I wasn't using readValue for Deserialization, but instead convertValue.

As the javadoc of convertValue mentions:

This method is functionally similar to first serializing given value into JSON, and then binding JSON data into value of given type

So since the serializationInclusion was set to skip nulls on output, the field was being removed completely in the intermediate step.

Reproduction example:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.junit.Test;

import java.util.HashMap;
import java.util.Optional;

public class JacksonOptionalTest {
    @Test
    public void testOptional() throws JsonProcessingException {
        var OBJECT_MAPPER = JsonMapper.builder()
                .addModule(new Jdk8Module())
                .serializationInclusion(JsonInclude.Include.NON_ABSENT)
                .build();

        System.out.println("writeValueAsString");
        System.out.println(OBJECT_MAPPER.writeValueAsString(new MyClass(null)));
        System.out.println(OBJECT_MAPPER.writeValueAsString(new MyClass(Optional.empty())));
        System.out.println(OBJECT_MAPPER.writeValueAsString(new MyClass(Optional.of("jim"))));

        System.out.println("\nreadValue");
        System.out.println(OBJECT_MAPPER.readValue("{}", MyClass.class).toString());
        System.out.println(OBJECT_MAPPER.readValue("{\"something\":null}", MyClass.class).toString());
        System.out.println(OBJECT_MAPPER.readValue("{\"something\":\"jim\"}", MyClass.class).toString());


        final var map = new HashMap<>();
        map.put("something", null);

        System.out.println("\nconvertValue");
        System.out.println(OBJECT_MAPPER.convertValue(map, MyClass.class).toString());
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    private static class MyClass {
        @JsonProperty
        private Optional<String> something;
    }
}

If you run the above test, it prints the following:

writeValueAsString
{}
{}
{"something":"jim"}

readValue
JacksonOptionalTest.MyClass(something=null)
JacksonOptionalTest.MyClass(something=Optional.empty)
JacksonOptionalTest.MyClass(something=Optional[jim])

convertValue
JacksonOptionalTest.MyClass(something=null)

In the last test with convertValue we can see that instead of deserializing the value to Optional.empty, we got null, which is expected according to what convertValue does.

k.liakos
  • 639
  • 6
  • 11