At SportsLabs we regularly encounter proprietary, non-standard APIs and formats. Our job is to integrate with these APIs, normalize them, and distribute the data in web- and mobile-friendly web services.

One of the scenarios we often encounter is a provider supplying multiple resource JSON-based APIs that share a lot of the same data in their responses, but without any particular field dedicated to identying the type of the resource e.g.

{
...
	"common": "a common field within multiple resource responses",
	"one": "one is a field only within this response type"
...
}

and

{
...
	"common": "a common field within multiple resource responses",
	"two": "two is a field only within this response type"
...
}

Instead of mapping 1-to-1 with these APIs, we often try to follow DRY principles and model them as implementations of a common polymorphic abstraction.

When using Jackson for polmorphic deserialization and not being in control of the API response data, the lack of any kind of type identifier requires a different solution.

One of the ways we’ve addressed this problem is to identify fields and properties that are unique to a particular resource API’s response.

We then add this field to a registry of known unique-property-to-type mappings and then, during deserialization, lookup the response’s field names to see if any of them are stored within the registry.

Planning out the solution, there were a couple of things I wanted to incorporate:

  • the deserializer should be initialized with the abstract class representing the shared response data
  • a register(String uniqueProperty, Class<? extends T> clazz) method will add the field-name-to-concrete-class mapping to the registry
  • the custom deserializer would be added to a Jackson SimpleModule which in turn would be registered with the ObjectMapper.
package org.springframework.social.dto.ser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Deserializes documents without a specific field designated for Polymorphic Type
* identification, when the document contains a field registered to be unique to that type
*
* @author robin
*/
public class UniquePropertyPolymorphicDeserializer<T> extends StdDeserializer<T> {
private static final long serialVersionUID = 1L;
// the registry of unique field names to Class types
private Map<String, Class<? extends T>> registry;
public UniquePropertyPolymorphicDeserializer(Class<T> clazz) {
super(clazz);
registry = new HashMap<String, Class<? extends T>>();
}
public void register(String uniqueProperty, Class<? extends T> clazz) {
registry.put(uniqueProperty, clazz);
}
/* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Class<? extends T> clazz = null;
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode obj = (ObjectNode) mapper.readTree(jp);
Iterator<Entry<String, JsonNode>> elementsIterator = obj.fields();
while (elementsIterator.hasNext()) {
Entry<String, JsonNode> element = elementsIterator.next();
String name = element.getKey();
if (registry.containsKey(name)) {
clazz = registry.get(name);
break;
}
}
if (clazz == null) {
throw ctxt.mappingException("No registered unique properties found for polymorphic deserialization");
}
return mapper.treeToValue(obj, clazz);
}
}

The deserialize method reads each of the fields in the response and looks up the registry to see if it is present. If it finds a match, the mapper.treeToValue method is invoked with the response object and the mapped class returned by the registry. If no match is found an exception is thrown.

For the unit test, I created an inner static abstract class AbstractTestObject (containing shared data) with two concrete implementations (TestObjectOne and TestObjectTwo) that each contain a property unique to that type.

The test also contains a inner class TestObjectDeserializer that extends UniquePropertyPolymorphicDeserializer. The test’s setUp method:

  • initializes the custom deserializer,
  • registers the unique-field-name-to-type mappings, and
  • adds the custom deserializer to a new SimpleModule which is registered with the ObjectMapper in turn.
package org.springframework.social.dto.ser;
import java.io.IOException;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
/**
* Test that documents without a specific field designated for Polymorphic Type
* identification can be deserialized with {@link UniquePropertyPolymorphicDeserializer},
* when the document contains a field registered to be unique to that type
*
* @author robin
*/
@SuppressWarnings("unused")
public class UniquePropertyPolymorphicDeserializerTest {
private static final String ONE = "one";
private static final String TWO = "two";
private static final String THREE = "three";
private ObjectMapper mapper;
/*
* Example extension of UniquePropertyPolymorphicDeserializer
*/
private class TestObjectDeserializer extends UniquePropertyPolymorphicDeserializer<AbstractTestObject> {
private static final long serialVersionUID = 1L;
// Register the abstract class that doesn't provide type information for child implementations
public TestObjectDeserializer() {
super(AbstractTestObject.class);
}
}
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
mapper = new ObjectMapper();
/*
* Register unique field names to TestObject types
*/
TestObjectDeserializer deserializer = new TestObjectDeserializer();
deserializer.register(ONE, TestObjectOne.class); // if "one" field is present, then it's a TestObjectOne
deserializer.register(TWO, TestObjectTwo.class); // if "two" field is present, then it's a TestObjectTwo
// Add and register the UniquePropertyPolymorphicDeserializer to the Jackson module
SimpleModule module = new SimpleModule("PolymorphicTestObjectDeserializer",
new Version(1, 0, 0, null, "com.sportslabs.amp", "spring-social-bootstrap"));
module.addDeserializer(AbstractTestObject.class, deserializer);
mapper.registerModule(module);
}
/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
mapper = null;
}
@Test
public void deserialize_WithRegisteredFieldNameOne_CreatesATestObjectOne() throws JsonParseException, IOException {
AbstractTestObject testObject = deserializeJsonToTestObjectWithProvidedFieldValueName(ONE);
Assert.assertTrue(testObject.getClass().isAssignableFrom(TestObjectOne.class));
}
@Test
public void deserialize_WithRegisteredFieldNameTwo_CreatesATestObjectTwo() throws JsonParseException, IOException {
AbstractTestObject testObject = deserializeJsonToTestObjectWithProvidedFieldValueName(TWO);
Assert.assertTrue(testObject.getClass().isAssignableFrom(TestObjectTwo.class));
}
@Test(expected=JsonMappingException.class)
public void deserialize_WithUnregisteredFieldName_ThrowsException() throws JsonParseException, IOException {
deserializeJsonToTestObjectWithProvidedFieldValueName(THREE);
}
private AbstractTestObject deserializeJsonToTestObjectWithProvidedFieldValueName(String field) throws IOException, JsonParseException, JsonProcessingException {
return mapper.readValue("{\"common\": \"value\", \"" + field + "\": \"value\"}", AbstractTestObject.class);
}
/*
* The abstract class that doesn't provide type information for its child implementations
*/
private static abstract class AbstractTestObject {
private final String common = "value";
public String getCommon() {
return common;
}
}
private static class TestObjectOne extends AbstractTestObject {
private final String one = "value";
public String getOne() {
return one;
}
}
private static class TestObjectTwo extends AbstractTestObject {
private final String two = "value";
public String getTwo() {
return two;
}
}
}

If others have solved this problem differently, I’d love to hear about it!