Skip to content

Commit d14332c

Browse files
ISSUE_3289 Add support for pojo-hierarchies in deduction deserialization
1 parent c100ed5 commit d14332c

File tree

2 files changed

+132
-3
lines changed

2 files changed

+132
-3
lines changed

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
125125
@SuppressWarnings("resource")
126126
final TokenBuffer tb = ctxt.bufferForInputBuffering(p);
127127
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
128+
BitSet existingFingerprint = new BitSet();
128129

129130
for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
130131
String name = p.currentName();
@@ -134,15 +135,22 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
134135

135136
Integer bit = fieldBitIndex.get(name);
136137
if (bit != null) {
138+
existingFingerprint.set(bit);
137139
// field is known by at least one subtype
138140
prune(candidates, bit);
139141
if (candidates.size() == 1) {
140142
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0)));
141143
}
142144
}
143145
}
146+
for(BitSet candidate: candidates) {
147+
if (existingFingerprint.equals(candidate)){
148+
tb.copyCurrentStructure(p);
149+
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidate));
150+
}
151+
}
144152

145-
// We have zero or multiple candidates, deduction has failed
153+
// We have zero or multiple candidates and none of them fit exactly the existing fingerprint, deduction has failed
146154
String msgToReportIfDefaultImplFailsToo = String.format("Cannot deduce unique subtype of %s (%d candidates match)", ClassUtil.getTypeDescription(_baseType), candidates.size());
147155
return _deserializeTypedUsingDefaultImpl(p, ctxt, tb, msgToReportIfDefaultImplFailsToo);
148156
}

Diff for: src/test/java/com/fasterxml/jackson/databind/jsontype/TestPolymorphicDeduction.java

+123-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.databind.jsontype;
22

3+
import java.util.ArrayList;
34
import java.util.List;
45
import java.util.Map;
56

@@ -13,6 +14,7 @@
1314
import com.fasterxml.jackson.databind.ObjectMapper;
1415
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
1516
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
17+
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
1618
import com.fasterxml.jackson.databind.json.JsonMapper;
1719
import com.fasterxml.jackson.databind.type.TypeFactory;
1820

@@ -210,9 +212,33 @@ public void testAmbiguousClasses() throws Exception {
210212
}
211213
}
212214

