Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DEDUCTION of empty subtypes #3140

Merged
merged 1 commit into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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