Skip to content

Commit 4231bb1

Browse files
authored
Support DEDUCTION of empty subtypes (#3140)
1 parent 6bf910c commit 4231bb1

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeDeserializer.java

+10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer
3030
{
3131
private static final long serialVersionUID = 1L;
32+
private static final BitSet EMPTY_CLASS_FINGERPRINT = new BitSet(0);
3233

3334
// Fieldname -> bitmap-index of every field discovered, across all subtypes
3435
private final Map<String, Integer> fieldBitIndex;
@@ -111,8 +112,10 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
111112
@SuppressWarnings("resource")
112113
TokenBuffer tb = new TokenBuffer(p, ctxt);
113114
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
115+
boolean incomingIsEmpty = true;
114116

115117
for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
118+
incomingIsEmpty = false; // Has at least one property
116119
String name = p.currentName();
117120
if (ignoreCase) name = name.toLowerCase();
118121

@@ -128,6 +131,13 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
128131
}
129132
}
130133

134+
if (incomingIsEmpty) { // Special case - if we have empty content ...
135+
String emptySubtype = subtypeFingerprints.get(EMPTY_CLASS_FINGERPRINT);
136+
if (emptySubtype != null) { // ... and an "empty" subtype registered
137+
return _deserializeTypedForId(p, ctxt, null, emptySubtype);
138+
}
139+
}
140+
131141
// We have zero or multiple candidates, deduction has failed
132142
String msgToReportIfDefaultImplFailsToo = String.format("Cannot deduce unique subtype of %s (%d candidates match)", ClassUtil.getTypeDescription(_baseType), candidates.size());
133143
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, msgToReportIfDefaultImplFailsToo);

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,10 @@ protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctx
131131
p.clearCurrentToken();
132132
p = JsonParserSequence.createFlattened(false, tb.asParser(p), p);
133133
}
134-
// Must point to the next value; tb had no current, jp pointed to VALUE_STRING:
135-
p.nextToken(); // to skip past String value
134+
if (p.currentToken() != JsonToken.END_OBJECT) {
135+
// Must point to the next value; tb had no current, p pointed to VALUE_STRING:
136+
p.nextToken(); // to skip past String value
137+
}
136138
// deserializer should take care of closing END_OBJECT as well
137139
return deser.deserialize(p, ctxt);
138140
}

src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java

