Skip to content

Commit 9a271ef

Browse files
authored
Polymorphic subtype deduction from available fields (#2813)
Polymorphic deduction implemented
1 parent dd2ed05 commit 9a271ef

File tree

4 files changed

+388
-10
lines changed

4 files changed

+388
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.fasterxml.jackson.databind.jsontype.impl;
2+
3+
import java.io.IOException;
4+
import java.util.BitSet;
5+
import java.util.Collection;
6+
import java.util.HashMap;
7+
import java.util.Iterator;
8+
import java.util.LinkedList;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
13+
import com.fasterxml.jackson.core.JsonParser;
14+
import com.fasterxml.jackson.core.JsonToken;
15+
import com.fasterxml.jackson.databind.BeanProperty;
16+
import com.fasterxml.jackson.databind.DeserializationConfig;
17+
import com.fasterxml.jackson.databind.DeserializationContext;
18+
import com.fasterxml.jackson.databind.JavaType;
19+
import com.fasterxml.jackson.databind.MapperFeature;
20+
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
21+
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
22+
import com.fasterxml.jackson.databind.jsontype.NamedType;
23+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
24+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
25+
import com.fasterxml.jackson.databind.util.TokenBuffer;
26+
27+
/**
28+
* A {@link TypeDeserializer} capable of deducing polymorphic types based on the fields available. Deduction
29+
* is limited to the <i>names</i> of child fields (not their values or, consequently, any nested descendants).
30+
* Exceptions will be thrown if not enough unique information is present to select a single subtype.
31+
*/
32+
public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer {
33+
34+
// Fieldname -> bitmap-index of every field discovered, across all subtypes
35+
private final Map<String, Integer> fieldBitIndex;
36+
// Bitmap of available fields in each subtype (including its parents)
37+
private final Map<BitSet, String> subtypeFingerprints;
38+
39+
public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl, DeserializationConfig config, Collection<NamedType> subtypes) {
40+
super(bt, idRes, null, false, defaultImpl);
41+
fieldBitIndex = new HashMap<>();
42+
subtypeFingerprints = buildFingerprints(config, subtypes);
43+
}
44+
45+
public AsDeductionTypeDeserializer(AsDeductionTypeDeserializer src, BeanProperty property) {
46+
super(src, property);
47+
fieldBitIndex = src.fieldBitIndex;
48+
subtypeFingerprints = src.subtypeFingerprints;
49+
}
50+
51+
@Override
52+
public JsonTypeInfo.As getTypeInclusion() {
53+
return null;
54+
}
55+
56+
@Override
57+
public TypeDeserializer forProperty(BeanProperty prop) {
58+
return (prop == _property) ? this : new AsDeductionTypeDeserializer(this, prop);
59+
}
60+
61+
protected Map<BitSet, String> buildFingerprints(DeserializationConfig config, Collection<NamedType> subtypes) {
62+
boolean ignoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
63+
64+
int nextField = 0;
65+
Map<BitSet, String> fingerprints = new HashMap<>();
66+
67+
for (NamedType subtype : subtypes) {
68+
JavaType subtyped = config.getTypeFactory().constructType(subtype.getType());
69+
List<BeanPropertyDefinition> properties = config.introspect(subtyped).findProperties();
70+
71+
BitSet fingerprint = new BitSet(nextField + properties.size());
72+
for (BeanPropertyDefinition property : properties) {
73+
String name = property.getName();
74+
if (ignoreCase) name = name.toLowerCase();
75+
Integer bitIndex = fieldBitIndex.get(name);
76+
if (bitIndex == null) {
77+
bitIndex = nextField;
78+
fieldBitIndex.put(name, nextField++);
79+
}
80+
fingerprint.set(bitIndex);
81+
}
82+
83+
String existingFingerprint = fingerprints.put(fingerprint, subtype.getType().getName());
84+
85+
// Validate uniqueness
86+
if (existingFingerprint != null) {
87+
throw new IllegalStateException(
88+
String.format("Subtypes %s and %s have the same signature and cannot be uniquely deduced.", existingFingerprint, subtype.getType().getName())
89+
);
90+
}
91+
92+
}
93+
return fingerprints;
94+
}
95+
96+
@Override
97+
public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
98+
99+
JsonToken t = p.currentToken();
100+
if (t == JsonToken.START_OBJECT) {
101+
t = p.nextToken();
102+
} else {
103+
/* This is most likely due to the fact that not all Java types are
104+
* serialized as JSON Objects; so if "as-property" inclusion is requested,
105+
* serialization of things like Lists must be instead handled as if
106+
* "as-wrapper-array" was requested.
107+
* But this can also be due to some custom handling: so, if "defaultImpl"
108+
* is defined, it will be asked to handle this case.
109+
*/
110+
return _deserializeTypedUsingDefaultImpl(p, ctxt, null);
111+
}
112+
113+
List<BitSet> candidates = new LinkedList<>(subtypeFingerprints.keySet());
114+
115+
// Record processed tokens as we must rewind once after deducing the deserializer to use
116+
TokenBuffer tb = new TokenBuffer(p, ctxt);
117+
boolean ignoreCase = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
118+
119+
for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
120+
String name = p.getCurrentName();
121+
if (ignoreCase) name = name.toLowerCase();
122+
123+
tb.copyCurrentStructure(p);
124+
125+
Integer bit = fieldBitIndex.get(name);
126+
if (bit != null) {
127+
// field is known by at least one subtype
128+
prune(candidates, bit);
129+
if (candidates.size() == 1) {
130+
return _deserializeTypedForId(p, ctxt, tb, subtypeFingerprints.get(candidates.get(0)));
131+
}
132+
}
133+
}
134+
135+
throw new InvalidTypeIdException(
136+
p,
137+
String.format("Cannot deduce unique subtype of %s (%d candidates match)", _baseType.toString(), candidates.size()),
138+
_baseType
139+
, "DEDUCED"
140+
);
141+
}
142+
143+
// Keep only fingerprints containing this field
144+
private static void prune(List<BitSet> candidates, int bit) {
145+
for (Iterator<BitSet> iter = candidates.iterator(); iter.hasNext(); ) {
146+
if (!iter.next().get(bit)) {
147+
iter.remove();
148+
}
149+
}
150+
}
151+
152+
}

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
import java.io.IOException;
44

