Skip to content
Merged
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
@@ -0,0 +1,152 @@
package com.fasterxml.jackson.databind.jsontype.impl;

import java.io.IOException;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.util.TokenBuffer;

/**
* A {@link TypeDeserializer} capable of deducing polymorphic types based on the fields available. Deduction
* is limited to the <i>names</i> of child fields (not their values or, consequently, any nested descendants).
* Exceptions will be thrown if not enough unique information is present to select a single subtype.
*/
public class AsDeductionTypeDeserializer extends AsPropertyTypeDeserializer {

// Fieldname -> bitmap-index of every field discovered, across all subtypes
private final Map<String, Integer> fieldBitIndex;
// Bitmap of available fields in each subtype (including its parents)
private final Map<BitSet, String> subtypeFingerprints;

public AsDeductionTypeDeserializer(JavaType bt, TypeIdResolver idRes, JavaType defaultImpl, DeserializationConfig config, Collection<NamedType> subtypes) {
super(bt, idRes, null, false, defaultImpl);
fieldBitIndex = new HashMap<>();
subtypeFingerprints = buildFingerprints(config, subtypes);
}

public AsDeductionTypeDeserializer(AsDeductionTypeDeserializer src, BeanProperty property) {
super(src, property);
fieldBitIndex = src.fieldBitIndex;
subtypeFingerprints = src.subtypeFingerprints;
}

@Override
public JsonTypeInfo.As getTypeInclusion() {
return null;
}

@Override
public TypeDeserializer forProperty(BeanProperty prop) {
return (prop == _property) ? this : new AsDeductionTypeDeserializer(this, prop);
}

protected Map<BitSet, String> buildFingerprints(DeserializationConfig config, Collection<NamedType> subtypes) {
boolean ignoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);

int nextField = 0;
Map<BitSet, String> fingerprints = new HashMap<>();

for (NamedType subtype : subtypes) {
JavaType subtyped = config.getTypeFactory().constructType(subtype.getType());
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++);
}
fingerprint.set(bitIndex);
}

String existingFingerprint = fingerprints.put(fingerprint, subtype.getType().getName());

// Validate uniqueness
if (existingFingerprint != null) {
throw new IllegalStateException(
String.format("Subtypes %s and %s have the same signature and cannot be uniquely deduced.", existingFingerprint, subtype.getType().getName())
);
}

}
return fingerprints;
}

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

JsonToken t = p.currentToken();
if (t == JsonToken.START_OBJECT) {
t = p.nextToken();
} else {
/* This is most likely due to the fact that not all Java types are
* serialized as JSON Objects; so if "as-property" inclusion is requested,
* serialization of things like Lists must be instead handled as if
* "as-wrapper-array" was requested.
* But this can also be due to some custom handling: so, if "defaultImpl"
* is defined, it will be asked to handle this case.
*/
return _deserializeTypedUsingDefaultImpl(p, ctxt, null);
}

List<BitSet> candidates = new LinkedList<>(subtypeFingerprints.keySet());

// Record processed tokens as we must rewind once after deducing the deserializer to use
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.getCurrentName();
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)));
}
}
}

throw new InvalidTypeIdException(
p,
String.format("Cannot deduce unique subtype of %s (%d candidates match)", _baseType.toString(), candidates.size()),
_baseType
, "DEDUCED"
);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
import java.io.IOException;

import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.util.JsonParserSequence;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.util.TokenBuffer;
Expand Down Expand Up @@ -96,7 +102,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct
p.nextToken(); // to point to the value
if (name.equals(_typePropertyName)
|| (ignoreCase && name.equalsIgnoreCase(_typePropertyName))) { // gotcha!
return _deserializeTypedForId(p, ctxt, tb);
return _deserializeTypedForId(p, ctxt, tb, p.getText());
}
if (tb == null) {
tb = new TokenBuffer(p, ctxt);
Expand All @@ -109,9 +115,7 @@ public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ct

@SuppressWarnings("resource")
protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctxt,
TokenBuffer tb) throws IOException
{
String typeId = p.getText();
TokenBuffer tb, String typeId) throws IOException {
JsonDeserializer<Object> deser = _findDeserializer(ctxt, typeId);
if (_typeIdVisible) { // need to merge id back in JSON input?
if (tb == null) {
Expand All @@ -131,7 +135,7 @@ protected Object _deserializeTypedForId(JsonParser p, DeserializationContext ctx
// deserializer should take care of closing END_OBJECT as well
return deser.deserialize(p, ctxt);
}

// off-lined to keep main method lean and mean...
@SuppressWarnings("resource")
protected Object _deserializeTypedUsingDefaultImpl(JsonParser p,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
import java.util.Collection;

import com.fasterxml.jackson.annotation.JsonTypeInfo;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.annotation.NoClass;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.*;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator.Validity;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.util.ClassUtil;

/**
Expand Down Expand Up @@ -89,8 +96,15 @@ public TypeSerializer buildTypeSerializer(SerializationConfig config,
return null;
}
}

TypeIdResolver idRes = idResolver(config, baseType, subTypeValidator(config),
subtypes, true, false);

if(_idType == JsonTypeInfo.Id.DEDUCTION) {
// Deduction doesn't require a type property. We use EXISTING_PROPERTY with a name of <null> to drive this.
return new AsExistingPropertyTypeSerializer(idRes, null, _typeProperty);
}

switch (_includeAs) {
case WRAPPER_ARRAY:
return new AsArrayTypeSerializer(idRes, null);
Expand Down Expand Up @@ -135,6 +149,11 @@ public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,

JavaType defaultImpl = defineDefaultImpl(config, baseType);

if(_idType == JsonTypeInfo.Id.DEDUCTION) {
// Deduction doesn't require an includeAs property
return new AsDeductionTypeDeserializer(baseType, idRes, defaultImpl, config, subtypes);
}

// First, method for converting type info to type id:
switch (_includeAs) {
case WRAPPER_ARRAY:
Expand Down Expand Up @@ -268,6 +287,7 @@ protected TypeIdResolver idResolver(MapperConfig<?> config,
if (_customIdResolver != null) { return _customIdResolver; }
if (_idType == null) throw new IllegalStateException("Cannot build, 'init()' not yet called");
switch (_idType) {
case DEDUCTION: // Deduction produces class names to be resolved
case CLASS:
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
case MINIMAL_CLASS:
Expand Down
Loading