skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    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.


  2. 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);
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search