diff --git a/release-notes/VERSION b/release-notes/VERSION index df98713858..44c020520c 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -11,6 +11,9 @@ Versions: 3.x (for earlier see VERSION-2.x) #1196: Add opt-in error collection for deserialization (requested by @odrotbohm) (contributed by @sri-adarsh-kumar) +#1654: @JsonDeserialize(contentUsing=...) is ignored if content + type is determined by @JsonTypeInfo + (reported by @pdegoeje) #1980: Add method `remove(JsonPointer)` in `ContainerNode` (fix by @cowtowncoder, w/ Claude code) #3964: Deserialization issue: MismatchedInputException, Bean not diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index e7585cd05f..1c489ad2c7 100644 --- a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java @@ -24,6 +24,7 @@ import tools.jackson.databind.ext.jdk8.OptionalLongDeserializer; import tools.jackson.databind.introspect.*; import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeResolverBuilder; import tools.jackson.databind.type.*; import tools.jackson.databind.util.*; @@ -771,6 +772,7 @@ public ValueDeserializer createCollectionDeserializer(DeserializationContext if (contentTypeDeser == null) { contentTypeDeser = ctxt.findTypeDeserializer(contentType); } + // 23-Nov-2010, tatu: Custom deserializer? ValueDeserializer deser = _findCustomCollectionDeserializer(type, config, beanDescRef, contentTypeDeser, contentDeser); diff --git a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java index 8f9f3bd11f..ba906ca261 100644 --- a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java +++ b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java @@ -9,6 +9,8 @@ import tools.jackson.databind.introspect.Annotated; import tools.jackson.databind.introspect.AnnotatedClass; import tools.jackson.databind.introspect.AnnotatedMember; +import tools.jackson.databind.jsontype.impl.NoOpTypeDeserializer; +import tools.jackson.databind.jsontype.impl.NoOpTypeSerializer; import tools.jackson.databind.jsontype.impl.StdTypeResolverBuilder; /** @@ -113,6 +115,10 @@ public TypeSerializer findPropertyTypeSerializer(SerializationContext ctxt, if (b == null) { return findTypeSerializer(ctxt, baseType, ctxt.introspectClassAnnotations(baseType)); } + // [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info + if (b == NO_RESOLVER) { + return null; + } Collection subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByClass( config, accessor, baseType); // 10-Jun-2015, tatu: Since not created for Bean Property, no need for post-processing @@ -133,6 +139,10 @@ public TypeDeserializer findPropertyTypeDeserializer(DeserializationContext ctxt if (b == null) { return findTypeDeserializer(ctxt, baseType, ctxt.introspectClassAnnotations(baseType)); } + // [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info + if (b == NO_RESOLVER) { + return null; + } Collection subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByTypeId(config, accessor, baseType); // May need to figure out default implementation, if none found yet @@ -162,6 +172,10 @@ public TypeSerializer findPropertyContentTypeSerializer(SerializationContext ctx return findTypeSerializer(ctxt, contentType, ctxt.introspectClassAnnotations(contentType.getRawClass())); } + // [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info + if (b == NO_RESOLVER) { + return NoOpTypeSerializer.instance(); + } Collection subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByClass( config, accessor, contentType); return b.buildTypeSerializer(ctxt, contentType, subtypes); @@ -181,6 +195,10 @@ public TypeDeserializer findPropertyContentTypeDeserializer(DeserializationConte if (b == null) { return findTypeDeserializer(ctxt, contentType, ctxt.introspectClassAnnotations(contentType)); } + // [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info + if (b == NO_RESOLVER) { + return NoOpTypeDeserializer.forBaseType(ctxt, contentType); + } Collection subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByTypeId(config, accessor, contentType); // May need to figure out default implementation, if none found yet diff --git a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java new file mode 100644 index 0000000000..0c259b3c85 --- /dev/null +++ b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java @@ -0,0 +1,116 @@ +package tools.jackson.databind.jsontype.impl; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import tools.jackson.core.*; +import tools.jackson.databind.*; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.util.ClassUtil; + +/** + * Special {@link TypeDeserializer} implementation used to explicitly + * block type deserialization. This is used when a property or class + * is annotated with {@code @JsonTypeInfo(use = Id.NONE)}, indicating + * that type information should not be expected or processed even if + * the value type has a class-level type info annotation. + *