+59-12
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,36 @@
2222
// for [databind#43], deduction-based polymorphism
2323
public class TestPolymorphicDeduction extends BaseMapTest {
2424

25+
@JsonTypeInfo(use = DEDUCTION)
26+
@JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class), @Type(Fleabag.class)})
27+
// A general supertype with no properties - used for tests involving {}
28+
interface Feline {}
29+
2530
@JsonTypeInfo(use = DEDUCTION)
2631
@JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class)})
27-
public static class Cat {
32+
// A supertype containing common properties
33+
public static class Cat implements Feline {
2834
public String name;
2935
}
3036

37+
// Distinguished by its parent and a unique property
3138
static class DeadCat extends Cat {
3239
public String causeOfDeath;
3340
}
3441

42+
// Distinguished by its parent and a unique property
3543
static class LiveCat extends Cat {
3644
public boolean angry;
3745
}
3846

47+
// No distinguishing properties whatsoever
48+
static class Fleabag implements Feline {
49+
// NO OP
50+
}
51+
52+
// Something to put felines in
3953
static class Box {
40-
public Cat cat;
54+
public Feline feline;
4155
}
4256

4357
/*
@@ -50,8 +64,12 @@ static class Box {
5064
private static final String liveCatJson = aposToQuotes("{'name':'Felix','angry':true}");
5165
private static final String luckyCatJson = aposToQuotes("{'name':'Felix','angry':true,'lives':8}");
5266
private static final String ambiguousCatJson = aposToQuotes("{'name':'Felix','age':2}");
53-
private static final String box1Json = aposToQuotes("{'cat':" + liveCatJson + "}");
54-
private static final String box2Json = aposToQuotes("{'cat':" + deadCatJson + "}");
67+
private static final String fleabagJson = aposToQuotes("{}");
68+
private static final String box1Json = aposToQuotes("{'feline':" + liveCatJson + "}");
69+
private static final String box2Json = aposToQuotes("{'feline':" + deadCatJson + "}");
70+
private static final String box3Json = aposToQuotes("{'feline':" + fleabagJson + "}");
71+
private static final String box4Json = aposToQuotes("{'feline':null}");
72+
private static final String box5Json = aposToQuotes("{}");
5573
private static final String arrayOfCatsJson = aposToQuotes("[" + liveCatJson + "," + deadCatJson + "]");
5674
private static final String mapOfCatsJson = aposToQuotes("{'live':" + liveCatJson + "}");
5775

@@ -75,6 +93,24 @@ public void testSimpleInference() throws Exception {
7593
assertEquals("entropy", ((DeadCat)cat).causeOfDeath);
7694
}
7795

96+
public void testSimpleInferenceOfEmptySubtype() throws Exception {
97+
// Given:
98+
ObjectMapper mapper = sharedMapper();
99+
// When:
100+
Feline feline = mapper.readValue(fleabagJson, Feline.class);
101+
// Then:
102+
assertTrue(feline instanceof Fleabag);
103+
}
104+
105+
public void testSimpleInferenceOfEmptySubtypeDoesntMatchNull() throws Exception {
106+
// Given:
107+
ObjectMapper mapper = sharedMapper();
108+
// When:
109+
Feline feline = mapper.readValue("null", Feline.class);
110+
// Then:
111+
assertNull(feline);
112+
}
113+
78114
public void testCaseInsensitiveInference() throws Exception {
79115
Cat cat = JsonMapper.builder() // Don't use shared mapper!
80116
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
@@ -101,16 +137,27 @@ public void testCaseInsensitiveInference() throws Exception {
101137

102138
public void testContainedInference() throws Exception {
103139
Box box = sharedMapper().readValue(box1Json, Box.class);
104-
assertTrue(box.cat instanceof LiveCat);
105-
assertSame(box.cat.getClass(), LiveCat.class);
106-
assertEquals("Felix", box.cat.name);
107-
assertTrue(((LiveCat)box.cat).angry);
140+
assertTrue(box.feline instanceof LiveCat);
141+
assertSame(box.feline.getClass(), LiveCat.class);
142+
assertEquals("Felix", ((LiveCat)box.feline).name);
143+
assertTrue(((LiveCat)box.feline).angry);
108144

109145
box = sharedMapper().readValue(box2Json, Box.class);
110-
assertTrue(box.cat instanceof DeadCat);
111-
assertSame(box.cat.getClass(), DeadCat.class);
112-
assertEquals("Felix", box.cat.name);
113-
assertEquals("entropy", ((DeadCat)box.cat).causeOfDeath);
146+
assertTrue(box.feline instanceof DeadCat);
147+
assertSame(box.feline.getClass(), DeadCat.class);
148+
assertEquals("Felix", ((DeadCat)box.feline).name);
149+
assertEquals("entropy", ((DeadCat)box.feline).causeOfDeath);
150+
}
151+
152+
public void testContainedInferenceOfEmptySubtype() throws Exception {
153+
Box box = sharedMapper().readValue(box3Json, Box.class);
154+
assertTrue(box.feline instanceof Fleabag);
155+
156+
box = sharedMapper().readValue(box4Json, Box.class);
157+
assertNull("null != {}", box.feline);
158+
159+
box = sharedMapper().readValue(box5Json, Box.class);
160+
assertNull("<absent> != {}", box.feline);
114161
}
115162

116163
public void testListInference() throws Exception {

0 commit comments

Comments
 (0)