From a83b947843bd4b2b769c6e37638ac7039c1b7a47 Mon Sep 17 00:00:00 2001 From: Marc Carter Date: Mon, 3 May 2021 10:13:17 +0100 Subject: [PATCH] Support DEDUCTION of empty subtypes --- .../impl/AsDeductionTypeDeserializer.java | 10 +++ .../impl/AsPropertyTypeDeserializer.java | 6 +- .../jsontype/TestPolymorphicDeduction.java | 71 +++++++++++++++---- 3 files changed, 73 insertions(+), 14 deletions(-) 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 70058949a8..ad28b4e898 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 @@ -29,6 +29,7 @@ public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer { private static final long serialVersionUID = 1L; + private static final BitSet EMPTY_CLASS_FINGERPRINT = new BitSet(0); // Fieldname -> bitmap-index of every field discovered, across all subtypes private final Map fieldBitIndex; @@ -111,8 +112,10 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct @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(); @@ -128,6 +131,13 @@ 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/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java index 552f7fc51a..06da84a69c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java @@ -131,8 +131,10 @@ protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctx p.clearCurrentToken(); p = JsonParserSequence.createFlattened(false, tb.asParser(p), p); } - // Must point to the next value; tb had no current, jp pointed to VALUE_STRING: - p.nextToken(); // to skip past String value + if (p.currentToken() != JsonToken.END_OBJECT) { + // Must point to the next value; tb had no current, p pointed to VALUE_STRING: + p.nextToken(); // to skip past String value + } // deserializer should take care of closing END_OBJECT as well return deser.deserialize(p, ctxt); } 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 d147998950..3e4d8aeeeb 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java @@ -22,22 +22,36 @@ // for [databind#43], deduction-based polymorphism public class TestPolymorphicDeduction extends BaseMapTest { + @JsonTypeInfo(use = DEDUCTION) + @JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class), @Type(Fleabag.class)}) + // A general supertype with no properties - used for tests involving {} + interface Feline {} + @JsonTypeInfo(use = DEDUCTION) @JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class)}) - public static class Cat { + // A supertype containing common properties + public static class Cat implements Feline { public String name; } + // Distinguished by its parent and a unique property static class DeadCat extends Cat { public String causeOfDeath; } + // Distinguished by its parent and a unique property static class LiveCat extends Cat { public boolean angry; } + // No distinguishing properties whatsoever + static class Fleabag implements Feline { + // NO OP + } + + // Something to put felines in static class Box { - public Cat cat; + public Feline feline; } /* @@ -50,8 +64,12 @@ static class Box { 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 box1Json = aposToQuotes("{'cat':" + liveCatJson + "}"); - private static final String box2Json = aposToQuotes("{'cat':" + deadCatJson + "}"); + 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 + "}"); @@ -75,6 +93,24 @@ public void testSimpleInference() throws Exception { assertEquals("entropy", ((DeadCat)cat).causeOfDeath); } + public void testSimpleInferenceOfEmptySubtype() throws Exception { + // Given: + ObjectMapper mapper = sharedMapper(); + // When: + Feline feline = mapper.readValue(fleabagJson, Feline.class); + // Then: + assertTrue(feline instanceof Fleabag); + } + + public void testSimpleInferenceOfEmptySubtypeDoesntMatchNull() throws Exception { + // Given: + ObjectMapper mapper = sharedMapper(); + // When: + Feline feline = mapper.readValue("null", Feline.class); + // Then: + assertNull(feline); + } + public void testCaseInsensitiveInference() throws Exception { Cat cat = JsonMapper.builder() // Don't use shared mapper! .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) @@ -101,16 +137,27 @@ public void testCaseInsensitiveInference() throws Exception { public void testContainedInference() throws Exception { Box box = sharedMapper().readValue(box1Json, Box.class); - assertTrue(box.cat instanceof LiveCat); - assertSame(box.cat.getClass(), LiveCat.class); - assertEquals("Felix", box.cat.name); - assertTrue(((LiveCat)box.cat).angry); + 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); - assertTrue(box.cat instanceof DeadCat); - assertSame(box.cat.getClass(), DeadCat.class); - assertEquals("Felix", box.cat.name); - assertEquals("entropy", ((DeadCat)box.cat).causeOfDeath); + assertTrue(box.feline instanceof DeadCat); + assertSame(box.feline.getClass(), DeadCat.class); + assertEquals("Felix", ((DeadCat)box.feline).name); + assertEquals("entropy", ((DeadCat)box.feline).causeOfDeath); + } + + public void testContainedInferenceOfEmptySubtype() throws Exception { + Box box = sharedMapper().readValue(box3Json, Box.class); + assertTrue(box.feline instanceof Fleabag); + + box = sharedMapper().readValue(box4Json, Box.class); + assertNull("null != {}", box.feline); + + box = sharedMapper().readValue(box5Json, Box.class); + assertNull(" != {}", box.feline); } public void testListInference() throws Exception {