diff --git a/release-notes/VERSION b/release-notes/VERSION index 44c020520c..e4b7bf5dd8 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -14,6 +14,7 @@ Versions: 3.x (for earlier see VERSION-2.x) #1654: @JsonDeserialize(contentUsing=...) is ignored if content type is determined by @JsonTypeInfo (reported by @pdegoeje) + (fix by @cowtowncoder, @JacksonJang) #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/ValueSerializer.java b/src/main/java/tools/jackson/databind/ValueSerializer.java index 95aaf99a50..d138228af7 100644 --- a/src/main/java/tools/jackson/databind/ValueSerializer.java +++ b/src/main/java/tools/jackson/databind/ValueSerializer.java @@ -4,6 +4,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import tools.jackson.core.*; import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; @@ -252,13 +253,20 @@ public void serializeWithType(T value, JsonGenerator gen, SerializationContext c TypeSerializer typeSer) throws JacksonException { + // 07-Dec-2025, tatu: [databind#1654] Check for "no-op" type serializer + // indirectly + if (typeSer.getTypeInclusion() == As.NOTHING) { + serialize(value, gen, ctxt); + return; + } + Class clz = handledType(); if (clz == null) { clz = value.getClass(); } ctxt.reportBadDefinition(clz, String.format( - "Type id handling not implemented for type %s (by serializer of type %s)", - clz.getName(), getClass().getName())); +"Type id handling (method `serializeWithType()`) not implemented for type %s (by serializer of type %s)", + ClassUtil.nameOf(clz), ClassUtil.nameOf(getClass()))); } /* diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index e7585cd05f..36c559b08f 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.impl.NoOpTypeDeserializer; import tools.jackson.databind.type.*; import tools.jackson.databind.util.*; @@ -767,8 +768,12 @@ public ValueDeserializer createCollectionDeserializer(DeserializationContext // Then optional type info: if type has been resolved, we may already know type deserializer: TypeDeserializer contentTypeDeser = (TypeDeserializer) contentType.getTypeHandler(); - // but if not, may still be possible to find: - if (contentTypeDeser == null) { + // [databind#1654]: @JsonTypeInfo(use = Id.NONE) should not apply type deserializer + // when custom content deserializer is specified via @JsonDeserialize(contentUsing = ...) + if (contentTypeDeser instanceof NoOpTypeDeserializer) { + contentTypeDeser = null; + } else if (contentTypeDeser == null) { + // but if not, may still be possible to find: contentTypeDeser = ctxt.findTypeDeserializer(contentType); } // 23-Nov-2010, tatu: Custom deserializer? diff --git a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java index 2a5d915156..7c6de1b8bb 100644 --- a/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java +++ b/src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java @@ -119,6 +119,7 @@ public TypeSerializer findPropertyTypeSerializer(SerializationContext ctxt, if (b == NO_RESOLVER) { // 07-Dec-2025, tatu: Should we actually do this? (No test coverage yet) //return NoOpTypeSerializer.instance(); + return null; } Collection subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByClass( diff --git a/src/main/java/tools/jackson/databind/jsontype/TypeSerializer.java b/src/main/java/tools/jackson/databind/jsontype/TypeSerializer.java index 7974b55962..c943133d5d 100644 --- a/src/main/java/tools/jackson/databind/jsontype/TypeSerializer.java +++ b/src/main/java/tools/jackson/databind/jsontype/TypeSerializer.java @@ -92,6 +92,10 @@ public WritableTypeId typeId(Object value, JsonToken valueShape) { case WRAPPER_OBJECT: typeIdDef.include = WritableTypeId.Inclusion.WRAPPER_OBJECT; break; + case NOTHING: + // 07-Dec-2025, tatu: No suitable constant to use. Should add "NOTHING"? + typeIdDef.include = null; + break; default: VersionUtil.throwInternal(); } diff --git a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java index 0c259b3c85..bed72ec4d1 100644 --- a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java +++ b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeDeserializer.java @@ -48,8 +48,7 @@ public TypeDeserializer forProperty(BeanProperty prop) { @Override public JsonTypeInfo.As getTypeInclusion() { - // No proper value but need to return something - return JsonTypeInfo.As.EXISTING_PROPERTY; + return JsonTypeInfo.As.NOTHING; } @Override diff --git a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java index 0820ea70ea..371091177a 100644 --- a/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java +++ b/src/main/java/tools/jackson/databind/jsontype/impl/NoOpTypeSerializer.java @@ -38,8 +38,7 @@ public TypeSerializer forProperty(SerializationContext ctxt, BeanProperty prop) @Override public JsonTypeInfo.As getTypeInclusion() { - // No proper one to use but must return something: - return JsonTypeInfo.As.EXISTING_PROPERTY; + return JsonTypeInfo.As.NOTHING; } @Override diff --git a/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java b/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java index dfabf39b31..280cc0d099 100644 --- a/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java +++ b/src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java @@ -6,9 +6,13 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; import tools.jackson.core.JsonParser; + import tools.jackson.databind.*; import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; import tools.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -37,28 +41,63 @@ public Value1654TypedContainer(Value1654... v) { } static class Value1654UntypedContainer { - //@JsonDeserialize(contentUsing = Value1654Deserializer.class) @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) public List values; - protected Value1654UntypedContainer() { - } + protected Value1654UntypedContainer() { } public Value1654UntypedContainer(Value1654... v) { values = Arrays.asList(v); } } - /* + static class Value1654UsingCustomSerDeserUntypedContainer { + @JsonDeserialize(contentUsing = Value1654Deserializer.class) + @JsonSerialize(contentUsing = Value1654Serializer.class) + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public List values; + + protected Value1654UsingCustomSerDeserUntypedContainer() { } + + public Value1654UsingCustomSerDeserUntypedContainer(Value1654... v) { + values = Arrays.asList(v); + } + } + + static class SingleValue1654UsingCustomSerDeserUntyped { + @JsonDeserialize(using = Value1654Deserializer.class) + @JsonSerialize(using = Value1654Serializer.class) + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public Value1654 value; + + protected SingleValue1654UsingCustomSerDeserUntyped() { } + + public SingleValue1654UsingCustomSerDeserUntyped(Value1654 v) { + value = 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); + JsonNode n = ctxt.readTree(p); + if (!n.has("v")) { + ctxt.reportInputMismatch(Value1654.class, "Bad JSON input (no 'v'): " + n); + } + return new Value1654(n.path("v").intValue()); + } + } + + + static class Value1654Serializer extends ValueSerializer { + @Override + public void serialize(Value1654 value, JsonGenerator gen, SerializationContext ctxt) + throws JacksonException { + gen.writeStartObject(value); + gen.writeNumberProperty("v", value.x); + gen.writeEndObject(); } } - */ private final ObjectMapper MAPPER = newJsonMapper(); @@ -79,9 +118,9 @@ void withoutNoTypeElementOverrideSerAndDeser() throws Exception { assertEquals(2, result.values.get(1).x); } - // [databind#1654]: override, no polymorphic type id + // [databind#1654]: override, no polymorphic type id, serialization @Test - void withNoTypeInfoOverrideSer() throws Exception { + void withNoTypeInfoDefaultSer() throws Exception { Value1654UntypedContainer cont = new Value1654UntypedContainer( new Value1654(3), new Value1654(7) @@ -90,15 +129,59 @@ void withNoTypeInfoOverrideSer() throws Exception { MAPPER.writeValueAsString(cont)); } - // [databind#1654] + // [databind#1654]: override, no polymorphic type id, deserialization @Test - void withNoTypeInfoOverrideDeser() throws Exception { - // and then actual failing case + void withNoTypeInfoDefaultDeser() throws Exception { final String noTypeJson = a2q( "{'values':[{'x':3},{'x':7}]}" ); - Value1654UntypedContainer unResult = MAPPER.readValue(noTypeJson, Value1654UntypedContainer.class); + Value1654UntypedContainer unResult = MAPPER.readValue(noTypeJson, + Value1654UntypedContainer.class); assertEquals(2, unResult.values.size()); assertEquals(7, unResult.values.get(1).x); } + + // [databind#1654]: override, no polymorphic type id, custom serialization + @Test + void withNoTypeInfoOverrideSer() throws Exception { + Value1654UsingCustomSerDeserUntypedContainer cont = new Value1654UsingCustomSerDeserUntypedContainer( + new Value1654(1), + new Value1654(2) + ); + assertEquals(a2q("{'values':[{'v':1},{'v':2}]}"), + MAPPER.writeValueAsString(cont)); + } + + // [databind#1654]: override, no polymorphic type id, custom deserialization + @Test + void withNoTypeInfoOverrideDeser() throws Exception { + final String noTypeJson = a2q( + "{'values':[{'v':3},{'v':7}]}" + ); + Value1654UsingCustomSerDeserUntypedContainer unResult = MAPPER.readValue(noTypeJson, + Value1654UsingCustomSerDeserUntypedContainer.class); + assertEquals(2, unResult.values.size()); + assertEquals(3, unResult.values.get(0).x); + assertEquals(7, unResult.values.get(1).x); + } + + // // And then validation for individual value, not in Container + + // override, no polymorphic type id, custom serialization + @Test + void singleWithNoTypeInfoOverrideSer() throws Exception { + SingleValue1654UsingCustomSerDeserUntyped wrapper = new SingleValue1654UsingCustomSerDeserUntyped( + new Value1654(42)); + assertEquals(a2q("{'value':{'v':42}}"), + MAPPER.writeValueAsString(wrapper)); + } + + // override, no polymorphic type id, custom deserialization + @Test + void singleWithNoTypeInfoOverrideDeser() throws Exception { + String noTypeJson = a2q("{'value':{'v':42}}"); + SingleValue1654UsingCustomSerDeserUntyped result = MAPPER.readValue(noTypeJson, + SingleValue1654UsingCustomSerDeserUntyped.class); + assertEquals(42,result.value.x); + } }