Created
March 19, 2015 17:17
-
-
Save robinhowlett/ce45e575197060b8392d to your computer and use it in GitHub Desktop.
Custom Jackson Polymorphic Deserialization without Type Metadata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment