Skip to content

Commit

Permalink
Support DEDUCTION of empty subtypes (#3140)
Browse files Browse the repository at this point in the history
  • Loading branch information
drekbour authored May 3, 2021
1 parent 6bf910c commit 4231bb1
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Integer> fieldBitIndex;
Expand Down Expand Up @@ -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();

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/*
Expand All @@ -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 + "}");

Expand All @@ -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)
Expand All @@ -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("<absent> != {}", box.feline);
}

public void testListInference() throws Exception {
Expand Down

0 comments on commit 4231bb1

Please sign in to comment.