From 1749676b5f70b64a2d9b382031fc1717593b41d8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 8 Feb 2026 12:44:31 -0800 Subject: [PATCH] Possible fix for #5380 --- .../jackson/databind/ser/PropertyBuilder.java | 11 +++ ...sonValueIncludeConfigOverride5380Test.java | 89 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/test/java/tools/jackson/databind/ser/filter/JsonValueIncludeConfigOverride5380Test.java diff --git a/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java b/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java index 7844076863..cca077abba 100644 --- a/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java +++ b/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java @@ -136,6 +136,17 @@ protected BeanPropertyWriter buildWriter(SerializationContext ctxt, } Class rawPropertyType = accessor.getRawType(); + // [databind#5380]: If property type uses @JsonValue, use the @JsonValue return type + // for config override inclusion lookup (since that's the actual serialized type) + if (!rawPropertyType.isPrimitive() && !rawPropertyType.isArray() + && !ClassUtil.isJDKClass(rawPropertyType)) { + AnnotatedMember jsonValueAccessor = ctxt.introspectBeanDescription(actualType) + .findJsonValueAccessor(); + if (jsonValueAccessor != null) { + rawPropertyType = jsonValueAccessor.getRawType(); + } + } + // 17-Aug-2016, tatu: Default inclusion covers global default (for all types), as well // as type-default for enclosing POJO. What we need, then, is per-type default (if any) // for declared property type... and finally property annotation overrides diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonValueIncludeConfigOverride5380Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonValueIncludeConfigOverride5380Test.java new file mode 100644 index 0000000000..03f7923124 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonValueIncludeConfigOverride5380Test.java @@ -0,0 +1,89 @@ +package tools.jackson.databind.ser.filter; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonValue; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +// [databind#5380]: @JsonValue should respect config override for serialized-as type +public class JsonValueIncludeConfigOverride5380Test + extends DatabindTestUtil +{ + static class JsonValueWrapper { + @JsonValue + public String value() { return _value; } + + private final String _value; + + JsonValueWrapper(String v) { _value = v; } + } + + static class Pojo { + public JsonValueWrapper wrapped; + public String direct; + + Pojo(JsonValueWrapper w, String d) { + wrapped = w; + direct = d; + } + } + + // Default: NON_EMPTY globally, NON_NULL override for String as property type. + // Empty string should NOT be suppressed because String override is NON_NULL (not NON_EMPTY). + // Before fix, "wrapped" was excluded because inclusion was looked up for JsonValueWrapper + // (which has no override), falling back to the NON_EMPTY default. + @Test + public void testJsonValueRespectsStringConfigOverride() throws Exception + { + ObjectMapper mapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(v -> JsonInclude.Value.construct( + JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)) + .withConfigOverride(String.class, o -> + o.setIncludeAsProperty(JsonInclude.Value.construct( + JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))) + .build(); + + // Empty string with NON_NULL override for String => should be included + Pojo pojo = new Pojo(new JsonValueWrapper(""), ""); + String json = mapper.writeValueAsString(pojo); + assertEquals(a2q("{'direct':'','wrapped':''}"), json); + } + + // Null wrapper objects should still be suppressed by NON_EMPTY default + @Test + public void testNullWrapperSuppressed() throws Exception + { + ObjectMapper mapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(v -> JsonInclude.Value.construct( + JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)) + .withConfigOverride(String.class, o -> + o.setIncludeAsProperty(JsonInclude.Value.construct( + JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))) + .build(); + + // null wrapper with NON_NULL for String property => wrapper itself is null, suppressed + Pojo pojo = new Pojo(null, null); + String json = mapper.writeValueAsString(pojo); + assertEquals("{}", json); + } + + // Without config override, default NON_EMPTY should suppress empty strings + @Test + public void testJsonValueDefaultNonEmptyApplies() throws Exception + { + ObjectMapper mapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(v -> JsonInclude.Value.construct( + JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)) + .build(); + + Pojo pojo = new Pojo(new JsonValueWrapper(""), ""); + String json = mapper.writeValueAsString(pojo); + assertEquals("{}", json); + } +}