As other have pointed out, there's no consensus on how it should work so it hasn't been implemented.
If you have classes Foo, Bar and their parent FooBar solution seems pretty obvious when you have JSONs like:
{
"foo":<value>
}
or
{
"bar":<value>
}
but there's no common answer to what happens when you get
{
"foo":<value>,
"bar":<value>
}
At first glance last example seems like an obvious case of 400 Bad Request but there are many different approaches in practice:
- Handle it as 400 Bad Request
- Precedence by type/field (for example if field error exists it has higher precedence than some other field foo)
- More complex cases of 2.
My current solution which works for most cases and tries to leverage as much of existing Jackson infrastructure as possible is (you only need 1 deserializer per hierarchy):
public class PresentPropertyPolymorphicDeserializer<T> extends StdDeserializer<T> {
private final Map<String, Class<?>> propertyNameToType;
public PresentPropertyPolymorphicDeserializer(Class<T> vc) {
super(vc);
this.propertyNameToType = Arrays.stream(vc.getAnnotation(JsonSubTypes.class).value())
.collect(Collectors.toMap(Type::name, Type::value,
(a, b) -> a, LinkedHashMap::new)); // LinkedHashMap to support precedence case by definition order
}
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
ObjectNode object = objectMapper.readTree(p);
for (String propertyName : propertyNameToType.keySet()) {
if (object.has(propertyName)) {
return deserialize(objectMapper, propertyName, object);
}
}
throw new IllegalArgumentException("could not infer to which class to deserialize " + object);
}
@SuppressWarnings("unchecked")
private T deserialize(ObjectMapper objectMapper,
String propertyName,
ObjectNode object) throws IOException {
return (T) objectMapper.treeToValue(object, propertyNameToType.get(propertyName));
}
}
Example usage:
@JsonSubTypes({
@JsonSubTypes.Type(value = Foo.class, name = "foo"),
@JsonSubTypes.Type(value = Bar.class, name = "bar"),
})
interface FooBar {
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Foo implements FooBar {
private final String foo;
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Bar implements FooBar {
private final String bar;
}
Jackson configuration
SimpleModule module = new SimpleModule();
module.addDeserializer(FooBar.class, new PresentPropertyPolymorphicDeserializer<>(FooBar.class));
objectMapper.registerModule(module);
or if you're using Spring Boot:
@JsonComponent
public class FooBarDeserializer extends PresentPropertyPolymorphicDeserializer<FooBar> {
public FooBarDeserializer() {
super(FooBar.class);
}
}
Tests:
@Test
void shouldDeserializeFoo() throws IOException {
// given
var json = "{\"foo\":\"foo\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Foo("foo"));
}
@Test
void shouldDeserializeBar() throws IOException {
// given
var json = "{\"bar\":\"bar\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Bar("bar"));
}
@Test
void shouldDeserializeUsingAnnotationDefinitionPrecedenceOrder() throws IOException {
// given
var json = "{\"bar\":\"\", \"foo\": \"foo\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Foo("foo"));
}
EDIT: I've added support for this case and more in this project.
PropertyPresentDeserializer
seems to be a good thing. However, it does not seem to be included in Jackson. See GitHub search results: github.com/… – Cavafy