Skip to content

Commit 4cf1a12

Browse files
authored
Support defaultImpl and FAIL_ON_INVALID_SUBTYPE features when using subtype deduction (#3057)
1 parent eba7b84 commit 4cf1a12

File tree

5 files changed

+54
-49
lines changed

5 files changed

+54
-49
lines changed

src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1938,7 +1938,7 @@ public JsonMappingException invalidTypeIdException(JavaType baseType, String typ
19381938
*/
19391939
public JsonMappingException missingTypeIdException(JavaType baseType,
19401940
String extraDesc) {
1941-
String msg = String.format("Missing type id when trying to resolve subtype of %s",
1941+
String msg = String.format("Could not resolve subtype of %s",
19421942
baseType);
19431943
return InvalidTypeIdException.from(_parser, _colonConcat(msg, extraDesc), baseType, null);
19441944
}

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java

+11-18
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
import java.io.IOException;
44
import java.util.*;
55

6-
import com.fasterxml.jackson.annotation.JsonTypeInfo;
7-
86
import com.fasterxml.jackson.core.JsonParser;
97
import com.fasterxml.jackson.core.JsonToken;
108

119
import com.fasterxml.jackson.databind.*;
12-
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
1310
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
1411
import com.fasterxml.jackson.databind.jsontype.NamedType;
1512
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
@@ -21,6 +18,11 @@
2118
* A {@link TypeDeserializer} capable of deducing polymorphic types based on the fields available. Deduction
2219
* is limited to the <i>names</i> of child fields (not their values or, consequently, any nested descendants).
2320
* Exceptions will be thrown if not enough unique information is present to select a single subtype.
21+
* <p>
22+
* The current deduction process <b>does not</b> support pojo-hierarchies such that the
23+
* absence of child fields infers a parent type. That is, every deducible subtype
24+
* MUST have some unique fields and the input data MUST contain said unique fields
25+
* to provide a <i>positive match</i>.
2426
*/
2527
public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer
2628
{
@@ -32,7 +34,7 @@ public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer
3234
private final Map<BitSet, String> subtypeFingerprints;
3335

3436
public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl, DeserializationConfig config, Collection<NamedType> subtypes) {
35-
super(bt, idRes, null, false, defaultImpl);
37+
super(bt, idRes, null, false, defaultImpl, null);
3638
fieldBitIndex = new HashMap<>();
3739
subtypeFingerprints = buildFingerprints(config, subtypes);
3840
}
@@ -43,11 +45,6 @@ public AsDeductionTypeDeserializer(AsDeductionTypeDeserializer src, BeanProperty
4345
subtypeFingerprints = src.subtypeFingerprints;
4446
}
4547

46-
@Override
47-
public JsonTypeInfo.As getTypeInclusion() {
48-
return null;
49-
}
50-
5148
@Override
5249
public TypeDeserializer forProperty(BeanProperty prop) {
5350
return (prop == _property) ? this : new AsDeductionTypeDeserializer(this, prop);
@@ -93,15 +90,15 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
9390
JsonToken t = p.currentToken();
9491
if (t == JsonToken.START_OBJECT) {
9592
t = p.nextToken();
96-
} else {
93+
} else if (/*t == JsonToken.START_ARRAY ||*/ t != JsonToken.FIELD_NAME) {
9794
/* This is most likely due to the fact that not all Java types are
9895
* serialized as JSON Objects; so if "as-property" inclusion is requested,
9996
* serialization of things like Lists must be instead handled as if
10097
* "as-wrapper-array" was requested.
10198
* But this can also be due to some custom handling: so, if "defaultImpl"
10299
* is defined, it will be asked to handle this case.
103100
*/
104-
return _deserializeTypedUsingDefaultImpl(p, ctxt, null);
101+
return _deserializeTypedUsingDefaultImpl(p, ctxt, null, "Unexpected input");
105102
}
106103

107104
List<BitSet> candidates = new LinkedList<>(subtypeFingerprints.keySet());
@@ -127,13 +124,9 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
127124
}
128125
}
129126

130-
throw new InvalidTypeIdException(p,
131-
String.format("Cannot deduce unique subtype of %s (%d candidates match)",
132-
ClassUtil.getTypeDescription(_baseType),
133-
candidates.size()),
134-
_baseType
135-
, "DEDUCED"
136-
);
127+
// We have zero or multiple candidates, deduction has failed
128+
String msgToReportIfDefaultImplFailsToo = String.format("Cannot deduce unique subtype of %s (%d candidates match)", ClassUtil.getTypeDescription(_baseType), candidates.size());
129+
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, msgToReportIfDefaultImplFailsToo);
137130
}
138131

139132
// Keep only fingerprints containing this field

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java

+8-10
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public class AsPropertyTypeDeserializer extends AsArrayTypeDeserializer
3131

3232
protected final As _inclusion;
3333

