Skip to content

Commit

Permalink
ISSUE_3289 Add support for pojo-hierarchies in deduction deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
paulina-korcz-viacomcbs committed Sep 29, 2021
1 parent c100ed5 commit 7169cb8
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
@SuppressWarnings("resource")
final TokenBuffer tb = ctxt.bufferForInputBuffering(p);
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
BitSet existingFingerprint = new BitSet();

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

Integer bit = fieldBitIndex.get(name);
if (bit != null) {
existingFingerprint.set(bit);
// field is known by at least one subtype
prune(candidates, bit);
if (candidates.size() == 1) {
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0)));
}
}
}
for(BitSet candidate: candidates) {
if (existingFingerprint.equals(candidate)){
tb.copyCurrentStructure(p);
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidate));
}
}

// We have zero or multiple candidates, deduction has failed
// We have zero or multiple candidates and none of them fit exactly the existing fingerprint, 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
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.databind.jsontype;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

Expand All @@ -13,6 +14,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;

Expand Down Expand Up @@ -210,9 +212,33 @@ public void testAmbiguousClasses() throws Exception {
}
}

public void testUnrecognizedProperties() throws Exception {
try {
/*Cat cat =*/
MAPPER.readValue(ambiguousCatJson, Cat.class);
fail("Unable to map, because there is unknown field 'age'");
} catch (UnrecognizedPropertyException e) {
verifyException(e, "Unrecognized field");
}
}

public void testNotFailOnUnknownProperty() throws Exception {
// Given:
JsonMapper mapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
// When:
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
// Then:
// unknown proparty 'age' is ignored, and json is deserialized to Cat class
assertTrue(cat instanceof Cat);
assertSame(Cat.class, cat.getClass());
assertEquals("Felix", cat.name);
}

public void testAmbiguousProperties() throws Exception {
try {
/*Cat cat =*/ MAPPER.readValue(ambiguousCatJson, Cat.class);
/*Feline cat =*/ MAPPER.readValue(ambiguousCatJson, Feline.class);
fail("Should not get here");
} catch (InvalidTypeIdException e) {
verifyException(e, "Cannot deduce unique subtype");
Expand All @@ -225,7 +251,7 @@ public void testFailOnInvalidSubtype() throws Exception {
.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
.build();
// When:
Cat cat = mapper.readValue(ambiguousCatJson, Cat.class);
Feline cat = mapper.readValue(ambiguousCatJson, Feline.class);
// Then:
assertNull(cat);
}
Expand Down Expand Up @@ -269,4 +295,99 @@ public void testListSerialization() throws Exception {
// Then:
assertEquals(arrayOfCatsJson, json);
}

@JsonTypeInfo(use = DEDUCTION, defaultImpl = ListOfPlaces.class)
@JsonSubTypes( {@Type(ListOfPlaces.class), @Type(CompositePlace.class), @Type(Place.class)})
interface WorthSeeing {}

public static class Place implements WorthSeeing {
public String name;
}

public static class CompositePlace extends Place implements WorthSeeing {

public Map<String, WorthSeeing> places;
}

static class ListOfPlaces extends ArrayList<WorthSeeing> implements WorthSeeing {
}

private static final String colosseumJson = a2q("{'name': 'The Colosseum'}");
private static final String romanForumJson = a2q("{'name': 'The Roman Forum'}");
private static final String romeJson = a2q("{'name': 'Rome', 'places': {'colosseum': " + colosseumJson + ","+ "'romanForum': "+ romanForumJson +"}}");

private static final String rialtoBridgeJson = a2q("{'name': 'Rialto Bridge'}");
private static final String sighsBridgeJson = a2q("{'name': 'The Bridge Of Sighs'}");
private static final String bridgesJson = a2q("["+ rialtoBridgeJson +"," + sighsBridgeJson +"]");
private static final String veniceJson = a2q("{'name': 'Venice', 'places': {'bridges': " + bridgesJson + "}}");

private static final String alpsJson = a2q("{'name': 'The Alps'}");
private static final String citesJson = a2q("[" + romeJson + "," + veniceJson + "]");
private static final String italy = a2q("{'name': 'Italy', 'places': {'mountains': " + alpsJson + ", 'cities': "+ citesJson +"}}}");

public void testSupertypeInferenceWhenDefaultDefined() throws Exception {
//When:
WorthSeeing worthSeeing = MAPPER.readValue(alpsJson, WorthSeeing.class);
// Then:
assertEqualsPlace("The Alps", worthSeeing);
}

public void testDefaultImplementation() throws Exception {
// When:
WorthSeeing worthSeeing = MAPPER.readValue(citesJson, WorthSeeing.class);
// Then:
assertCities(worthSeeing);
}

public void testCompositeInference() throws Exception {
// When:
WorthSeeing worthSeeing = MAPPER.readValue(italy, WorthSeeing.class);
// Then:
assertSame(CompositePlace.class, worthSeeing.getClass());
CompositePlace italy = (CompositePlace) worthSeeing;
assertEquals("Italy", italy.name);
assertEquals(2, italy.places.size());
assertEqualsPlace("The Alps", italy.places.get("mountains"));
assertEquals(2, italy.places.size());
assertCities(italy.places.get("cities"));
}

private void assertCities(WorthSeeing worthSeeing) {
assertSame(ListOfPlaces.class, worthSeeing.getClass());
ListOfPlaces cities = (ListOfPlaces) worthSeeing;
assertEquals(2, cities.size());
assertRome(cities.get(0));
assertVenice(cities.get(1));
}

private void assertRome(WorthSeeing worthSeeing) {
assertSame(CompositePlace.class, worthSeeing.getClass());
CompositePlace rome = (CompositePlace) worthSeeing;
assertEquals("Rome", rome.name);
assertEquals(2, rome.places.size());
assertEqualsPlace("The Colosseum", rome.places.get("colosseum"));
assertEqualsPlace("The Roman Forum", rome.places.get("romanForum"));
}

private void assertVenice(WorthSeeing worthSeeing) {
assertSame(CompositePlace.class, worthSeeing.getClass());
CompositePlace venice = (CompositePlace) worthSeeing;
assertEquals("Venice", venice.name);
assertEquals(1, venice.places.size());
assertVeniceBridges(venice.places.get("bridges"));

}

private void assertVeniceBridges(WorthSeeing worthSeeing){
assertSame(ListOfPlaces.class, worthSeeing.getClass());
ListOfPlaces bridges = (ListOfPlaces) worthSeeing;
assertEqualsPlace("Rialto Bridge", bridges.get(0));
assertEqualsPlace("The Bridge Of Sighs", bridges.get(1));
}

private void assertEqualsPlace(String expectedName, WorthSeeing worthSeeing){
assertTrue(worthSeeing instanceof Place);
assertSame(Place.class, worthSeeing.getClass());
assertEquals(expectedName,((Place) worthSeeing).name);
}
}

0 comments on commit 7169cb8

Please sign in to comment.