Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that sounds odd. It must be due to fundamental problem with @JsonValue handling usually changing type of value being serialized (POJO) into intermediate type; so assumption here being former is not "natural", but if latter is, we want to determine type serialization requirement on former.

Like @JsonValue annotated method returning String, for some POJO.

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...
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading