Skip to content

Commit 5d67470

Browse files
authored
Merge branch '3.x' into 5342-fix-prevent-name-conflict-JsonAnyGetter
2 parents 62b4247 + 44900ef commit 5d67470

File tree

5 files changed

+248
-20
lines changed

5 files changed

+248
-20
lines changed

release-notes/VERSION

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Versions: 3.x (for earlier see VERSION-2.x)
1111
#1196: Add opt-in error collection for deserialization
1212
(requested by @odrotbohm)
1313
(contributed by @sri-adarsh-kumar)
14+
#1654: @JsonDeserialize(contentUsing=...) is ignored if content
15+
type is determined by @JsonTypeInfo
16+
(reported by @pdegoeje)
1417
#1980: Add method `remove(JsonPointer)` in `ContainerNode`
1518
(fix by @cowtowncoder, w/ Claude code)
1619
#3964: Deserialization issue: MismatchedInputException, Bean not

src/main/java/tools/jackson/databind/jsontype/TypeResolverProvider.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import tools.jackson.databind.introspect.Annotated;
1010
import tools.jackson.databind.introspect.AnnotatedClass;
1111
import tools.jackson.databind.introspect.AnnotatedMember;
12+
import tools.jackson.databind.jsontype.impl.NoOpTypeDeserializer;
13+
import tools.jackson.databind.jsontype.impl.NoOpTypeSerializer;
1214
import tools.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
1315

