JsonValueInclusion5380Test.java
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 com.fasterxml.jackson.annotation.JsonValue;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.*;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.databind.ser.jackson.JsonValueSerializer;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertEquals;
// [databind#5380]: `@JsonValue` should honor inclusion configured (via config
// override) for the type it serializes as
class JsonValueInclusion5380Test extends DatabindTestUtil
{
record JsonValuePojo(String stringValue) {
@JsonValue
public String toString() { return stringValue; }
}
// non-final variant, to exercise dynamic serializer lookup
static class NonFinalValue {
private final String value;
NonFinalValue(String value) { this.value = value; }
@JsonValue
public String value() { return value; }
}
record WrapperPojo(JsonValuePojo jsonValuePojo) { }
record NonFinalWrapper(NonFinalValue value) { }
record StringPojo(String stringValue) { }
record AnnotatedWrapper(@JsonInclude(JsonInclude.Include.NON_EMPTY) JsonValuePojo jsonValuePojo) { }
// `@JsonInclude` on the `@JsonValue` accessor itself
record AccessorAnnotatedValue(String value) {
@JsonValue
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public String toString() { return value; }
}
record AccessorAnnotatedWrapper(AccessorAnnotatedValue value) { }
record AccessorAlwaysValue(String value) {
@JsonValue
@JsonInclude(JsonInclude.Include.ALWAYS)
public String toString() { return value; }
}
record AccessorAlwaysWrapper(AccessorAlwaysValue value) { }
record AnnotatedAccessorAlwaysWrapper(
@JsonInclude(JsonInclude.Include.NON_EMPTY) AccessorAlwaysValue value) { }
// Nested `@JsonValue`: outer serializes as inner, which serializes as String
record OuterValue(JsonValuePojo inner) {
@JsonValue
public JsonValuePojo inner() { return inner; }
}
record NestedWrapper(@JsonInclude(JsonInclude.Include.NON_EMPTY) OuterValue value) { }
// Non-Record wrapper, needed for global NON_DEFAULT to fall back to "empty" check
static class PojoWrapper {
public JsonValuePojo value;
PojoWrapper(JsonValuePojo value) { this.value = value; }
}
// Custom sub-class: must be retained through contextualization
static class CustomJsonValueSerializer extends JsonValueSerializer {
CustomJsonValueSerializer(JsonValueSerializer src) {
super(src, null, null, null, true);
}
CustomJsonValueSerializer(CustomJsonValueSerializer src, BeanProperty property,
TypeSerializer vts, ValueSerializer<?> ser, boolean forceTypeInfo,
boolean emptyCheckDisabled) {
super(src, property, vts, ser, forceTypeInfo, emptyCheckDisabled);
}
@Override
public JsonValueSerializer withResolved(BeanProperty property,
TypeSerializer vts, ValueSerializer<?> ser, boolean forceTypeInfo) {
return new CustomJsonValueSerializer(this, property, vts, ser, forceTypeInfo,
_emptyCheckDisabled);
}
@Override
public JsonValueSerializer withEmptyCheckDisabled(boolean state) {
return new CustomJsonValueSerializer(this, _property, _valueTypeSerializer,
_valueSerializer, _forceTypeInformation, state);
}
@Override
public void serialize(Object bean, JsonGenerator gen, SerializationContext ctxt) {
gen.writeString("custom:" + bean);
}
}
record MapWrapper(@JsonInclude(content = JsonInclude.Include.NON_EMPTY)
Map<String, JsonValuePojo> values) { }
record ObjectMapWrapper(@JsonInclude(content = JsonInclude.Include.NON_EMPTY)
Object values) { }
private final ObjectMapper MAPPER = mapperBuilder()
.withConfigOverride(String.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)))
.build();
private static JsonMapper.Builder mapperBuilder() {
return JsonMapper.builder()
.changeDefaultPropertyInclusion(incl -> JsonInclude.Value.construct(
JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY));
}
@Test
void plainStringProperty() throws Exception {
assertEquals("""
{"stringValue":""}""", MAPPER.writeValueAsString(new StringPojo("")));
}
@Test
void jsonValueUsesValueTypeOverride() throws Exception {
assertEquals("""
{"jsonValuePojo":""}""",
MAPPER.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
assertEquals("""
{"jsonValuePojo":"x"}""",
MAPPER.writeValueAsString(new WrapperPojo(new JsonValuePojo("x"))));
// nulls still suppressed, as per NON_NULL
assertEquals("{}", MAPPER.writeValueAsString(new WrapperPojo(null)));
assertEquals("{}", MAPPER.writeValueAsString(new WrapperPojo(new JsonValuePojo(null))));
}
@Test
void jsonValueNonFinalUsesValueTypeOverride() throws Exception {
assertEquals("""
{"value":""}""",
MAPPER.writeValueAsString(new NonFinalWrapper(new NonFinalValue(""))));
}
// Without override for value type, global NON_EMPTY applies
@Test
void jsonValueWithoutOverride() throws Exception {
ObjectMapper mapper = mapperBuilder().build();
assertEquals("{}", mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
}
// Explicit annotation on property has precedence over value type override
@Test
void propertyAnnotationHasPrecedence() throws Exception {
assertEquals("{}",
MAPPER.writeValueAsString(new AnnotatedWrapper(new JsonValuePojo(""))));
}
// As does annotation on the `@JsonValue` accessor
@Test
void accessorAnnotationHasPrecedence() throws Exception {
assertEquals("{}", MAPPER.writeValueAsString(
new AccessorAnnotatedWrapper(new AccessorAnnotatedValue(""))));
}
// Accessor annotation that does not exclude empty values is used as well;
// with or without value type override
@Test
void accessorAnnotationAllowingEmpty() throws Exception {
assertEquals("""
{"value":""}""", MAPPER.writeValueAsString(
new AccessorAlwaysWrapper(new AccessorAlwaysValue(""))));
assertEquals("""
{"value":""}""", mapperBuilder().build().writeValueAsString(
new AccessorAlwaysWrapper(new AccessorAlwaysValue(""))));
}
// ... but annotation on enclosing property has precedence over accessor annotation
@Test
void propertyAnnotationBeatsAccessorAnnotation() throws Exception {
assertEquals("{}", MAPPER.writeValueAsString(
new AnnotatedAccessorAlwaysWrapper(new AccessorAlwaysValue(""))));
}
// Explicit property annotation must also win for nested `@JsonValue`
@Test
void nestedJsonValueHonorsPropertyAnnotation() throws Exception {
assertEquals("{}", MAPPER.writeValueAsString(
new NestedWrapper(new OuterValue(new JsonValuePojo("")))));
assertEquals("""
{"value":"x"}""", MAPPER.writeValueAsString(
new NestedWrapper(new OuterValue(new JsonValuePojo("x")))));
}
// As does override for declared property type
@Test
void declaredTypeOverrideHasPrecedence() throws Exception {
ObjectMapper mapper = mapperBuilder()
.withConfigOverride(String.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null)))
.withConfigOverride(JsonValuePojo.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, null)))
.build();
assertEquals("{}", mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
}
// Override "include" (not just "include as property") for declared type is
// also used for property inclusion, so it too has precedence
@Test
void declaredTypeIncludeOverrideHasPrecedence() throws Exception {
ObjectMapper mapper = mapperBuilder()
.withConfigOverride(String.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null)))
.withConfigOverride(JsonValuePojo.class, o -> o.setInclude(
JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, null)))
.build();
assertEquals("{}", mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
}
// Global NON_DEFAULT falls back to "empty" check for POJO-valued property
@Test
void nonDefaultInclusion() throws Exception {
JsonMapper.Builder b = JsonMapper.builder()
.changeDefaultPropertyInclusion(incl -> JsonInclude.Value.construct(
JsonInclude.Include.NON_DEFAULT, JsonInclude.Include.NON_DEFAULT));
assertEquals("{}", b.build().writeValueAsString(new PojoWrapper(new JsonValuePojo(""))));
ObjectMapper mapper = b.withConfigOverride(String.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null)))
.build();
assertEquals("""
{"value":""}""", mapper.writeValueAsString(new PojoWrapper(new JsonValuePojo(""))));
assertEquals("{}", mapper.writeValueAsString(new PojoWrapper(null)));
}
// Custom sub-class retained, with and without empty check disabled
@Test
void customSubclassRetained() throws Exception {
SimpleModule mod = new SimpleModule().setSerializerModifier(new ValueSerializerModifier() {
private static final long serialVersionUID = 1L;
@Override
public ValueSerializer<?> modifySerializer(SerializationConfig config,
BeanDescription.Supplier beanDesc, ValueSerializer<?> serializer) {
if (serializer instanceof JsonValueSerializer jvs) {
return new CustomJsonValueSerializer(jvs);
}
return serializer;
}
});
// No override for value type: empty value suppressed
ObjectMapper mapper = mapperBuilder().addModule(mod).build();
assertEquals("""
{"jsonValuePojo":"custom:x"}""",
mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo("x"))));
assertEquals("{}", mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
// With override: empty value retained, custom serializer still used
mapper = mapperBuilder().addModule(mod)
.withConfigOverride(String.class, o -> o.setIncludeAsProperty(
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null)))
.build();
assertEquals("""
{"jsonValuePojo":"custom:"}""",
mapper.writeValueAsString(new WrapperPojo(new JsonValuePojo(""))));
}
// Content inclusion of container properties not affected
@Test
void mapContentInclusionNotAffected() throws Exception {
Map<String, JsonValuePojo> values = new LinkedHashMap<>();
values.put("a", new JsonValuePojo(""));
values.put("b", new JsonValuePojo("x"));
assertEquals("""
{"values":{"b":"x"}}""", MAPPER.writeValueAsString(new MapWrapper(values)));
// including case of `Object`-typed property
assertEquals("""
{"values":{"b":"x"}}""", MAPPER.writeValueAsString(new ObjectMapWrapper(values)));
}
}