Skip to content

Add empty class support for deduction #3137

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

Closed
wants to merge 2 commits into from
Closed
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 @@ -35,6 +35,8 @@ public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer
// Bitmap of available fields in each subtype (including its parents)
private final Map<BitSet, String> subtypeFingerprints;

private static final String EMPTY_CLASS_MARKER = "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs Javadoc to explain use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment: empty String is actually valid property name in JSON -- is this problematic?


public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl,
DeserializationConfig config, Collection<NamedType> subtypes)
{
Expand Down Expand Up @@ -65,15 +67,14 @@ protected Map<BitSet, String> buildFingerprints(DeserializationConfig config, Co
List<BeanPropertyDefinition> properties = config.introspect(subtyped).findProperties();

BitSet fingerprint = new BitSet(nextField + properties.size());
for (BeanPropertyDefinition property : properties) {
String name = property.getName();
if (ignoreCase) name = name.toLowerCase();
Integer bitIndex = fieldBitIndex.get(name);
if (bitIndex == null) {
bitIndex = nextField;
fieldBitIndex.put(name, nextField++);
if (properties.size() > 0) {
for (BeanPropertyDefinition property : properties) {
String name = property.getName();
if (ignoreCase) name = name.toLowerCase();
nextField = setField(fingerprint, name, nextField);
}
fingerprint.set(bitIndex);
} else {
nextField = setField(fingerprint, EMPTY_CLASS_MARKER, nextField);
}

String existingFingerprint = fingerprints.put(fingerprint, subtype.getType().getName());
Expand All @@ -88,6 +89,16 @@ protected Map<BitSet, String> buildFingerprints(DeserializationConfig config, Co
return fingerprints;
}

private int setField(BitSet fingerprint, String name, int nextField) {
Integer bitIndex = fieldBitIndex.get(name);
if (bitIndex == null) {
bitIndex = nextField;
fieldBitIndex.put(name, nextField++);
}
fingerprint.set(bitIndex);
return nextField;
}

@Override
public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {

Expand All @@ -110,35 +121,48 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
// Record processed tokens as we must rewind once after deducing the deserializer to use
@SuppressWarnings("resource")
TokenBuffer tb = new TokenBuffer(p, ctxt);
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);

for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
String name = p.currentName();
if (ignoreCase) name = name.toLowerCase();

tb.copyCurrentStructure(p);

Integer bit = fieldBitIndex.get(name);
if (bit != null) {
// 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)));
Object result = null;

// Next character of empty class is '}'
if (t == JsonToken.END_OBJECT) {
result = deserializeCandidates(EMPTY_CLASS_MARKER, candidates, p, ctxt, tb);
} else {
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
String name = p.currentName();
if (ignoreCase) name = name.toLowerCase();
if ((result = deserializeCandidates(name, candidates, p, ctxt, tb)) != null) {
break;
}
}
}
if (result != null) {
return result;
}

// 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);
}

// Keep only fingerprints containing this field
private static void prune(List<BitSet> candidates, int bit) {
for (Iterator<BitSet> iter = candidates.iterator(); iter.hasNext(); ) {
if (!iter.next().get(bit)) {
iter.remove();
private Object deserializeCandidates(String name, List<BitSet> candidates, JsonParser p, DeserializationContext ctxt, TokenBuffer tb) throws IOException {
Integer bit = fieldBitIndex.get(name);
// No empty class is registered
if (bit == null && name.equals(EMPTY_CLASS_MARKER)) {
return null;
}
if (bit != null) {
// field is known by at least one subtype
for (Iterator<BitSet> iter = candidates.iterator(); iter.hasNext(); ) {
if (!iter.next().get(bit)) {
iter.remove();
}
}
}
tb.copyCurrentStructure(p);
if (candidates.size() != 1) {
return null;
}
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0)));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.databind.jsontype;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

Expand All @@ -22,11 +23,12 @@
// for [databind#43], deduction-based polymorphism
public class TestPolymorphicDeduction extends BaseMapTest {

@JsonTypeInfo(use = DEDUCTION)
@JsonSubTypes( {@Type(LiveCat.class), @Type(DeadCat.class)})
public static class Cat {
public String name;
}
@JsonTypeInfo(use = DEDUCTION)
@JsonSubTypes({@Type(LiveCat.class), @Type(DeadCat.class)})
public static class Cat {
public String name;
public Flea flea;
}

static class DeadCat extends Cat {
public String causeOfDeath;
Expand All @@ -36,6 +38,18 @@ static class LiveCat extends Cat {
public boolean angry;
}

@JsonTypeInfo(use = DEDUCTION)
@JsonSubTypes({@Type(NoFlea.class), @Type(HasFlea.class)})
interface Flea {
}

static class NoFlea implements Flea {
}

static class HasFlea implements Flea {
public int count;
}

static class Box {
public Cat cat;
}
Expand All @@ -46,40 +60,60 @@ static class Box {
/**********************************************************
*/

private static final String deadCatJson = aposToQuotes("{'name':'Felix','causeOfDeath':'entropy'}");
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 arrayOfCatsJson = aposToQuotes("[" + liveCatJson + "," + deadCatJson + "]");
private static final String mapOfCatsJson = aposToQuotes("{'live':" + liveCatJson + "}");
private static final String deadCatJson = aposToQuotes("{'name':'Felix','flea':null,'causeOfDeath':'entropy'}");
private static final String liveCatJson = aposToQuotes("{'name':'Felix','flea':null,'angry':true}");
private static final String liveNoFleaCatJson = aposToQuotes("{'name':'Felix','flea':{},'angry':true}");
private static final String liveHasFleaCatJson = aposToQuotes("{'name':'Felix','flea':{'count':42},'angry':true}");
private static final String luckyCatJson = aposToQuotes("{'name':'Felix','flea':null,'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 arrayOfCatsJson = aposToQuotes("[" + liveCatJson + "," + deadCatJson + "," + liveNoFleaCatJson + "]");
private static final String mapOfCatsJson = aposToQuotes("{'live':" + liveCatJson + ",'noFlea':" + liveNoFleaCatJson + "}");

/*
/**********************************************************
/* Test methods
/**********************************************************
*/

public void testSimpleInference() throws Exception {
Cat cat = sharedMapper().readValue(liveCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
assertSame(cat.getClass(), LiveCat.class);
assertEquals("Felix", cat.name);
assertTrue(((LiveCat)cat).angry);
public void testSimpleInference() throws Exception {

cat = sharedMapper().readValue(deadCatJson, Cat.class);
assertTrue(cat instanceof DeadCat);
assertSame(cat.getClass(), DeadCat.class);
assertEquals("Felix", cat.name);
assertEquals("entropy", ((DeadCat)cat).causeOfDeath);
}
Cat cat = sharedMapper().readValue(liveCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
assertSame(cat.getClass(), LiveCat.class);
assertEquals("Felix", cat.name);
assertTrue(((LiveCat) cat).angry);

cat = sharedMapper().readValue(deadCatJson, Cat.class);
assertTrue(cat instanceof DeadCat);
assertSame(cat.getClass(), DeadCat.class);
assertEquals("Felix", cat.name);
assertEquals("entropy", ((DeadCat) cat).causeOfDeath);

cat = sharedMapper().readValue(liveNoFleaCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
assertTrue(cat.flea instanceof NoFlea);
assertSame(cat.getClass(), LiveCat.class);
assertSame(cat.flea.getClass(), NoFlea.class);
assertEquals("Felix", cat.name);
assertTrue(((LiveCat) cat).angry);

cat = sharedMapper().readValue(liveHasFleaCatJson, Cat.class);
assertTrue(cat instanceof LiveCat);
assertTrue(cat.flea instanceof HasFlea);
assertSame(cat.getClass(), LiveCat.class);
assertSame(cat.flea.getClass(), HasFlea.class);
assertEquals("Felix", cat.name);
assertEquals(42, ((HasFlea) cat.flea).count);
assertTrue(((LiveCat) cat).angry);
}

public void testCaseInsensitiveInference() throws Exception {
Cat cat = JsonMapper.builder() // Don't use shared mapper!
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.build()
.readValue(deadCatJson.toUpperCase(), Cat.class);
.readValue(deadCatJson.toUpperCase().replace("NULL", "null"), Cat.class);
assertTrue(cat instanceof DeadCat);
assertSame(cat.getClass(), DeadCat.class);
assertEquals("FELIX", cat.name);
Expand Down Expand Up @@ -120,12 +154,18 @@ public void testListInference() throws Exception {
assertTrue(boxes.get(1) instanceof DeadCat);
}

public void testMapInference() throws Exception {
JavaType mapOfCats = TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, Cat.class);
Map<String, Cat> map = sharedMapper().readValue(mapOfCatsJson, mapOfCats);
assertEquals(1, map.size());
assertTrue(map.entrySet().iterator().next().getValue() instanceof LiveCat);
}
public void testMapInference() throws Exception {
JavaType mapOfCats = TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, Cat.class);
Map<String, Cat> map = sharedMapper().readValue(mapOfCatsJson, mapOfCats);
assertEquals(2, map.size());

Iterator<Map.Entry<String, Cat>> iterator = map.entrySet().iterator();
assertTrue(iterator.next().getValue() instanceof LiveCat);

Cat noFlea = iterator.next().getValue();
assertTrue(noFlea instanceof LiveCat);
assertTrue(noFlea.flea instanceof NoFlea);
}

public void testArrayInference() throws Exception {
Cat[] boxes = sharedMapper().readValue(arrayOfCatsJson, Cat[].class);
Expand Down Expand Up @@ -200,16 +240,39 @@ public void testDefaultImpl() throws Exception {
assertEquals("Felix", cat.name);
}

public void testSimpleSerialization() throws Exception {
// Given:
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);
List<Cat> list = sharedMapper().readValue(arrayOfCatsJson, listOfCats);
Cat cat = list.get(0);
// When:
String json = sharedMapper().writeValueAsString(cat);
// Then:
assertEquals(liveCatJson, json);
}

@JsonTypeInfo(use = DEDUCTION)
@JsonSubTypes({@Type(HasFlea.class)})
abstract static class FleaMixin {

}
public void testNoEmptyClassRegistered() throws Exception {
JsonMapper mapper = JsonMapper.builder() // Don't use shared mapper!
.addMixIn(Flea.class, FleaMixin.class)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
try {
mapper.readValue(liveNoFleaCatJson, Cat.class);
} catch (InvalidTypeIdException e) {
verifyException(e, "Cannot deduce unique subtype");
}
}

public void testSimpleSerialization() throws Exception {
// Given:
JavaType listOfCats = TypeFactory.defaultInstance().constructParametricType(List.class, Cat.class);
List<Cat> list = sharedMapper().readValue(arrayOfCatsJson, listOfCats);
Cat liveCat = list.get(0);
Cat liveNoFleaCat = list.get(2);
// When:
String json = sharedMapper().writeValueAsString(liveCat);
String noFleaJson = sharedMapper().writeValueAsString(liveNoFleaCat);
// Then:
assertEquals(liveCatJson, json);
assertEquals(liveNoFleaCatJson, noFleaJson);
}



public void testListSerialization() throws Exception {
// Given:
Expand Down