diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index e8a8373301..57039d31b9 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -1200,6 +1200,7 @@ Marc Carter (drekbour@github)
(2.12.0)
* Contributed #3055: Polymorphic subtype deduction ignores `defaultImpl` attribute
(2.12.2)
+ * Contributed #3139: Deserialization of "empty" subtype with DEDUCTION failed
Mike Gilbode (gilbode@github)
* Reported #792: Deserialization Not Working Right with Generic Types and Builders
@@ -1313,3 +1314,7 @@ Miguel G (Migwel@github)
Jelle Voost (jellevoost@github)
* Reported #3038: Two cases of incorrect error reporting about DeserializationFeature
(2.12.2)
+
+JoeWoo (xJoeWoo@github)
+ * Reported #3139: Deserialization of "empty" subtype with DEDUCTION failed
+ (2.12.4)
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 9b0f2f62a6..e93d8426db 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -4,6 +4,11 @@ Project: jackson-databind
=== Releases ===
------------------------------------------------------------------------
+2.12.4 (not yet released)
+
+#3139: Deserialization of "empty" subtype with DEDUCTION failed
+ (reported by JoeWoo; fix provided by drekbour@github)
+
2.12.3 (12-Apr-2021)
#3108: `TypeFactory` cannot convert `Collection` sub-type without type parameters
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java
index ad28b4e898..112e379ea3 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java
@@ -25,10 +25,14 @@
* the absence of child fields infers a parent type. That is, every deducible subtype
* MUST have some unique fields and the input data MUST contain said unique fields
* to provide a positive match.
+ *
+ * @since 2.12
*/
public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer
{
private static final long serialVersionUID = 1L;
+
+ // 03-May-2021, tatu: for [databind#3139], support for "empty" type
private static final BitSet EMPTY_CLASS_FINGERPRINT = new BitSet(0);
// Fieldname -> bitmap-index of every field discovered, across all subtypes
@@ -106,16 +110,22 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
return _deserializeTypedUsingDefaultImpl(p, ctxt, null, "Unexpected input");
}
+ // 03-May-2021, tatu: [databind#3139] Special case, "empty" Object
+ if (t == JsonToken.END_OBJECT) {
+ String emptySubtype = subtypeFingerprints.get(EMPTY_CLASS_FINGERPRINT);
+ if (emptySubtype != null) { // ... and an "empty" subtype registered
+ return _deserializeTypedForId(p, ctxt, null, emptySubtype);
+ }
+ }
+
List candidates = new LinkedList<>(subtypeFingerprints.keySet());
// Record processed tokens as we must rewind once after deducing the deserializer to use
@SuppressWarnings("resource")
TokenBuffer tb = new TokenBuffer(p, ctxt);
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
- boolean incomingIsEmpty = true;
for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
- incomingIsEmpty = false; // Has at least one property
String name = p.currentName();
if (ignoreCase) name = name.toLowerCase();
@@ -131,13 +141,6 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
}
}
- if (incomingIsEmpty) { // Special case - if we have empty content ...
- String emptySubtype = subtypeFingerprints.get(EMPTY_CLASS_FINGERPRINT);
- if (emptySubtype != null) { // ... and an "empty" subtype registered
- return _deserializeTypedForId(p, ctxt, null, emptySubtype);
- }
- }
-
// We have zero or multiple candidates, deduction has failed
String msgToReportIfDefaultImplFailsToo = String.format("Cannot deduce unique subtype of %s (%d candidates match)", ClassUtil.getTypeDescription(_baseType), candidates.size());
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, msgToReportIfDefaultImplFailsToo);
diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java
index 3e4d8aeeeb..799f7754df 100644
--- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java
+++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java
@@ -60,18 +60,18 @@ static class Box {
/**********************************************************
*/
- private static final String deadCatJson = aposToQuotes("{'name':'Felix','causeOfDeath':'entropy'}");
- private static final String liveCatJson = aposToQuotes("{'name':'Felix','angry':true}");
- private static final String luckyCatJson = aposToQuotes("{'name':'Felix','angry':true,'lives':8}");
- private static final String ambiguousCatJson = aposToQuotes("{'name':'Felix','age':2}");
- private static final String fleabagJson = aposToQuotes("{}");
- private static final String box1Json = aposToQuotes("{'feline':" + liveCatJson + "}");
- private static final String box2Json = aposToQuotes("{'feline':" + deadCatJson + "}");
- private static final String box3Json = aposToQuotes("{'feline':" + fleabagJson + "}");
- private static final String box4Json = aposToQuotes("{'feline':null}");
- private static final String box5Json = aposToQuotes("{}");
- private static final String arrayOfCatsJson = aposToQuotes("[" + liveCatJson + "," + deadCatJson + "]");
- private static final String mapOfCatsJson = aposToQuotes("{'live':" + liveCatJson + "}");
+ private static final String deadCatJson = a2q("{'name':'Felix','causeOfDeath':'entropy'}");
+ private static final String liveCatJson = a2q("{'name':'Felix','angry':true}");
+ private static final String luckyCatJson = a2q("{'name':'Felix','angry':true,'lives':8}");
+ private static final String ambiguousCatJson = a2q("{'name':'Felix','age':2}");
+ private static final String fleabagJson = a2q("{}");
+ private static final String box1Json = a2q("{'feline':" + liveCatJson + "}");
+ private static final String box2Json = a2q("{'feline':" + deadCatJson + "}");
+ private static final String box3Json = a2q("{'feline':" + fleabagJson + "}");
+ private static final String box4Json = a2q("{'feline':null}");
+ private static final String box5Json = a2q("{}");
+ private static final String arrayOfCatsJson = a2q("[" + liveCatJson + "," + deadCatJson + "]");
+ private static final String mapOfCatsJson = a2q("{'live':" + liveCatJson + "}");
/*
/**********************************************************
@@ -79,14 +79,16 @@ static class Box {
/**********************************************************
*/
+ private final ObjectMapper MAPPER = newJsonMapper();
+
public void testSimpleInference() throws Exception {
- Cat cat = sharedMapper().readValue(liveCatJson, Cat.class);
+ Cat cat = MAPPER.readValue(liveCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
assertSame(cat.getClass(), LiveCat.class);
assertEquals("Felix", cat.name);
assertTrue(((LiveCat)cat).angry);
- cat = sharedMapper().readValue(deadCatJson, Cat.class);
+ cat = MAPPER.readValue(deadCatJson, Cat.class);
assertTrue(cat instanceof DeadCat);
assertSame(cat.getClass(), DeadCat.class);
assertEquals("Felix", cat.name);
@@ -95,7 +97,7 @@ public void testSimpleInference() throws Exception {
public void testSimpleInferenceOfEmptySubtype() throws Exception {
// Given:
- ObjectMapper mapper = sharedMapper();
+ ObjectMapper mapper = MAPPER;
// When:
Feline feline = mapper.readValue(fleabagJson, Feline.class);
// Then:
@@ -104,7 +106,7 @@ public void testSimpleInferenceOfEmptySubtype() throws Exception {
public void testSimpleInferenceOfEmptySubtypeDoesntMatchNull() throws Exception {
// Given:
- ObjectMapper mapper = sharedMapper();
+ ObjectMapper mapper = MAPPER;
// When:
Feline feline = mapper.readValue("null", Feline.class);
// Then:
@@ -136,13 +138,13 @@ public void testCaseInsensitiveInference() throws Exception {
// }
public void testContainedInference() throws Exception {
- Box box = sharedMapper().readValue(box1Json, Box.class);
+ Box box = MAPPER.readValue(box1Json, Box.class);
assertTrue(box.feline instanceof LiveCat);
assertSame(box.feline.getClass(), LiveCat.class);
assertEquals("Felix", ((LiveCat)box.feline).name);
assertTrue(((LiveCat)box.feline).angry);
- box = sharedMapper().readValue(box2Json, Box.class);
+ box = MAPPER.readValue(box2Json, Box.class);
assertTrue(box.feline instanceof DeadCat);
assertSame(box.feline.getClass(), DeadCat.class);
assertEquals("Felix", ((DeadCat)box.feline).name);
@@ -150,38 +152,38 @@ public void testContainedInference() throws Exception {
}
public void testContainedInferenceOfEmptySubtype() throws Exception {
- Box box = sharedMapper().readValue(box3Json, Box.class);
+ Box box = MAPPER.readValue(box3Json, Box.class);
assertTrue(box.feline instanceof Fleabag);
- box = sharedMapper().readValue(box4Json, Box.class);
+ box = MAPPER.readValue(box4Json, Box.class);
assertNull("null != {}", box.feline);
- box = sharedMapper().readValue(box5Json, Box.class);
+ box = MAPPER.readValue(box5Json, Box.class);
assertNull(" != {}", box.feline);
}
public void testListInference() throws Exception {
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);
- List boxes = sharedMapper().readValue(arrayOfCatsJson, listOfCats);
+ List boxes = MAPPER.readValue(arrayOfCatsJson, listOfCats);
assertTrue(boxes.get(0) instanceof LiveCat);
assertTrue(boxes.get(1) instanceof DeadCat);
}
public void testMapInference() throws Exception {
JavaType mapOfCats = TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, Cat.class);
- Map map = sharedMapper().readValue(mapOfCatsJson, mapOfCats);
+ Map map = MAPPER.readValue(mapOfCatsJson, mapOfCats);
assertEquals(1, map.size());
assertTrue(map.entrySet().iterator().next().getValue() instanceof LiveCat);
}
public void testArrayInference() throws Exception {
- Cat[] boxes = sharedMapper().readValue(arrayOfCatsJson, Cat[].class);
+ Cat[] boxes = MAPPER.readValue(arrayOfCatsJson, Cat[].class);
assertTrue(boxes[0] instanceof LiveCat);
assertTrue(boxes[1] instanceof DeadCat);
}
public void testIgnoreProperties() throws Exception {
- Cat cat = sharedMapper().reader()
+ Cat cat = MAPPER.reader()
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.readValue(luckyCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
@@ -210,7 +212,7 @@ public void testAmbiguousClasses() throws Exception {
public void testAmbiguousProperties() throws Exception {
try {
- /*Cat cat =*/ sharedMapper().readValue(ambiguousCatJson, Cat.class);
+ /*Cat cat =*/ MAPPER.readValue(ambiguousCatJson, Cat.class);
fail("Should not get here");
} catch (InvalidTypeIdException e) {
verifyException(e, "Cannot deduce unique subtype");
@@ -250,10 +252,10 @@ public void testDefaultImpl() throws Exception {
public void testSimpleSerialization() throws Exception {
// Given:
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);
- List list = sharedMapper().readValue(arrayOfCatsJson, listOfCats);
+ List list = MAPPER.readValue(arrayOfCatsJson, listOfCats);
Cat cat = list.get(0);
// When:
- String json = sharedMapper().writeValueAsString(cat);
+ String json = MAPPER.writeValueAsString(cat);
// Then:
assertEquals(liveCatJson, json);
}
@@ -261,9 +263,9 @@ public void testSimpleSerialization() throws Exception {
public void testListSerialization() throws Exception {
// Given:
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);
- List list = sharedMapper().readValue(arrayOfCatsJson, listOfCats);
+ List list = MAPPER.readValue(arrayOfCatsJson, listOfCats);
// When:
- String json = sharedMapper().writeValueAsString(list);
+ String json = MAPPER.writeValueAsString(list);
// Then:
assertEquals(arrayOfCatsJson, json);
}