jackson-databind
jackson-databind copied to clipboard
@JsonValue ignores annotations on annotated field (particularly @JsonInclude)
Search before asking
- [X] I searched in the issues and found nothing similar.
Describe the bug
The problem is as follows:
- I want to write to a JSON object of the type Map<String, Object>, where the values are similar maps.
- I want to have behavior similar to
var objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
In other words, I should not write keys with null values, even in nested maps. BUT
- I do not have access to the objectMapper instance, so the problem must be solved "externally."
- And there are options for wrapper classes that can have annotations attached to them:
//????
public class NonNullWrapper<K, V> extends HashMap<K, V> {
//some constructors
}
//????
public class AnotherNonNullWrapper {
@JsonValue
//????
private final Map<String, Object> value;
public AnotherNonNullWrapper(Map<String, Object> value) {
this.value = value;
}
}
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