34+
protected final String msgMissingId = _property == null ?
35+
String.format("missing type id property '%s'", _typePropertyName) :
36+
String.format("missing type id property '%s' (for POJO property '%s')", _typePropertyName, _property.getName());
37+
3438
/**
3539
* @since 2.8
3640
*/
@@ -91,7 +95,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
9195
* But this can also be due to some custom handling: so, if "defaultImpl"
9296
* is defined, it will be asked to handle this case.
9397
*/
94-
return _deserializeTypedUsingDefaultImpl(p, ctxt, null);
98+
return _deserializeTypedUsingDefaultImpl(p, ctxt, null, msgMissingId);
9599
}
96100
// Ok, let's try to find the property. But first, need token buffer...
97101
TokenBuffer tb = null;
@@ -110,7 +114,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
110114
tb.writeFieldName(name);
111115
tb.copyCurrentStructure(p);
112116
}
113-
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb);
117+
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, msgMissingId);
114118
}
115119

116120
protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctxt,
@@ -137,7 +141,7 @@ protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctx
137141

138142
// off-lined to keep main method lean and mean...
139143
protected Object _deserializeTypedUsingDefaultImpl(JsonParser p,
140-
DeserializationContext ctxt, TokenBuffer tb) throws IOException
144+
DeserializationContext ctxt, TokenBuffer tb, String priorFailureMsg) throws IOException
141145
{
142146
// May have default implementation to use
143147
// 13-Oct-2020, tatu: As per [databind#2775], need to be careful to
@@ -165,13 +169,7 @@ protected Object _deserializeTypedUsingDefaultImpl(JsonParser p,
165169
// genuine, or faked for "dont fail on bad type id")
166170
JsonDeserializer<Object> deser = _findDefaultImplDeserializer(ctxt);
167171
if (deser == null) {
168-
String msg = String.format("missing type id property '%s'",
169-
_typePropertyName);
170-
// even better, may know POJO property polymorphic value would be assigned to
171-
if (_property != null) {
172-
msg = String.format("%s (for POJO property '%s')", msg, _property.getName());
173-
}
174-
JavaType t = _handleMissingTypeId(ctxt, msg);
172+
JavaType t = _handleMissingTypeId(ctxt, priorFailureMsg);
175173
if (t == null) {
176174
// 09-Mar-2017, tatu: Is this the right thing to do?
177175
return null;

src/test/java/com/fasterxml/jackson/databind/jsontype/JsonTypeInfoCaseInsensitive1983Test.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void testReadMixedCasePropertyName() throws Exception
5454
MAPPER.readValue(serialised, Filter.class);
5555
fail("Should not pass");
5656
} catch (InvalidTypeIdException e) {
57-
verifyException(e, "Missing type id when trying to resolve subtype");
57+
verifyException(e, "missing type id property");
5858
}
5959

6060
ObjectMapper mapper = jsonMapperBuilder()

src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java

+33-19
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,16 @@ public class TestPolymorphicDeduction extends BaseMapTest {
2222

2323
@JsonTypeInfo(use = DEDUCTION)
2424
@JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class)})
25-
static abstract class Cat {
26-
public final String name;
27-
28-
protected Cat(String name) {
29-
this.name = name;
30-
}
25+
public static class Cat {
26+
public String name;
3127
}
3228

3329
static class DeadCat extends Cat {
3430
public String causeOfDeath;
35-
36-
DeadCat(@JsonProperty("name") String name) {
37-
super(name);
38-
}
3931
}
4032

4133
static class LiveCat extends Cat {
4234
public boolean angry;
43-
44-
LiveCat(@JsonProperty("name") String name) {
45-
super(name);
46-
}
4735
}
4836

4937
static class Box {
@@ -155,10 +143,6 @@ public void testIgnoreProperties() throws Exception {
155143

156144
static class AnotherLiveCat extends Cat {
157145
public boolean angry;
158-
159-
AnotherLiveCat(@JsonProperty("name") String name) {
160-
super(name);
161-
}
162146
}
163147

164148
public void testAmbiguousClasses() throws Exception {
@@ -180,10 +164,40 @@ public void testAmbiguousProperties() throws Exception {
180164
/*Cat cat =*/ sharedMapper().readValue(ambiguousCatJson, Cat.class);
181165
fail("Should not get here");
182166
} catch (InvalidTypeIdException e) {
183-
verifyException(e, "Cannot deduce unique subtype of");
167+
verifyException(e, "Cannot deduce unique subtype");
184168
}
185169
}
186170

171+
public void testFailOnInvalidSubtype() throws Exception {
172+
// Given:
173+
JsonMapper mapper = JsonMapper.builder() // Don't use shared mapper!
174+
.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
175+
.build();
176+
// When:
177+
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
178+
// Then:
179+
assertNull(cat);
180+
}
181+
182+
@JsonTypeInfo(use = DEDUCTION, defaultImpl = Cat.class)
183+
abstract static class CatMixin {
184+
}
185+
186+
public void testDefaultImpl() throws Exception {
187+
// Given:
188+
JsonMapper mapper = JsonMapper.builder() // Don't use shared mapper!
189+
.addMixIn(Cat.class, CatMixin.class)
190+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
191+
.build();
192+
// When:
193+
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
194+
// Then:
195+
// Even though "age":2 implies this was a failed subtype, we are instructed to fallback to Cat regardless.
196+
assertTrue(cat instanceof Cat);
197+
assertSame(Cat.class, cat.getClass());
198+
assertEquals("Felix", cat.name);
199+
}
200+
187201
public void testSimpleSerialization() throws Exception {
188202
// Given:
189203
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);

0 commit comments

Comments
 (0)