+ * Unlike returning {@code null} (which means "no special type handling, + * use defaults"), this actively prevents type information from being read. + * + * @since 3.1 + */ +public class NoOpTypeDeserializer extends TypeDeserializer +{ + private final JavaType _baseType; + private final BeanProperty _property; + + // Dynamically constructed deserializer + private volatile ValueDeserializer _deserializer; + + private NoOpTypeDeserializer(JavaType baseType, BeanProperty prop) { + _baseType = baseType; + _property = prop; + } + + public static NoOpTypeDeserializer forBaseType(DeserializationContext ctxt, + JavaType baseType) { + return new NoOpTypeDeserializer(baseType, null); + } + + @Override + public TypeDeserializer forProperty(BeanProperty prop) { + if (_property == prop) { + return this; + } + return new NoOpTypeDeserializer(_baseType, prop); + } + + @Override + public JsonTypeInfo.As getTypeInclusion() { + // No proper value but need to return something + return JsonTypeInfo.As.EXISTING_PROPERTY; + } + + @Override + public String getPropertyName() { + return null; + } + + @Override + public TypeIdResolver getTypeIdResolver() { + return null; + } + + @Override + public Class getDefaultImpl() { + return null; + } + + @Override + public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + return _deserialize(p, ctxt); + } + + @Override + public Object deserializeTypedFromArray(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + return _deserialize(p, ctxt); + } + + @Override + public Object deserializeTypedFromScalar(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + return _deserialize(p, ctxt); + } + + @Override + public Object deserializeTypedFromAny(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + return _deserialize(p, ctxt); + } + + protected Object _deserialize(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + ValueDeserializer deser = _deserializer; + + // Find deserializer for the base type, given property (if any). + // This will find custom deserializers registered for this type, + // including those from @JsonDeserialize annotations) + if (deser == null) { + deser = ctxt.findContextualValueDeserializer(_baseType, _property); + if (deser == null) { + ctxt.reportBadDefinition(_baseType, + "Cannot find deserializer for type " +ClassUtil.getTypeDescription(_baseType)); + } + _deserializer = deser; + } + return deser.deserialize(p, ctxt); + } +} diff --git a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java new file mode 100644 index 0000000000..0820ea70ea --- /dev/null +++ b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java @@ -0,0 +1,85 @@ +package tools.jackson.databind.jsontype.impl; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import tools.jackson.core.*; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeSerializer; + +/** + * Special {@link TypeSerializer} implementation used to explicitly + * block type serialization. This is used when a property or class + * is annotated with {@code @JsonTypeInfo(use = Id.NONE)}, indicating + * that type information should not be included even if the value type + * has a class-level type info annotation. + *

+ * Unlike returning {@code null} (which means "no special type handling, + * use defaults"), this actively prevents type information from being written. + * + * @since 3.1 + */ +public class NoOpTypeSerializer extends TypeSerializer +{ + private static final NoOpTypeSerializer INSTANCE = new NoOpTypeSerializer(); + + private NoOpTypeSerializer() { } + + public static NoOpTypeSerializer instance() { + return INSTANCE; + } + + @Override + public TypeSerializer forProperty(SerializationContext ctxt, BeanProperty prop) { + return this; + } + + @Override + public JsonTypeInfo.As getTypeInclusion() { + // No proper one to use but must return something: + return JsonTypeInfo.As.EXISTING_PROPERTY; + } + + @Override + public String getPropertyName() { + return null; + } + + @Override + public TypeIdResolver getTypeIdResolver() { + return null; + } + + @Override + public WritableTypeId writeTypePrefix(JsonGenerator g, + SerializationContext ctxt, WritableTypeId typeId) + throws JacksonException + { + // Write the value start token if needed, but NO type information + if (typeId.valueShape == JsonToken.START_OBJECT) { + g.writeStartObject(typeId.forValue); + } else if (typeId.valueShape == JsonToken.START_ARRAY) { + g.writeStartArray(); + } + // 1. Start marker (part of value) was written but + // 2. No value wrapper was written. + typeId.wrapperWritten = false; + return typeId; + } + + @Override + public WritableTypeId writeTypeSuffix(JsonGenerator g, + SerializationContext ctxt, WritableTypeId typeId) + throws JacksonException + { + // Write the value end token if needed, but no wrapper to close + if (typeId.valueShape == JsonToken.START_OBJECT) { + g.writeEndObject(); + } else if (typeId.valueShape == JsonToken.START_ARRAY) { + g.writeEndArray(); + } + return typeId; + } +} diff --git a/src/test/java/tools/jackson/databind/tofix/NoTypeInfo1654Test.java b/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java similarity index 57% rename from src/test/java/tools/jackson/databind/tofix/NoTypeInfo1654Test.java rename to src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java index 476811f895..fde2d4a2ef 100644 --- a/src/test/java/tools/jackson/databind/tofix/NoTypeInfo1654Test.java +++ b/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.tofix; +package tools.jackson.databind.jsontype; import java.util.*; @@ -6,22 +6,21 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.databind.*; import tools.jackson.databind.annotation.JsonDeserialize; import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; -class NoTypeInfo1654Test extends DatabindTestUtil { - +class NoTypeInfo1654Test extends DatabindTestUtil +{ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) static class Value1654 { public int x; - protected Value1654() { - } + protected Value1654() { } public Value1654(int x) { this.x = x; @@ -31,8 +30,7 @@ public Value1654(int x) { static class Value1654TypedContainer { public List values; - protected Value1654TypedContainer() { - } + protected Value1654TypedContainer() { } public Value1654TypedContainer(Value1654... v) { values = Arrays.asList(v); @@ -40,7 +38,7 @@ public Value1654TypedContainer(Value1654... v) { } static class Value1654UntypedContainer { - @JsonDeserialize(contentUsing = Value1654Deserializer.class) + //@JsonDeserialize(contentUsing = Value1654Deserializer.class) @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) public List values; @@ -52,34 +50,51 @@ public Value1654UntypedContainer(Value1654... v) { } } + static class Value1654UsingDeserializerUntypedContainer { + @JsonDeserialize(contentUsing = Value1654Deserializer.class) + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public List values; + + protected Value1654UsingDeserializerUntypedContainer() { + } + + public Value1654UsingDeserializerUntypedContainer(Value1654... v) { + values = Arrays.asList(v); + } + } + static class Value1654Deserializer extends ValueDeserializer { @Override public Value1654 deserialize(JsonParser p, DeserializationContext ctxt) { + //JsonNode n = ctxt.readTree(p); p.skipChildren(); return new Value1654(13); } } + private final ObjectMapper MAPPER = newJsonMapper(); - // [databind#1654] + // [databind#1654]: no override, default polymorphic type id @Test - void noTypeElementOverride() throws Exception { - // egular typed case + void withoutNoTypeElementOverrideSerAndDeser() throws Exception { + // regular typed case String json = MAPPER.writeValueAsString(new Value1654TypedContainer( new Value1654(1), - new Value1654(2), - new Value1654(3) + new Value1654(2) )); + String typeId = Value1654.class.getName(); + typeId = "'@type':'" + typeId.substring(typeId.lastIndexOf('.') + 1) + "'"; + assertEquals(a2q("{'values':[{"+typeId+",'x':1},{"+typeId+",'x':2}]}"), json); + Value1654TypedContainer result = MAPPER.readValue(json, Value1654TypedContainer.class); - assertEquals(3, result.values.size()); + assertEquals(2, result.values.size()); assertEquals(2, result.values.get(1).x); } - // [databind#1654] - @JacksonTestFailureExpected + // [databind#1654]: override, no polymorphic type id @Test - void noTypeInfoOverrideSer() throws Exception { + void withNoTypeInfoOverrideSer() throws Exception { Value1654UntypedContainer cont = new Value1654UntypedContainer( new Value1654(3), new Value1654(7) @@ -89,9 +104,8 @@ void noTypeInfoOverrideSer() throws Exception { } // [databind#1654] - @JacksonTestFailureExpected @Test - void noTypeInfoOverrideDeser() throws Exception { + void withNoTypeInfoDeser() throws Exception { // and then actual failing case final String noTypeJson = a2q( "{'values':[{'x':3},{'x':7}]}" @@ -100,4 +114,17 @@ void noTypeInfoOverrideDeser() throws Exception { assertEquals(2, unResult.values.size()); assertEquals(7, unResult.values.get(1).x); } + + // [databind#1654] + @Test + void withNoTypeInfoOverrideDeser() throws Exception { + // and then actual failing case + final String noTypeJson = a2q( + "{'values':[{'x':3},{'x':7}]}" + ); + Value1654UsingDeserializerUntypedContainer unResult = MAPPER.readValue(noTypeJson, Value1654UsingDeserializerUntypedContainer.class); + assertEquals(2, unResult.values.size()); + assertEquals(13, unResult.values.get(0).x); + assertEquals(13, unResult.values.get(1).x); + } }