jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Unable to deserialize @JsonUnwrapped Optional<> field using Jackson

Open vbarhatov opened this issue 5 years ago • 4 comments

I tried to apply @JsonUnwrapped annotation to Optional<> field and can't figure out why deserialization turns this field into null value.

I wrote a test to demonstrate this issue and marked a place where the test fails: // throws java.lang.AssertionError

Tried this with jackson-databind:2.9.10.4 and jackson-databind:2.11.0 using corresponding version of jackson-datatype-jdk8. The result is the same.

public class JacksonTest {
    public static class Person {
        @JsonUnwrapped
        public MainData mainData;
        @JsonUnwrapped
        public Optional<AdditionalData> additionalData;
    }

    public static class MainData {
        public String name;
    }

    public static class AdditionalData {
        public String address;
    }

    @Test
    public void jsonUnwrappedWithOptionalTest() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new Jdk8Module());

        Person expected = new Person();
        expected.mainData = new MainData();
        expected.mainData.name = "Homer";
        expected.additionalData = Optional.of(new AdditionalData());
        expected.additionalData.get().address = "Springfield";

        String json = mapper.writeValueAsString(expected);

        assertThat(json, is("{\"name\":\"Homer\",\"address\":\"Springfield\"}"));

        Person actual = mapper.readValue(json, Person.class);

        assertNotNull(actual.mainData);
        assertThat(actual.mainData.name, is("Homer"));
        assertNotNull(actual.additionalData); // throws java.lang.AssertionError; actual.additionalData is null
        assertThat(actual.additionalData.isPresent(), is(true));
        assertThat(actual.additionalData.get().address, is("Springfield"));
    }
}

It looks like a bug. Isn't it?

WORKAROUND: Finally, I added a custom deserializer using @JsonDeserialize as a workaround but I would like to find a right solution.

vbarhatov avatar May 26 '20 12:05 vbarhatov

One quick note: it is not a good idea to try to combine @JsonUnwrapped with Optional. That is unlikely to work, at all, since semantics of combination are unclear (unwrapping would refer to POJO referenced by Optional, and Optional itself is sort of wrapper type as well.

It would be nice to catch such usage (perhaps I should try to see how to fail on such declarations -- but this may be tricky to make work reliably).

cowtowncoder avatar May 26 '20 18:05 cowtowncoder

@cowtowncoder could you explain why this might be ambiguous? Serialization works well for this case and I just expect that it will work for deserialization too.

vbarhatov avatar May 27 '20 14:05 vbarhatov

Serialization is trivially simple compared to deserialization, unfortunately, and sides are not symmetric in many ways: for example, actual runtime type is available during serialization but not deserialization (only declared type in class definitions). But it gets worse for unwrapping as serialization can basically just omit START_OBJECT / END_OBJECT wrapping and write properties without any consideration on scoping. Deserialization will have to try to figure out who should get which property, and since delegation model is designed to be mostly context-free ("parent" deserializer delegates to "child" but there is very little interaction otherwise). So it is not currently possible (for example) to know which properties might be "unknown" if wrapping is involved, or detect collisions. In case of Optional and @JsonUnwrapped we also have conceptual nesting of unwrapping (Optional can be seen as single value wrapper). Situation would be different if shape of Optional was originally defined as, say, JSON Array (actual physical wrapper) instead of logical wrapping with nothing in JSON indicating it.

Or put another way: implementation of a set of interacting features, some early design choices, make this particular combination difficult to implement. In fact, I am not sure if it was a good idea to ever add @JsonUnwrapped at all, due to difficulty in actually implementing it work correctly.

Having said all that maybe it is possible to make this work, eventually (maybe Jackson 3.0), so I'll keep this issue open. But in the meantime it is probably best to avoid trying to use this combination.

cowtowncoder avatar May 27 '20 16:05 cowtowncoder

@vbarhatov how did you deserialize it?

anirbandas18 avatar Jan 05 '25 23:01 anirbandas18