FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.52k stars 1.38k forks source link

@JsonValue ignores annotations on annotated field (particularly @JsonInclude) #4762

Open sikorskii opened 2 days ago

sikorskii commented 2 days ago

Search before asking

Describe the bug

The problem is as follows:

But when placing @JsonInclude(...NON_NULL) above the NonNullWrapper class, nulls are removed only at the top level. Jackson then sees regular maps in the values, which naturally don’t have annotations, and applies the default policy.

Similarly, Jackson ignores such annotations on a field marked with @JsonValue. Inside Jackson's source code, during serialization (with @JsonValue), the JsonValueSerializer simply does not have information about the annotations that are attached to the marked field.

In the end, the question is: Is this behavior expected? Can this task be solved using annotations or other ways? However, I would prefer not to write my own JsonSerializer, Filter, or similar things for this.

Version Information

2.15.3 (but still fails on 2.18.0)

Reproduction

@JsonInclude(value = JsonInclude.Include.NON_NULL, content = JsonInclude.Include.NON_NULL)
public class NonNullWrapper<K, V> extends HashMap<K, V> {

    public NonNullWrapper(Map<K, V> map) {
        super(map);
    }
}

public class AnotherNonNullWrapper {

    @JsonValue
    @JsonInclude(value = JsonInclude.Include.NON_NULL, content = JsonInclude.Include.NON_NULL)
    private final Map<String, Object> value;

    public AnotherNonNullWrapper(Map<String, Object> value) {
        this.value = value;
    }
}

public static final TypeReference<Map<String, Object>> MAP_REF = new TypeReference<>() {};
    public static final TypeReference<List<Object>> LIST_REF = new TypeReference<>() {};

    @Test
    public void test() throws JsonProcessingException {
        var map = new HashMap<String, Object>();
        map.put("ignored", null);
        Map<String, Object> nested = new HashMap<>();
        nested.put("ignored", null);
        nested.put("array", List.of(map, map));
        Map<String, Object> topLevel = new HashMap<>();
        topLevel.put("ignored", null);
        topLevel.put("empty", "");
        topLevel.put("nested", nested);

        var expected = """
            {
              "nested": {
                "array": [
                  {
                  },
                  {
                  }
                ]
              },
              "empty": ""
            }
            """;

        var serialized = getDefaultMapper().writeValueAsString(new NonNullWrapper OR AnotherNonNullWrapper(topLevel));

        assertMapEqualsAfterSerialization(serialized, expected);
    }

    private void assertMapEqualsAfterSerialization(String mapAsJson, String expected) throws JsonProcessingException {
        assertEqualsAfterSerialization(mapAsJson, expected, MAP_REF);
    }

    private void assertEqualsAfterSerialization(String sourceJson, String expectedJson, TypeReference<?> tr)
        throws JsonProcessingException {
        var sourceMap = mapper.readValue(sourceJson, tr);
        var expectedMap = mapper.readValue(expectedJson, tr);

        var resultJson = mapper.writeValueAsString(sourceMap);
        var resultMap = mapper.readValue(resultJson, tr);

        assertThat(resultMap).isEqualTo(expectedMap);
    }

Results

NonNullWrapper

org.opentest4j.AssertionFailedError: 
expected: {"empty"="", "nested"={"array"=[{}, {}]}}
 but was: {"empty"="", "nested"={"array"=[{"ignored"=null}, {"ignored"=null}], "ignored"=null}}
Expected :{"empty"="", "nested"={"array"=[{}, {}]}}
Actual   :{"empty"="", "nested"={"array"=[{"ignored"=null}, {"ignored"=null}], "ignored"=null}}

AnotherNonNullWrapper

org.opentest4j.AssertionFailedError: 
expected: {"empty"="", "nested"={"array"=[{}, {}]}}
 but was: {"empty"="", "ignored"=null, "nested"={"array"=[{"ignored"=null}, {"ignored"=null}], "ignored"=null}}
Expected :{"empty"="", "nested"={"array"=[{}, {}]}}
Actual   :{"empty"="", "ignored"=null, "nested"={"array"=[{"ignored"=null}, {"ignored"=null}], "ignored"=null}}

Expected behavior

@JsonInclude annotation has effect on nested maps or/and @JsonValue marked field

Additional context

No response

JooHyukKim commented 2 days ago

Please try later versions of Jackson, like recently released 2.18 and please let us know!

I have a feeling nested deserialization of Map<String, Object> might not work.... 🤔

sikorskii commented 2 days ago

@JooHyukKim I tried version 2.18.0 and behavior did not change

JooHyukKim commented 2 days ago

I have a feeling nested deserialization of Map<String, Object> might not work.... 🤔

Okay, probably this is the cause. Will try to look into it later! Thank you for reporting and providing reproduction tho

cowtowncoder commented 1 day ago

From quick look:

  1. Ideally, it should work, but
  2. I am not surprised it doesn't; contextual annotations probably are not applied to @JsonValue serialized values

So adding a failing test (under src/test/java/.../tofix) would make sense.

Fix is probably not trivial to do, although I could be wrong: contextualization should be done against accessor (Field, Getter) on which @JsonValue is applied. But there might not be BeanProperty created for it (as it's not really a property).