Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ Lee Jiwon (@dlwldnjs1009)
* Contributed #6022: Follow-up to #5369: apply content @JsonInclude in
`StringCollectionSerializer.serializeWithType`
[3.2.1]
* Reported #6101: `@JsonInclude(NON_EMPTY, content=CUSTOM)` does not omit a Map property
after all entries are filtered
[3.2.2]

Garret Wilson (@garretwilson)
* Suggested #4157: Add `MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY` to ignore
Expand Down Expand Up @@ -596,6 +599,9 @@ Théo Szanto (@indyteo)
* Fixed #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully
remove empty collection during serialization
[3.2.1]
* Fixed #6101: `@JsonInclude(NON_EMPTY, content=CUSTOM)` does not omit a Map property
after all entries are filtered
[3.2.2]

@doeiqts
* Reported #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully
Expand Down
7 changes: 7 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ Versions: 3.x (for earlier see VERSION-2.x)

No changes since 3.2

3.2.2 (not yet released)

#6101: `@JsonInclude(NON_EMPTY, content=CUSTOM)` does not omit a Map property
after all entries are filtered
(reported by @dlwldnjs1009)
(fix by @seonwooj0810)

3.2.1 (10-Jul-2026)

#6022: Follow-up to #5369: apply content @JsonInclude in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ public boolean isEmpty(SerializationContext prov, Map<?,?> value)
if (!valueSer.isEmpty(prov, elemValue)) {
return false;
}
} else if ((supp == null) || !supp.equals(value)) {
} else if ((supp == null) || !supp.equals(elemValue)) {
return false;
}
}
Expand All @@ -514,7 +514,7 @@ public boolean isEmpty(SerializationContext prov, Map<?,?> value)
if (!valueSer.isEmpty(prov, elemValue)) {
return false;
}
} else if ((supp == null) || !supp.equals(value)) {
} else if ((supp == null) || !supp.equals(elemValue)) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package tools.jackson.databind.ser.filter;

import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;

// [databind#6101]: a NON_EMPTY Map property whose entries are all removed by a
// CUSTOM content filter should be treated as empty and omitted, matching the
// behavior for content=NON_EMPTY and the (already correct) write path.
public class JsonIncludeMapContentFilter6101Test extends DatabindTestUtil
{
// equals() returns true for values the content filter should suppress
static class FooFilter {
@Override
public boolean equals(Object other) {
return "foo".equals(other);
}

@Override
public int hashCode() { return 0; }
}

// Statically-typed value serializer (String) -> isEmpty() static-serializer loop
static class Bean {
@JsonInclude(value = JsonInclude.Include.NON_EMPTY,
content = JsonInclude.Include.CUSTOM,
contentFilter = FooFilter.class)
public Map<String, String> stuff = new LinkedHashMap<>();

public Bean add(String key, String value) {
stuff.put(key, value);
return this;
}
}

// Dynamically-resolved value serializer (Object) -> isEmpty() fallback loop
static class DynBean {
@JsonInclude(value = JsonInclude.Include.NON_EMPTY,
content = JsonInclude.Include.CUSTOM,
contentFilter = FooFilter.class)
public Map<String, Object> stuff = new LinkedHashMap<>();

public DynBean add(String key, Object value) {
stuff.put(key, value);
return this;
}
}

private final ObjectMapper MAPPER = new ObjectMapper();

// Before the fix this produced {"stuff":{}} because MapSerializer.isEmpty()
// compared the content filter to the whole Map instead of to each entry value.
@Test
public void mapEmptyAfterContentFilter() throws Exception {
Bean bean = new Bean().add("a", "foo").add("b", "foo");
assertEquals("{}", MAPPER.writeValueAsString(bean));
}

@Test
public void mapKeepsSurvivingEntries() throws Exception {
Bean bean = new Bean().add("a", "foo").add("b", "keep");
assertEquals("""
{"stuff":{"b":"keep"}}""",
MAPPER.writeValueAsString(bean));
}

@Test
public void genuinelyEmptyMapOmitted() throws Exception {
assertEquals("{}", MAPPER.writeValueAsString(new Bean()));
}

// Same bug on the fallback loop taken when the value serializer is not
// statically known (Map<String,Object>).
@Test
public void dynamicValueMapEmptyAfterContentFilter() throws Exception {
DynBean bean = new DynBean().add("a", "foo").add("b", "foo");
assertEquals("{}", MAPPER.writeValueAsString(bean));
}

@Test
public void dynamicValueMapKeepsSurvivingEntries() throws Exception {
DynBean bean = new DynBean().add("a", "foo").add("b", "keep");
assertEquals("""
{"stuff":{"b":"keep"}}""",
MAPPER.writeValueAsString(bean));
}
}