1416
/**
@@ -113,6 +115,12 @@ public TypeSerializer findPropertyTypeSerializer(SerializationContext ctxt,
113115
if (b == null) {
114116
return findTypeSerializer(ctxt, baseType, ctxt.introspectClassAnnotations(baseType));
115117
}
118+
// [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info
119+
if (b == NO_RESOLVER) {
120+
// 07-Dec-2025, tatu: Should we actually do this? (No test coverage yet)
121+
//return NoOpTypeSerializer.instance();
122+
return null;
123+
}
116124
Collection<NamedType> subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByClass(
117125
config, accessor, baseType);
118126
// 10-Jun-2015, tatu: Since not created for Bean Property, no need for post-processing
@@ -133,6 +141,13 @@ public TypeDeserializer findPropertyTypeDeserializer(DeserializationContext ctxt
133141
if (b == null) {
134142
return findTypeDeserializer(ctxt, baseType, ctxt.introspectClassAnnotations(baseType));
135143
}
144+
// [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info
145+
if (b == NO_RESOLVER) {
146+
// 07-Dec-2025, tatu: Should we actually do this? (No test coverage yet)
147+
//return NoOpTypeDeserializer.forBaseType(ctxt, baseType);
148+
149+
return null;
150+
}
136151
Collection<NamedType> subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByTypeId(config,
137152
accessor, baseType);
138153
// May need to figure out default implementation, if none found yet
@@ -162,6 +177,10 @@ public TypeSerializer findPropertyContentTypeSerializer(SerializationContext ctx
162177
return findTypeSerializer(ctxt, contentType,
163178
ctxt.introspectClassAnnotations(contentType.getRawClass()));
164179
}
180+
// [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info
181+
if (b == NO_RESOLVER) {
182+
return NoOpTypeSerializer.instance();
183+
}
165184
Collection<NamedType> subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByClass(
166185
config, accessor, contentType);
167186
return b.buildTypeSerializer(ctxt, contentType, subtypes);
@@ -181,6 +200,10 @@ public TypeDeserializer findPropertyContentTypeDeserializer(DeserializationConte
181200
if (b == null) {
182201
return findTypeDeserializer(ctxt, contentType, ctxt.introspectClassAnnotations(contentType));
183202
}
203+
// [databind#1654]: Explicit `@JsonTypeInfo(Id.NONE)` should block class-level type info
204+
if (b == NO_RESOLVER) {
205+
return NoOpTypeDeserializer.forBaseType(ctxt, contentType);
206+
}
184207
Collection<NamedType> subtypes = config.getSubtypeResolver().collectAndResolveSubtypesByTypeId(config,
185208
accessor, contentType);
186209
// May need to figure out default implementation, if none found yet
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package tools.jackson.databind.jsontype.impl;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
5+
import tools.jackson.core.*;
6+
import tools.jackson.databind.*;
7+
import tools.jackson.databind.jsontype.TypeDeserializer;
8+
import tools.jackson.databind.jsontype.TypeIdResolver;
9+
import tools.jackson.databind.util.ClassUtil;
10+
11+
/**
12+
* Special {@link TypeDeserializer} implementation used to explicitly
13+
* block type deserialization. This is used when a property or class
14+
* is annotated with {@code @JsonTypeInfo(use = Id.NONE)}, indicating
15+
* that type information should not be expected or processed even if
16+
* the value type has a class-level type info annotation.
17+
*<p>
18+
* Unlike returning {@code null} (which means "no special type handling,
19+
* use defaults"), this actively prevents type information from being read.
20+
*
21+
* @since 3.1
22+
*/
23+
public class NoOpTypeDeserializer extends TypeDeserializer
24+
{
25+
private final JavaType _baseType;
26+
private final BeanProperty _property;
27+
28+
// Dynamically constructed deserializer
29+
private volatile ValueDeserializer<Object> _deserializer;
30+
31+
private NoOpTypeDeserializer(JavaType baseType, BeanProperty prop) {
32+
_baseType = baseType;
33+
_property = prop;
34+
}
35+
36+
public static NoOpTypeDeserializer forBaseType(DeserializationContext ctxt,
37+
JavaType baseType) {
38+
return new NoOpTypeDeserializer(baseType, null);
39+
}
40+
41+
@Override
42+
public TypeDeserializer forProperty(BeanProperty prop) {
43+
if (_property == prop) {
44+
return this;
45+
}
46+
return new NoOpTypeDeserializer(_baseType, prop);
47+
}
48+
49+
@Override
50+
public JsonTypeInfo.As getTypeInclusion() {
51+
// No proper value but need to return something
52+
return JsonTypeInfo.As.EXISTING_PROPERTY;
53+
}
54+
55+
@Override
56+
public String getPropertyName() {
57+
return null;
58+
}
59+
60+
@Override
61+
public TypeIdResolver getTypeIdResolver() {
62+
return null;
63+
}
64+
65+
@Override
66+
public Class<?> getDefaultImpl() {
67+
return null;
68+
}
69+
70+
@Override
71+
public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt)
72+
throws JacksonException
73+
{
74+
return _deserialize(p, ctxt);
75+
}
76+
77+
@Override
78+
public Object deserializeTypedFromArray(JsonParser p, DeserializationContext ctxt)
79+
throws JacksonException
80+
{
81+
return _deserialize(p, ctxt);
82+
}
83+
84+
@Override
85+
public Object deserializeTypedFromScalar(JsonParser p, DeserializationContext ctxt)
86+
throws JacksonException
87+
{
88+
return _deserialize(p, ctxt);
89+
}
90+
91+
@Override
92+
public Object deserializeTypedFromAny(JsonParser p, DeserializationContext ctxt)
93+
throws JacksonException
94+
{
95+
return _deserialize(p, ctxt);
96+
}
97+
98+
protected Object _deserialize(JsonParser p, DeserializationContext ctxt)
99+
throws JacksonException
100+
{
101+
ValueDeserializer<Object> deser = _deserializer;
102+
103+
// Find deserializer for the base type, given property (if any).
104+
// This will find custom deserializers registered for this type,
105+
// including those from @JsonDeserialize annotations)
106+
if (deser == null) {
107+
deser = ctxt.findContextualValueDeserializer(_baseType, _property);
108+
if (deser == null) {
109+
ctxt.reportBadDefinition(_baseType,
110+
"Cannot find deserializer for type " +ClassUtil.getTypeDescription(_baseType));
111+
}
112+
_deserializer = deser;
113+
}
114+
return deser.deserialize(p, ctxt);
115+
}
116+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package tools.jackson.databind.jsontype.impl;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
5+
import tools.jackson.core.*;
6+
import tools.jackson.core.type.WritableTypeId;
7+
import tools.jackson.databind.BeanProperty;
8+
import tools.jackson.databind.SerializationContext;
9+
import tools.jackson.databind.jsontype.TypeIdResolver;
10+
import tools.jackson.databind.jsontype.TypeSerializer;
11+
12+
/**
13+
* Special {@link TypeSerializer} implementation used to explicitly
14+
* block type serialization. This is used when a property or class
15+
* is annotated with {@code @JsonTypeInfo(use = Id.NONE)}, indicating
16+
* that type information should not be included even if the value type
17+
* has a class-level type info annotation.
18+
*<p>
19+
* Unlike returning {@code null} (which means "no special type handling,
20+
* use defaults"), this actively prevents type information from being written.
21+
*
22+
* @since 3.1
23+
*/
24+
public class NoOpTypeSerializer extends TypeSerializer
25+
{
26+
private static final NoOpTypeSerializer INSTANCE = new NoOpTypeSerializer();
27+
28+
private NoOpTypeSerializer() { }
29+
30+
public static NoOpTypeSerializer instance() {
31+
return INSTANCE;
32+
}
33+
34+
@Override
35+
public TypeSerializer forProperty(SerializationContext ctxt, BeanProperty prop) {
36+
return this;
37+
}
38+
39+
@Override
40+
public JsonTypeInfo.As getTypeInclusion() {
41+
// No proper one to use but must return something:
42+
return JsonTypeInfo.As.EXISTING_PROPERTY;
43+
}
44+
45+
@Override
46+
public String getPropertyName() {
47+
return null;
48+
}
49+
50+
@Override
51+
public TypeIdResolver getTypeIdResolver() {
52+
return null;
53+
}
54+
55+
@Override
56+
public WritableTypeId writeTypePrefix(JsonGenerator g,
57+
SerializationContext ctxt, WritableTypeId typeId)
58+
throws JacksonException
59+
{
60+
// Write the value start token if needed, but NO type information
61+
if (typeId.valueShape == JsonToken.START_OBJECT) {
62+
g.writeStartObject(typeId.forValue);
63+
} else if (typeId.valueShape == JsonToken.START_ARRAY) {
64+
g.writeStartArray();
65+
}
66+
// 1. Start marker (part of value) was written but
67+
// 2. No value wrapper was written.
68+
typeId.wrapperWritten = false;
69+
return typeId;
70+
}
71+
72+
@Override
73+
public WritableTypeId writeTypeSuffix(JsonGenerator g,
74+
SerializationContext ctxt, WritableTypeId typeId)
75+
throws JacksonException
76+
{
77+
// Write the value end token if needed, but no wrapper to close
78+
if (typeId.valueShape == JsonToken.START_OBJECT) {
79+
g.writeEndObject();
80+
} else if (typeId.valueShape == JsonToken.START_ARRAY) {
81+
g.writeEndArray();
82+
}
83+
return typeId;
84+
}
85+
}

src/test/java/tools/jackson/databind/tofix/NoTypeInfo1654Test.java renamed to src/test/java/tools/jackson/databind/jsontype/NoTypeInfo1654Test.java

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tools.jackson.databind.tofix;
1+
package tools.jackson.databind.jsontype;
22

33
import java.util.*;
44

@@ -10,18 +10,16 @@
1010
import tools.jackson.databind.*;
1111
import tools.jackson.databind.annotation.JsonDeserialize;
1212
import tools.jackson.databind.testutil.DatabindTestUtil;
13-
import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected;
1413

1514
import static org.junit.jupiter.api.Assertions.assertEquals;
1615

17-
class NoTypeInfo1654Test extends DatabindTestUtil {
18-
16+
class NoTypeInfo1654Test extends DatabindTestUtil
17+
{
1918
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
2019
static class Value1654 {
2120
public int x;
2221

23-
protected Value1654() {
24-
}
22+
protected Value1654() { }
2523

2624
public Value1654(int x) {
2725
this.x = x;
@@ -31,16 +29,15 @@ public Value1654(int x) {
3129
static class Value1654TypedContainer {
3230
public List<Value1654> values;
3331

34-
protected Value1654TypedContainer() {
35-
}
32+
protected Value1654TypedContainer() { }
3633

3734
public Value1654TypedContainer(Value1654... v) {
3835
values = Arrays.asList(v);
3936
}
4037
}
4138

4239
static class Value1654UntypedContainer {
43-
@JsonDeserialize(contentUsing = Value1654Deserializer.class)
40+
//@JsonDeserialize(contentUsing = Value1654Deserializer.class)
4441
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
4542
public List<Value1654> values;
4643

@@ -52,34 +49,39 @@ public Value1654UntypedContainer(Value1654... v) {
5249
}
5350
}
5451

52+
/*
5553
static class Value1654Deserializer extends ValueDeserializer<Value1654> {
5654
@Override
5755
public Value1654 deserialize(JsonParser p, DeserializationContext ctxt) {
56+
//JsonNode n = ctxt.readTree(p);
5857
p.skipChildren();
5958
return new Value1654(13);
6059
}
6160
}
61+
*/
6262

6363
private final ObjectMapper MAPPER = newJsonMapper();
6464

65-
// [databind#1654]
65+
// [databind#1654]: no override, default polymorphic type id
6666
@Test
67-
void noTypeElementOverride() throws Exception {
68-
// egular typed case
67+
void withoutNoTypeElementOverrideSerAndDeser() throws Exception {
68+
// regular typed case
6969
String json = MAPPER.writeValueAsString(new Value1654TypedContainer(
7070
new Value1654(1),
71-
new Value1654(2),
72-
new Value1654(3)
71+
new Value1654(2)
7372
));
73+
String typeId = Value1654.class.getName();
74+
typeId = "'@type':'" + typeId.substring(typeId.lastIndexOf('.') + 1) + "'";
75+
assertEquals(a2q("{'values':[{"+typeId+",'x':1},{"+typeId+",'x':2}]}"), json);
76+
7477
Value1654TypedContainer result = MAPPER.readValue(json, Value1654TypedContainer.class);
75-
assertEquals(3, result.values.size());
78+
assertEquals(2, result.values.size());
7679
assertEquals(2, result.values.get(1).x);
7780
}
7881

79-
// [databind#1654]
80-
@JacksonTestFailureExpected
82+
// [databind#1654]: override, no polymorphic type id
8183
@Test
82-
void noTypeInfoOverrideSer() throws Exception {
84+
void withNoTypeInfoOverrideSer() throws Exception {
8385
Value1654UntypedContainer cont = new Value1654UntypedContainer(
8486
new Value1654(3),
8587
new Value1654(7)
@@ -89,9 +91,8 @@ void noTypeInfoOverrideSer() throws Exception {
8991
}
9092

9193
// [databind#1654]
92-
@JacksonTestFailureExpected
9394
@Test
94-
void noTypeInfoOverrideDeser() throws Exception {
95+
void withNoTypeInfoOverrideDeser() throws Exception {
9596
// and then actual failing case
9697
final String noTypeJson = a2q(
9798
"{'values':[{'x':3},{'x':7}]}"

0 commit comments

Comments
 (0)