Fixes #528 - Add support for As.EXISTING_PROPERTY inclusion mechanism
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeDeserializer.java
new file mode 100644
index 0000000..f075233
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeDeserializer.java
@@ -0,0 +1,156 @@
+package com.fasterxml.jackson.databind.jsontype.impl;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.util.JsonParserSequence;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
+import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
+import com.fasterxml.jackson.databind.util.TokenBuffer;
+
+/**
+ * Type deserializer used with {@link As#EXISTING_PROPERTY}
+ * inclusion mechanism.
+ *
+ * @author fleeman (modeled after code by tatus)
+ */
+public class AsExistingPropertyTypeDeserializer extends AsArrayTypeDeserializer
+{
+ private static final long serialVersionUID = 1L;
+
+ public AsExistingPropertyTypeDeserializer(JavaType bt, TypeIdResolver idRes,
+ String typePropertyName, boolean typeIdVisible, Class<?> defaultImpl)
+ {
+ super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl);
+ }
+
+ public AsExistingPropertyTypeDeserializer(AsExistingPropertyTypeDeserializer src, BeanProperty property) {
+ super(src, property);
+ }
+
+ @Override
+ public TypeDeserializer forProperty(BeanProperty prop) {
+ return (prop == _property) ? this : new AsExistingPropertyTypeDeserializer(this, prop);
+ }
+
+ @Override
+ public As getTypeInclusion() { return As.EXISTING_PROPERTY; }
+
+ /**
+ * This is the trickiest thing to handle, since property we are looking
+ * for may be anywhere...
+ */
+ @Override
+ public Object deserializeTypedFromObject(JsonParser jp, DeserializationContext ctxt) throws IOException
+ {
+ // 02-Aug-2013, tatu: May need to use native type ids
+ if (jp.canReadTypeId()) {
+ Object typeId = jp.getTypeId();
+ if (typeId != null) {
+ return _deserializeWithNativeTypeId(jp, ctxt, typeId);
+ }
+ }
+
+ // but first, sanity check to ensure we have START_OBJECT or FIELD_NAME
+ JsonToken t = jp.getCurrentToken();
+ if (t == JsonToken.START_OBJECT) {
+ t = jp.nextToken();
+ } else if (t == JsonToken.START_ARRAY) {
+ /* 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(jp, ctxt, null);
+ } else if (t != JsonToken.FIELD_NAME) {
+ return _deserializeTypedUsingDefaultImpl(jp, ctxt, null);
+ }
+ // Ok, let's try to find the property. But first, need token buffer...
+ TokenBuffer tb = null;
+
+ for (; t == JsonToken.FIELD_NAME; t = jp.nextToken()) {
+ String name = jp.getCurrentName();
+ jp.nextToken(); // to point to the value
+ if (_typePropertyName.equals(name)) { // gotcha!
+ return _deserializeTypedForId(jp, ctxt, tb);
+ }
+ if (tb == null) {
+ tb = new TokenBuffer(null, false);
+ }
+ tb.writeFieldName(name);
+ tb.copyCurrentStructure(jp);
+ }
+ return _deserializeTypedUsingDefaultImpl(jp, ctxt, tb);
+ }
+
+ @SuppressWarnings("resource")
+ protected final Object _deserializeTypedForId(JsonParser jp, DeserializationContext ctxt, TokenBuffer tb) throws IOException
+ {
+ String typeId = jp.getText();
+ JsonDeserializer<Object> deser = _findDeserializer(ctxt, typeId);
+ if (_typeIdVisible) { // need to merge id back in JSON input?
+ if (tb == null) {
+ tb = new TokenBuffer(null, false);
+ }
+ tb.writeFieldName(jp.getCurrentName());
+ tb.writeString(typeId);
+ }
+ if (tb != null) { // need to put back skipped properties?
+ jp = JsonParserSequence.createFlattened(tb.asParser(jp), jp);
+ }
+ // Must point to the next value; tb had no current, jp pointed to VALUE_STRING:
+ jp.nextToken(); // to skip past String value
+ // deserializer should take care of closing END_OBJECT as well
+ return deser.deserialize(jp, ctxt);
+ }
+
+ // off-lined to keep main method lean and mean...
+ protected Object _deserializeTypedUsingDefaultImpl(JsonParser jp, DeserializationContext ctxt, TokenBuffer tb) throws IOException
+ {
+ // As per [JACKSON-614], may have default implementation to use
+ JsonDeserializer<Object> deser = _findDefaultImplDeserializer(ctxt);
+ if (deser != null) {
+ if (tb != null) {
+ tb.writeEndObject();
+ jp = tb.asParser(jp);
+ // must move to point to the first token:
+ jp.nextToken();
+ }
+ return deser.deserialize(jp, ctxt);
+ }
+ // or, perhaps we just bumped into a "natural" value (boolean/int/double/String)?
+ Object result = TypeDeserializer.deserializeIfNatural(jp, ctxt, _baseType);
+ if (result != null) {
+ return result;
+ }
+ // or, something for which "as-property" won't work, changed into "wrapper-array" type:
+ if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
+ return super.deserializeTypedFromAny(jp, ctxt);
+ }
+ throw ctxt.wrongTokenException(jp, JsonToken.FIELD_NAME,
+ "missing property '"+_typePropertyName+"' that is to contain type id (for class "+baseTypeName()+")");
+ }
+
+ /* As per [JACKSON-352], also need to re-route "unknown" version. Need to think
+ * this through bit more in future, but for now this does address issue and has
+ * no negative side effects (at least within existing unit test suite).
+ */
+ @Override
+ public Object deserializeTypedFromAny(JsonParser jp, DeserializationContext ctxt) throws IOException {
+ /* [JACKSON-387]: Sometimes, however, we get an array wrapper; specifically
+ * when an array or list has been serialized with type information.
+ */
+ if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
+ return super.deserializeTypedFromArray(jp, ctxt);
+ }
+ return deserializeTypedFromObject(jp, ctxt);
+ }
+
+ // These are fine from base class:
+ //public Object deserializeTypedFromArray(JsonParser jp, DeserializationContext ctxt)
+ //public Object deserializeTypedFromScalar(JsonParser jp, DeserializationContext ctxt)
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeSerializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeSerializer.java
new file mode 100644
index 0000000..b669106
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsExistingPropertyTypeSerializer.java
@@ -0,0 +1,96 @@
+package com.fasterxml.jackson.databind.jsontype.impl;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
+import com.fasterxml.jackson.core.*;
+
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
+
+/**
+ * Type serializer used with {@link As#EXISTING_PROPERTY} inclusion mechanism.
+ * Expects type information to be a well-defined property on all sub-classes.
+ *
+ * 10/15/2014 - At time of commit, deserialization identical to deserializer
+ * for {@link As#PROPERTY} inclusion mechanism
+ *
+ * @author fleeman (modeled after code by tatus)
+ */
+public class AsExistingPropertyTypeSerializer
+ extends AsArrayTypeSerializer
+{
+ protected final String _typePropertyName;
+
+ public AsExistingPropertyTypeSerializer(TypeIdResolver idRes, BeanProperty property, String propName)
+ {
+ super(idRes, property);
+ _typePropertyName = propName;
+ }
+
+ @Override
+ public AsExistingPropertyTypeSerializer forProperty(BeanProperty prop) {
+ return (_property == prop) ? this : new AsExistingPropertyTypeSerializer(this._idResolver, prop, this._typePropertyName);
+ }
+
+ @Override
+ public String getPropertyName() { return _typePropertyName; }
+
+ @Override
+ public As getTypeInclusion() { return As.EXISTING_PROPERTY; }
+
+ @Override
+ public void writeTypePrefixForObject(Object value, JsonGenerator jgen) throws IOException
+ {
+ final String typeId = idFromValue(value);
+ if (jgen.canWriteTypeId()) {
+ jgen.writeTypeId(typeId);
+ jgen.writeStartObject();
+ } else {
+ jgen.writeStartObject();
+ }
+ }
+
+ @Override
+ public void writeTypePrefixForObject(Object value, JsonGenerator jgen, Class<?> type) throws IOException
+ {
+ final String typeId = idFromValueAndType(value, type);
+ if (jgen.canWriteTypeId()) {
+ jgen.writeTypeId(typeId);
+ jgen.writeStartObject();
+ } else {
+ jgen.writeStartObject();
+ }
+ }
+
+ @Override
+ public void writeTypeSuffixForObject(Object value, JsonGenerator jgen) throws IOException {
+ // always need to close, regardless of whether its native type id or not
+ jgen.writeEndObject();
+ }
+
+
+ /*
+ /**********************************************************
+ /* Writing with custom type id
+ /**********************************************************
+ */
+
+ // Only need to override Object-variants
+
+ @Override
+ public void writeCustomTypePrefixForObject(Object value, JsonGenerator jgen, String typeId) throws IOException
+ {
+ if (jgen.canWriteTypeId()) {
+ jgen.writeTypeId(typeId);
+ jgen.writeStartObject();
+ } else {
+ jgen.writeStartObject();
+ }
+ }
+
+ @Override
+ public void writeCustomTypeSuffixForObject(Object value, JsonGenerator jgen, String typeId) throws IOException {
+ jgen.writeEndObject();
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
index cba4d32..1edadbe 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
@@ -84,15 +84,17 @@
return new AsExternalTypeSerializer(idRes, null,
_typeProperty);
case EXISTING_PROPERTY:
- throw _noExisting();
+ // as per [#528]
+ return new AsExistingPropertyTypeSerializer(idRes, null, _typeProperty);
}
throw new IllegalStateException("Do not know how to construct standard type serializer for inclusion type: "+_includeAs);
}
// as per [#368]
- private IllegalArgumentException _noExisting() {
- return new IllegalArgumentException("Inclusion type "+_includeAs+" not yet supported");
- }
+ // removed when fix [#528]
+ //private IllegalArgumentException _noExisting() {
+ // return new IllegalArgumentException("Inclusion type "+_includeAs+" not yet supported");
+ //}
@Override
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,
@@ -117,7 +119,9 @@
return new AsExternalTypeDeserializer(baseType, idRes,
_typeProperty, _typeIdVisible, _defaultImpl);
case EXISTING_PROPERTY:
- throw _noExisting();
+ // as per [#528]
+ return new AsExistingPropertyTypeDeserializer(baseType, idRes,
+ _typeProperty, _typeIdVisible, _defaultImpl);
}
throw new IllegalStateException("Do not know how to construct standard type serializer for inclusion type: "+_includeAs);
}
diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypesExistingProperty.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypesExistingProperty.java
new file mode 100644
index 0000000..e547d2e
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypesExistingProperty.java
@@ -0,0 +1,284 @@
+package com.fasterxml.jackson.databind.jsontype;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.databind.BaseMapTest;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestSubtypesExistingProperty extends BaseMapTest {
+
+ /**
+ * Polymorphic base class - existing property forced by abstract method
+ */
+ @JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type")
+ @JsonSubTypes({
+ @Type(value = Dog.class, name = "doggie") ,
+ @Type(value = Cat.class, name = "kitty")
+ })
+ static abstract class Animal {
+ public String name;
+
+ protected Animal(String n) { name = n; }
+
+ public abstract String getType();
+ }
+
+ @JsonTypeName("doggie")
+ static class Dog extends Animal
+ {
+ public int boneCount;
+
+ private Dog() { super(null); }
+ public Dog(String name, int b) {
+ super(name);
+ boneCount = b;
+ }
+
+ @Override
+ public String getType() {
+ return "doggie";
+ }
+ }
+
+ @JsonTypeName("kitty")
+ static class Cat extends Animal
+ {
+ public String furColor;
+
+ private Cat() { super(null); }
+ public Cat(String name, String c) {
+ super(name);
+ furColor = c;
+ }
+
+ @Override
+ public String getType() {
+ return "kitty";
+ }
+ }
+
+ static class AnimalWrapper {
+ public Animal animal;
+ public AnimalWrapper() {}
+ public AnimalWrapper(Animal a) { animal = a; }
+ }
+
+
+ /**
+ * Polymorphic base class - existing property NOT forced by abstract method on base class
+ */
+ @JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type")
+ @JsonSubTypes({
+ @Type(value = Accord.class, name = "accord") ,
+ @Type(value = Camry.class, name = "camry")
+ })
+ static abstract class Car {
+ public String name;
+ protected Car(String n) { name = n; }
+ }
+
+ @JsonTypeName("accord")
+ static class Accord extends Car
+ {
+ public int speakerCount;
+
+ private Accord() { super(null); }
+ public Accord(String name, int b) {
+ super(name);
+ speakerCount = b;
+ }
+
+ public String getType() {
+ return "accord";
+ }
+ }
+
+ @JsonTypeName("camry")
+ static class Camry extends Car
+ {
+ public String exteriorColor;
+
+ private Camry() { super(null); }
+ public Camry(String name, String c) {
+ super(name);
+ exteriorColor = c;
+ }
+
+ public String getType() {
+ return "camry";
+ }
+ }
+
+ static class CarWrapper {
+ public Car car;
+ public CarWrapper() {}
+ public CarWrapper(Car c) { car = c; }
+ }
+
+ private final ObjectMapper MAPPER = new ObjectMapper();
+
+ /*
+ /**********************************************************
+ /* Mock data
+ /**********************************************************
+ */
+
+ private static final Cat beelzebub = new Cat("Beelzebub", "tabby");
+ private static final String beelzebubJson = "{\"name\":\"Beelzebub\",\"furColor\":\"tabby\",\"type\":\"kitty\"}";
+ private static final Dog rover = new Dog("Rover", 42);
+ private static final String roverJson = "{\"name\":\"Rover\",\"boneCount\":42,\"type\":\"doggie\"}";
+ private static final AnimalWrapper beelzebubWrapper = new AnimalWrapper(beelzebub);
+ private static final String beelzebubWrapperJson = "{\"animal\":" + beelzebubJson + "}";
+ private static final List<Animal> animalList = Arrays.asList(beelzebub, rover);
+ private static final String animalListJson = "[" + beelzebubJson + "," + roverJson + "]";
+
+ private static final Camry camry = new Camry("Sweet Ride", "candy-apple-red");
+ private static final String camryJson = "{\"name\":\"Sweet Ride\",\"exteriorColor\":\"candy-apple-red\",\"type\":\"camry\"}";
+ private static final Accord accord = new Accord("Road Rage", 6);
+ private static final String accordJson = "{\"name\":\"Road Rage\",\"speakerCount\":6,\"type\":\"accord\"}";
+ private static final CarWrapper camryWrapper = new CarWrapper(camry);
+ private static final String camryWrapperJson = "{\"car\":" + camryJson + "}";
+ private static final List<Car> carList = Arrays.asList(camry, accord);
+ private static final String carListJson = "[" + camryJson + "," + accordJson + "]";
+
+ /*
+ /**********************************************************
+ /* Unit tests
+ /**********************************************************
+ */
+
+ /**
+ * Animals - serialization tests for abstract method in base class
+ */
+ public void testExistingPropertySerializationAnimals() throws Exception
+ {
+ Map<String,Object> result = writeAndMap(MAPPER, beelzebub);
+ assertEquals(3, result.size());
+ assertEquals(beelzebub.name, result.get("name"));
+ assertEquals(beelzebub.furColor, result.get("furColor"));
+ assertEquals(beelzebub.getType(), result.get("type"));
+
+ result = writeAndMap(MAPPER, rover);
+ assertEquals(3, result.size());
+ assertEquals(rover.name, result.get("name"));
+ assertEquals(rover.boneCount, result.get("boneCount"));
+ assertEquals(rover.getType(), result.get("type"));
+
+ String beelzebubSerialized = MAPPER.writeValueAsString(beelzebub);
+ assertEquals(beelzebubSerialized, beelzebubJson);
+
+ String roverSerialized = MAPPER.writeValueAsString(rover);
+ assertEquals(roverSerialized, roverJson);
+
+ String animalWrapperSerialized = MAPPER.writeValueAsString(beelzebubWrapper);
+ assertEquals(animalWrapperSerialized, beelzebubWrapperJson);
+
+ String animalListSerialized = MAPPER.writeValueAsString(animalList);
+ assertEquals(animalListSerialized, animalListJson);
+ }
+
+ /**
+ * Animals - deserialization tests for abstract method in base class
+ */
+ public void testSimpleClassAsExistingPropertyDeserializationAnimals() throws Exception
+ {
+ Animal beelzebubDeserialized = MAPPER.readValue(beelzebubJson, Animal.class);
+ assertTrue(beelzebubDeserialized instanceof Cat);
+ assertSame(beelzebubDeserialized.getClass(), Cat.class);
+ assertEquals(beelzebub.name, beelzebubDeserialized.name);
+ assertEquals(beelzebub.furColor, ((Cat) beelzebubDeserialized).furColor);
+ assertEquals(beelzebub.getType(), beelzebubDeserialized.getType());
+
+ AnimalWrapper beelzebubWrapperDeserialized = MAPPER.readValue(beelzebubWrapperJson, AnimalWrapper.class);
+ Animal beelzebubExtracted = beelzebubWrapperDeserialized.animal;
+ assertTrue(beelzebubExtracted instanceof Cat);
+ assertSame(beelzebubExtracted.getClass(), Cat.class);
+ assertEquals(beelzebub.name, beelzebubExtracted.name);
+ assertEquals(beelzebub.furColor, ((Cat) beelzebubExtracted).furColor);
+ assertEquals(beelzebub.getType(), beelzebubExtracted.getType());
+
+ @SuppressWarnings("unchecked")
+ List<Animal> animalListDeserialized = MAPPER.readValue(animalListJson, List.class);
+ assertNotNull(animalListDeserialized);
+ assertTrue(animalListDeserialized.size() == 2);
+ Animal cat = MAPPER.convertValue(animalListDeserialized.get(0), Animal.class);
+ assertTrue(cat instanceof Cat);
+ assertSame(cat.getClass(), Cat.class);
+ Animal dog = MAPPER.convertValue(animalListDeserialized.get(1), Animal.class);
+ assertTrue(dog instanceof Dog);
+ assertSame(dog.getClass(), Dog.class);
+ }
+
+
+ /**
+ * Cars - serialization tests for no abstract method or type variable in base class
+ */
+ public void testExistingPropertySerializationCars() throws Exception
+ {
+ Map<String,Object> result = writeAndMap(MAPPER, camry);
+ assertEquals(3, result.size());
+ assertEquals(camry.name, result.get("name"));
+ assertEquals(camry.exteriorColor, result.get("exteriorColor"));
+ assertEquals(camry.getType(), result.get("type"));
+
+ result = writeAndMap(MAPPER, accord);
+ assertEquals(3, result.size());
+ assertEquals(accord.name, result.get("name"));
+ assertEquals(accord.speakerCount, result.get("speakerCount"));
+ assertEquals(accord.getType(), result.get("type"));
+
+ String camrySerialized = MAPPER.writeValueAsString(camry);
+ assertEquals(camrySerialized, camryJson);
+
+ String accordSerialized = MAPPER.writeValueAsString(accord);
+ assertEquals(accordSerialized, accordJson);
+
+ String carWrapperSerialized = MAPPER.writeValueAsString(camryWrapper);
+ assertEquals(carWrapperSerialized, camryWrapperJson);
+
+ String carListSerialized = MAPPER.writeValueAsString(carList);
+ assertEquals(carListSerialized, carListJson);
+ }
+
+ /**
+ * Cars - deserialization tests for no abstract method or type variable in base class
+ */
+ public void testSimpleClassAsExistingPropertyDeserializationCars() throws Exception
+ {
+ Car camryDeserialized = MAPPER.readValue(camryJson, Camry.class);
+ assertTrue(camryDeserialized instanceof Camry);
+ assertSame(camryDeserialized.getClass(), Camry.class);
+ assertEquals(camry.name, camryDeserialized.name);
+ assertEquals(camry.exteriorColor, ((Camry) camryDeserialized).exteriorColor);
+ assertEquals(camry.getType(), ((Camry) camryDeserialized).getType());
+
+ CarWrapper camryWrapperDeserialized = MAPPER.readValue(camryWrapperJson, CarWrapper.class);
+ Car camryExtracted = camryWrapperDeserialized.car;
+ assertTrue(camryExtracted instanceof Camry);
+ assertSame(camryExtracted.getClass(), Camry.class);
+ assertEquals(camry.name, camryExtracted.name);
+ assertEquals(camry.exteriorColor, ((Camry) camryExtracted).exteriorColor);
+ assertEquals(camry.getType(), ((Camry) camryExtracted).getType());
+
+ @SuppressWarnings("unchecked")
+ List<Car> carListDeserialized = MAPPER.readValue(carListJson, List.class);
+ assertNotNull(carListDeserialized);
+ assertTrue(carListDeserialized.size() == 2);
+ Car camry = MAPPER.convertValue(carListDeserialized.get(0), Car.class);
+ assertTrue(camry instanceof Camry);
+ assertSame(camry.getClass(), Camry.class);
+ Car accord = MAPPER.convertValue(carListDeserialized.get(1), Car.class);
+ assertTrue(accord instanceof Accord);
+ assertSame(accord.getClass(), Accord.class);
+ }
+
+
+}