Jackson deserialize based on property name
Jackson doesn't offer an out of the box solution for that, but it doesn't mean that you are out of luck.
Assuming that your classes implement a common interface or extend a common class, as shown below:
public interface Animal {}
public class Dog implements Animal { private String bark; // Default constructor, getters and setters}
public class Cat implements Animal { private String meow; // Default constructor, getters and setters}
You can create a custom deserializer based on the property name. It allows you to define a unique property that will be used to look up the class to perform the deserialization to:
public class PropertyBasedDeserializer<T> extends StdDeserializer<T> { private Map<String, Class<? extends T>> deserializationClasses; public PropertyBasedDeserializer(Class<T> baseClass) { super(baseClass); deserializationClasses = new HashMap<String, Class<? extends T>>(); } public void register(String property, Class<? extends T> deserializationClass) { deserializationClasses.put(property, deserializationClass); } @Override public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { ObjectMapper mapper = (ObjectMapper) p.getCodec(); JsonNode tree = mapper.readTree(p); Class<? extends T> deserializationClass = findDeserializationClass(tree); if (deserializationClass == null) { throw JsonMappingException.from(ctxt, "No registered unique properties found for polymorphic deserialization"); } return mapper.treeToValue(tree, deserializationClass); } private Class<? extends T> findDeserializationClass(JsonNode tree) { Iterator<Entry<String, JsonNode>> fields = tree.fields(); Class<? extends T> deserializationClass = null; while (fields.hasNext()) { Entry<String, JsonNode> field = fields.next(); String property = field.getKey(); if (deserializationClasses.containsKey(property)) { deserializationClass = deserializationClasses.get(property); break; } } return deserializationClass; }}
Then instantiate and configure the deserializer:
PropertyBasedDeserializer<Animal> deserializer = new PropertyBasedDeserializer<>(Animal.class);deserializer.register("bark", Dog.class); // If "bark" is present, then it's a Dogdeserializer.register("meow", Cat.class); // If "meow" is present, then it's a Cat
Add it to a module:
SimpleModule module = new SimpleModule("custom-deserializers", Version.unknownVersion());module.addDeserializer(Animal.class, deserializer);
Register the module and perform the deserialization as usual:
ObjectMapper mapper = new ObjectMapper();mapper.registerModule(module);String json = "[{\"bark\":\"bowwow\"}, {\"bark\":\"woofWoof\"}, {\"meow\":\"meeeOwww\"}]";List<Animal> animals = mapper.readValue(json, new TypeReference<List<Animal>>() { });
With fasterxml jackson
, you can do this:
abstract class FooOrBar { companion object { @JvmStatic @JsonCreator private fun creator(json: Map<String, String>): FooOrBar? { return when { json.containsKey("foo") -> Foo(json["foo"] as String) json.containsKey("bar") -> Foo(json["bar"] as String) else -> null } } }}class Foo(val foo: String) : FooOrBar() // even can use map delegate if you know what it isclass Bar(val bar: String) : FooOrBar()
It is Kotlin, but you will get the idea.
Note the@JsonCreator
is used. the annotated creator
function has single argument (which is one kind of the two signatures required by JsonCreator
), JSON is deserialized as a Map
instance and passed to the creator
. From here, you can create your class instance.
----------------UPDATE-------------------------
You can also use JsonNode
for the creator
function for nested and complex JSON.
private fun creator(json: JsonNode): FooOrBar?
You'll have to tell jackson what class you expect:
Foo readValue = mapper.readValue(json, Foo.class);
Bar readValue = mapper.readValue(json, Bar.class);
Otherwise it may be worth using XML in this case if you strong types are necessary for your design.