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); }