55
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
6-
import com.fasterxml.jackson.core.*;
6+
import com.fasterxml.jackson.core.JsonParser;
7+
import com.fasterxml.jackson.core.JsonToken;
78
import com.fasterxml.jackson.core.util.JsonParserSequence;
8-
import com.fasterxml.jackson.databind.*;
9+
import com.fasterxml.jackson.databind.BeanProperty;
10+
import com.fasterxml.jackson.databind.DeserializationContext;
11+
import com.fasterxml.jackson.databind.DeserializationFeature;
12+
import com.fasterxml.jackson.databind.JavaType;
13+
import com.fasterxml.jackson.databind.JsonDeserializer;
14+
import com.fasterxml.jackson.databind.MapperFeature;
915
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
1016
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
1117
import com.fasterxml.jackson.databind.util.TokenBuffer;
@@ -96,7 +102,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
96102
p.nextToken(); // to point to the value
97103
if (name.equals(_typePropertyName)
98104
|| (ignoreCase && name.equalsIgnoreCase(_typePropertyName))) { // gotcha!
99-
return _deserializeTypedForId(p, ctxt, tb);
105+
return _deserializeTypedForId(p, ctxt, tb, p.getText());
100106
}
101107
if (tb == null) {
102108
tb = new TokenBuffer(p, ctxt);
@@ -109,9 +115,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
109115

110116
@SuppressWarnings("resource")
111117
protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctxt,
112-
TokenBuffer tb) throws IOException
113-
{
114-
String typeId = p.getText();
118+
TokenBuffer tb, String typeId) throws IOException {
115119
JsonDeserializer<Object> deser = _findDeserializer(ctxt, typeId);
116120
if (_typeIdVisible) { // need to merge id back in JSON input?
117121
if (tb == null) {
@@ -131,7 +135,7 @@ protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctx
131135
// deserializer should take care of closing END_OBJECT as well
132136
return deser.deserialize(p, ctxt);
133137
}
134-
138+
135139
// off-lined to keep main method lean and mean...
136140
@SuppressWarnings("resource")
137141
protected Object _deserializeTypedUsingDefaultImpl(JsonParser p,

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

+23-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
import java.util.Collection;
44

55
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6-
7-
import com.fasterxml.jackson.databind.*;
6+
import com.fasterxml.jackson.databind.DeserializationConfig;
7+
import com.fasterxml.jackson.databind.JavaType;
8+
import com.fasterxml.jackson.databind.MapperFeature;
9+
import com.fasterxml.jackson.databind.SerializationConfig;
810
import com.fasterxml.jackson.databind.annotation.NoClass;
911
import com.fasterxml.jackson.databind.cfg.MapperConfig;
10-
import com.fasterxml.jackson.databind.jsontype.*;
12+
import com.fasterxml.jackson.databind.jsontype.NamedType;
13+
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
1114
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator.Validity;
15+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
16+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
17+
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
18+
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
1219
import com.fasterxml.jackson.databind.util.ClassUtil;
1320

1421
/**
@@ -89,8 +96,15 @@ public TypeSerializer buildTypeSerializer(SerializationConfig config,
8996
return null;
9097
}
9198
}
99+
92100
TypeIdResolver idRes = idResolver(config, baseType, subTypeValidator(config),
93101
subtypes, true, false);
102+
103+
if(_idType == JsonTypeInfo.Id.DEDUCTION) {
104+
// Deduction doesn't require a type property. We use EXISTING_PROPERTY with a name of <null> to drive this.
105+
return new AsExistingPropertyTypeSerializer(idRes, null, _typeProperty);
106+
}
107+
94108
switch (_includeAs) {
95109
case WRAPPER_ARRAY:
96110
return new AsArrayTypeSerializer(idRes, null);
@@ -135,6 +149,11 @@ public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,
135149

136150
JavaType defaultImpl = defineDefaultImpl(config, baseType);
137151

152+
if(_idType == JsonTypeInfo.Id.DEDUCTION) {
153+
// Deduction doesn't require an includeAs property
154+
return new AsDeductionTypeDeserializer(baseType, idRes, defaultImpl, config, subtypes);
155+
}
156+
138157
// First, method for converting type info to type id:
139158
switch (_includeAs) {
140159
case WRAPPER_ARRAY:
@@ -268,6 +287,7 @@ protected TypeIdResolver idResolver(MapperConfig<?> config,
268287
if (_customIdResolver != null) { return _customIdResolver; }
269288
if (_idType == null) throw new IllegalStateException("Cannot build, 'init()' not yet called");
270289
switch (_idType) {
290+
case DEDUCTION: // Deduction produces class names to be resolved
271291
case CLASS:
272292
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
273293
case MINIMAL_CLASS:

0 commit comments

Comments
 (0)