diff --git a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java index fc8da3a1df..1370667cc1 100644 --- a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java +++ b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java @@ -280,6 +280,7 @@ protected TypeResolverBuilder _findTypeResolver(MapperConfig config, typeInfo = typeInfo.withInclusionType(JsonTypeInfo.As.PROPERTY); } + // [databind:4983] baseType comes from the annotated class/interface, not the serialized type detectedBaseType = ai.findPolymorphicBaseType(config, annotatedClass, typeInfo, baseType); } else { // when method/field annotated, declared type MUST be intended base type diff --git a/src/main/java/tools/jackson/databind/ser/jackson/JsonValueSerializer.java b/src/main/java/tools/jackson/databind/ser/jackson/JsonValueSerializer.java index 7d91917207..2868d670a0 100644 --- a/src/main/java/tools/jackson/databind/ser/jackson/JsonValueSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jackson/JsonValueSerializer.java @@ -180,12 +180,26 @@ public ValueSerializer createContextual(SerializationContext ctxt, * cases where "native" (aka "natural") type is being serialized, * using standard serializer */ - boolean forceTypeInformation = isNaturalTypeWithStdHandling(_valueType.getRawClass(), ser); + boolean forceTypeInformation; + if (_accessor == null) { + // Why do we forceTypeInformation if the type is natural? Shouldn't it be the contrary? + forceTypeInformation = isNaturalTypeWithStdHandling(_valueType.getRawClass(), ser); + } else { + // We came here due to a `@JsonValue`: the type of the accessed value is irrelevant as it is not the type of the original value + forceTypeInformation = false; + } return withResolved(property, vts, ser, forceTypeInformation); } // [databind#2822]: better hold on to "property", regardless if (property != _property) { - return withResolved(property, vts, ser, _forceTypeInformation); + boolean forceTypeInformation; + if (_accessor == null) { + forceTypeInformation = this._forceTypeInformation; + } else { + // We came here due to a `@JsonValue`: the type of the accessed value is irrelevant as it is not the type of the original value + forceTypeInformation = false; + } + return withResolved(property, vts, ser, forceTypeInformation); } } else { // 05-Sep-2013, tatu: I _think_ this can be considered a primary property... diff --git a/src/test/java/tools/jackson/databind/jsontype/TestJsonTypeInfoJsonValue.java b/src/test/java/tools/jackson/databind/jsontype/TestJsonTypeInfoJsonValue.java new file mode 100644 index 0000000000..2667010f74 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/TestJsonTypeInfoJsonValue.java @@ -0,0 +1,266 @@ +package tools.jackson.databind.jsontype; + +import java.util.Locale; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; + +import tools.jackson.databind.ObjectMapper; + +// Investigate around AsPropertyTypeSerializer +// tools.jackson.databind.ser.jackson.JsonValueSerializer.serializeWithType(Object, JsonGenerator, SerializationContext, TypeSerializer) +// tools.jackson.databind.ser.BasicSerializerFactory.findSerializerByAnnotations(SerializationContext, JavaType, Supplier) +public class TestJsonTypeInfoJsonValue { + + @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = AroundString.class) + public interface AroundSomething { + Object getInner(); + } + + public static class AroundString implements AroundSomething { + @JsonValue + String inner; + + @JsonCreator + public AroundString(String inner) { + this.inner = inner; + } + + @Override + public Object getInner() { + return inner; + } + + public void setInner(String inner) { + this.inner = inner; + } + + } + + public static class AroundObject implements AroundSomething { + @JsonValue + Object inner; + + @JsonCreator + public AroundObject(Object inner) { + this.inner = inner; + } + + @Override + public Object getInner() { + return inner; + } + + public void setInner(String inner) { + this.inner = inner; + } + + } + + public static class AroundObject_NotJsonValue implements AroundSomething { + Object inner; + + @Override + public Object getInner() { + return inner; + } + + public void setInner(String inner) { + this.inner = inner; + } + + } + + public static class HasAround { + AroundSomething wrapped; + + public AroundSomething getWrapped() { + return wrapped; + } + + public void setC(AroundSomething wrapped) { + this.wrapped = wrapped; + } + } + + @Test + public void aroundString() { + AroundString matcher = new AroundString("foo"); + + HasAround wrapper = new HasAround(); + wrapper.setC(matcher); + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(wrapper); + Assertions.assertThat(asString).isEqualTo("{\"wrapped\":\"foo\"}"); + + HasAround fromString = objectMapper.readValue(asString, HasAround.class); + Assertions.assertThat(fromString.getWrapped().getInner()).isEqualTo("foo"); + } + + @Test + public void aroundObject_simpleType() { + AroundObject matcher = new AroundObject("foo"); + + HasAround wrapper = new HasAround(); + wrapper.setC(matcher); + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(wrapper); + Assertions.assertThat(asString).isEqualTo("{\"wrapped\":\"foo\"}"); + + HasAround fromString = objectMapper.readValue(asString, HasAround.class); + Assertions.assertThat(fromString.getWrapped().getInner()).isEqualTo("foo"); + } + + @Test + public void aroundObject_complexType() { + AroundObject matcher = new AroundObject(Map.of("foo", "bar")); + + HasAround wrapper = new HasAround(); + wrapper.setC(matcher); + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(wrapper); + Assertions.assertThat(asString) + .isEqualTo("{\"wrapped\":{\"type\":\".TestJsonTypeInfoJsonValue$AroundObject\",\"foo\":\"bar\"}}"); + + HasAround fromString = objectMapper.readValue(asString, HasAround.class); + Assertions.assertThat(fromString.getWrapped().getInner()).isEqualTo(Map.of("foo", "bar")); + } + + @Test + public void aroundObjectNotJsonValue() { + AroundObject_NotJsonValue matcher = new AroundObject_NotJsonValue(); + matcher.setInner("foo"); + + HasAround wrapper = new HasAround(); + wrapper.setC(matcher); + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(wrapper); + Assertions.assertThat(asString) + .isEqualTo( + "{\"wrapped\":{\"type\":\".TestJsonTypeInfoJsonValue$AroundObject_NotJsonValue\",\"inner\":\"foo\"}}"); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = NativeOption.class) + public interface SomeOption { + } + + public static enum NativeOption implements SomeOption { + A, B; + + @JsonValue + public String toString() { + return this.name(); + } + + @JsonCreator + public static NativeOption forValue(String value) { + return NativeOption.valueOf(value.toUpperCase(Locale.US)); + } + } + + public static enum CustomOption implements SomeOption { + C, D; + + @JsonCreator + public static CustomOption forValue(String value) { + return CustomOption.valueOf(value.toUpperCase(Locale.US)); + } + } + + + public static enum CustomOption_WithJsonValue implements SomeOption { + C, D; + + @JsonValue + public String asString() { + return this.name(); + } + + @JsonCreator + public static CustomOption forValue(String value) { + return CustomOption.valueOf(value.toUpperCase(Locale.US)); + } + } + + public static enum OptionWithoutJsonTypeInfo { + E, F; + + @JsonCreator + public static OptionWithoutJsonTypeInfo forValue(String value) { + return OptionWithoutJsonTypeInfo.valueOf(value.toUpperCase(Locale.US)); + } + + } + + @Test + public void testEnum_Native() { + NativeOption matcher = NativeOption.A; + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(matcher); + Assertions.assertThat(asString).isEqualTo("\"A\""); + + SomeOption fromString = objectMapper.readValue(asString, SomeOption.class); + Assertions.assertThat(fromString).isSameAs(matcher); + } + + @Test + public void testEnum_Custom() { + CustomOption matcher = CustomOption.C; + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(matcher); + Assertions.assertThat(asString).isEqualTo("[\"TestJsonTypeInfoJsonValue$CustomOption\",\"C\"]"); + + SomeOption fromString = objectMapper.readValue(asString, SomeOption.class); + Assertions.assertThat(fromString).isSameAs(matcher); + } + + @Test + public void testEnum_Custom_jsonValue() { + CustomOption_WithJsonValue matcher = CustomOption_WithJsonValue.C; + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(matcher); + Assertions.assertThat(asString).isEqualTo("[\"TestJsonTypeInfoJsonValue$CustomOption\",\"C\"]"); + + SomeOption fromString = objectMapper.readValue(asString, SomeOption.class); + Assertions.assertThat(fromString).isSameAs(matcher); + } + + // To be removed, just to help debugging a standard scenario + @Test + public void testEnum_noJsonTypeInfo() { + OptionWithoutJsonTypeInfo matcher = OptionWithoutJsonTypeInfo.E; + + ObjectMapper objectMapper = new ObjectMapper(); + + String asString = objectMapper.writeValueAsString(matcher); + Assertions.assertThat(asString).isEqualTo("\"E\""); + + OptionWithoutJsonTypeInfo fromString = objectMapper.readValue(asString, OptionWithoutJsonTypeInfo.class); + Assertions.assertThat(fromString).isSameAs(matcher); + } +} \ No newline at end of file