215+
public void testUnrecognizedProperties() throws Exception {
216+
try {
217+
/*Cat cat =*/
218+
MAPPER.readValue(ambiguousCatJson, Cat.class);
219+
fail("Unable to map, because there is unknown field 'age'");
220+
} catch (UnrecognizedPropertyException e) {
221+
verifyException(e, "Unrecognized field");
222+
}
223+
}
224+
225+
public void testNotFailOnUnknownProperty() throws Exception {
226+
// Given:
227+
JsonMapper mapper = JsonMapper.builder()
228+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
229+
.build();
230+
// When:
231+
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
232+
// Then:
233+
// unknown proparty 'age' is ignored, and json is deserialized to Cat class
234+
assertTrue(cat instanceof Cat);
235+
assertSame(Cat.class, cat.getClass());
236+
assertEquals("Felix", cat.name);
237+
}
238+
213239
public void testAmbiguousProperties() throws Exception {
214240
try {
215-
/*Cat cat =*/ MAPPER.readValue(ambiguousCatJson, Cat.class);
241+
/*Feline cat =*/ MAPPER.readValue(ambiguousCatJson, Feline.class);
216242
fail("Should not get here");
217243
} catch (InvalidTypeIdException e) {
218244
verifyException(e, "Cannot deduce unique subtype");
@@ -225,7 +251,7 @@ public void testFailOnInvalidSubtype() throws Exception {
225251
.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
226252
.build();
227253
// When:
228-
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
254+
Feline cat = mapper.readValue(ambiguousCatJson, Feline.class);
229255
// Then:
230256
assertNull(cat);
231257
}
@@ -269,4 +295,99 @@ public void testListSerialization() throws Exception {
269295
// Then:
270296
assertEquals(arrayOfCatsJson, json);
271297
}
298+
299+
@JsonTypeInfo(use = DEDUCTION, defaultImpl = ListOfPlaces.class)
300+
@JsonSubTypes( {@Type(ListOfPlaces.class), @Type(CompositePlace.class), @Type(Place.class)})
301+
interface WorthSeeing {}
302+
303+
public static class Place implements WorthSeeing {
304+
public String name;
305+
}
306+
307+
public static class CompositePlace extends Place implements WorthSeeing {
308+
309+
public Map<String, WorthSeeing> places;
310+
}
311+
312+
static class ListOfPlaces extends ArrayList<WorthSeeing> implements WorthSeeing {
313+
}
314+
315+
private static final String colosseumJson = a2q("{'name': 'The Colosseum'}");
316+
private static final String romanForumJson = a2q("{'name': 'The Roman Forum'}");
317+
private static final String romeJson = a2q("{'name': 'Rome', 'places': {'colosseum': " + colosseumJson + ","+ "'romanForum': "+ romanForumJson +"}}");
318+
319+
private static final String rialtoBridgeJson = a2q("{'name': 'Rialto Bridge'}");
320+
private static final String sighsBridgeJson = a2q("{'name': 'The Bridge Of Sighs'}");
321+
private static final String bridgesJson = a2q("["+ rialtoBridgeJson +"," + sighsBridgeJson +"]");
322+
private static final String veniceJson = a2q("{'name': 'Venice', 'places': {'bridges': " + bridgesJson + "}}");
323+
324+
private static final String alpsJson = a2q("{'name': 'The Alps'}");
325+
private static final String citesJson = a2q("[" + romeJson + "," + veniceJson + "]");
326+
private static final String italy = a2q("{'name': 'Italy', 'places': {'mountains': " + alpsJson + ", 'cities': "+ citesJson +"}}}");
327+
328+
public void testSupertypeInferenceWhenDefaultDefined() throws Exception {
329+
//When:
330+
WorthSeeing worthSeeing = MAPPER.readValue(alpsJson, WorthSeeing.class);
331+
// Then:
332+
assertEqualsPlace("The Alps", worthSeeing);
333+
}
334+
335+
public void testDefaultImplementation() throws Exception {
336+
// When:
337+
WorthSeeing worthSeeing = MAPPER.readValue(citesJson, WorthSeeing.class);
338+
// Then:
339+
assertCities(worthSeeing);
340+
}
341+
342+
public void testCompositeInference() throws Exception {
343+
// When:
344+
WorthSeeing worthSeeing = MAPPER.readValue(italy, WorthSeeing.class);
345+
// Then:
346+
assertSame(CompositePlace.class, worthSeeing.getClass());
347+
CompositePlace italy = (CompositePlace) worthSeeing;
348+
assertEquals("Italy", italy.name);
349+
assertEquals(2, italy.places.size());
350+
assertEqualsPlace("The Alps", italy.places.get("mountains"));
351+
assertEquals(2, italy.places.size());
352+
assertCities(italy.places.get("cities"));
353+
}
354+
355+
private void assertCities(WorthSeeing worthSeeing) {
356+
assertSame(ListOfPlaces.class, worthSeeing.getClass());
357+
ListOfPlaces cities = (ListOfPlaces) worthSeeing;
358+
assertEquals(2, cities.size());
359+
assertRome(cities.get(0));
360+
assertVenice(cities.get(1));
361+
}
362+
363+
private void assertRome(WorthSeeing worthSeeing) {
364+
assertSame(CompositePlace.class, worthSeeing.getClass());
365+
CompositePlace rome = (CompositePlace) worthSeeing;
366+
assertEquals("Rome", rome.name);
367+
assertEquals(2, rome.places.size());
368+
assertEqualsPlace("The Colosseum", rome.places.get("colosseum"));
369+
assertEqualsPlace("The Roman Forum", rome.places.get("romanForum"));
370+
}
371+
372+
private void assertVenice(WorthSeeing worthSeeing) {
373+
assertSame(CompositePlace.class, worthSeeing.getClass());
374+
CompositePlace venice = (CompositePlace) worthSeeing;
375+
assertEquals("Venice", venice.name);
376+
assertEquals(1, venice.places.size());
377+
assertVeniceBridges(venice.places.get("bridges"));
378+
379+
}
380+
381+
private void assertVeniceBridges(WorthSeeing worthSeeing){
382+
assertSame(ListOfPlaces.class, worthSeeing.getClass());
383+
ListOfPlaces bridges = (ListOfPlaces) worthSeeing;
384+
assertEqualsPlace("Rialto Bridge", bridges.get(0));
385+
assertEqualsPlace("The Bridge Of Sighs", bridges.get(1));
386+
}
387+
388+
private void assertEqualsPlace(String expectedName, WorthSeeing worthSeeing){
389+
assertTrue(worthSeeing instanceof Place);
390+
assertSame(Place.class, worthSeeing.getClass());
391+
assertEquals(expectedName,((Place) worthSeeing).name);
392+
}
272393
}

0 commit comments

Comments
 (0)