Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save robinhowlett/ce45e575197060b8392d to your computer and use it in GitHub Desktop.
Save robinhowlett/ce45e575197060b8392d to your computer and use it in GitHub Desktop.
Custom Jackson Polymorphic Deserialization without Type Metadata
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);
}
}
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