I'm trying to implement a generic deserializer for all classes that extend from a certain abstract class or implement a certain interface. In this example I'm using interface StringConvertible
. I need to determine the concrete type and so that I can create an instance.
An old forum post by Programmer Bruce led me to using ContextDeserializer and it's working when the StringConvertible is a property in another class.
But when I want to deserialize the StringConvertible directly, I can't find a way to get the concrete type because the beanProperty
parameter is null
. Apparently that is expected, according to this question/answer at the Jackson JSON User Group:
The only case where property should be null is when serializing a "root value", meaning the object instance passed directly to ObjectMapper's (or ObjectWriter's) writeValue() method -- in this case there simply isn't a referring property. But otherwise it should always be passed.
See main method below for an example of both cases:
@JsonDeserialize(using = StringConvertibleDeserializer.class)
public final class SomeStringConvertible implements StringConvertible {
private final String value;
public SomeStringConvertible(final String value) {
this.value = value;
}
@Override
@JsonValue
public String stringValue() {
return value;
}
}
public final class SomeWrapper {
public SomeStringConvertible stringConvertible;
public SomeWrapper() {
}
}
public class StringConvertibleDeserializer extends StdDeserializer<StringConvertible> implements ContextualDeserializer {
private final Class<? extends StringConvertible> targetClass;
StringConvertibleDeserializer() {
super(StringConvertible.class);
this.targetClass = null;
}
StringConvertibleDeserializer(final Class<? extends StringConvertible> targetClass) {
super(StringConvertible.class);
this.targetClass = targetClass;
}
@Override
public JsonDeserializer<?> createContextual(final DeserializationContext deserializationContext, @Nullable final BeanProperty beanProperty)
throws JsonMappingException {
final StringConvertibleDeserializer contextualDeserializer;
// ==== Determine target type =====
final Class<? extends StringConvertible> targetClass;
JavaType type = beanProperty.getType(); // -> beanProperty is null when the StringConvertible type is a root value
targetClass = (Class<? extends StringConvertible>) type.getRawClass();
// ==== Create contextual deserializer =====
contextualDeserializer = new StringConvertibleDeserializer(targetClass);
// ==== Return =====
return contextualDeserializer;
}
@Override
public StringConvertible deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
final StringConvertible value;
// ==== Create instance using the target type =====
if (targetClass.equals(SomeStringConvertible.class))
value = new SomeStringConvertible(jsonParser.getText());
else {
throw new RuntimeException();
}
// ==== Return =====
return value;
}
}
public final class JacksonModule extends SimpleModule {
public JacksonModule() {
super();
addDeserializer(StringConvertible.class, new StringConvertibleDeserializer());
}
}
public final class Main {
public static void main(String[] args) {
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JacksonModule());
final String wrappedValueJSON = "{\"stringConvertible\":\"hello world\"}";
final String rootValueJSON = "\"hello world\"";
try {
mapper.readValue(wrappedValueJSON, SomeWrapper.class); // This works fine
mapper.readValue(rootValueJSON, SomeStringConvertible.class); // This causes a NPE in createContextual(...) because beanProperty is null
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Question: How can I get type concrete type in case of a root value? Or, if there is a better solution than this, what would you suggest instead?
When researching the solution StaxMan proposed I stumbled upon this Github issue for jackson-databind
which addresses the exact same problem. In response, the maintainer added a method to DeserializationContext
in version 2.5.0
:
This turned out relatively easy to implement, so now there is:
class DeserializationContext {
public JavaType getContextualType() { ... }
}
which will give expected type during call to createContextual(), including case of deserializers that are directly added via annotation.
So to make this work in my case, I just had to change some code in the createContextual(...)
method. I changed this:
// ==== Determine target type =====
final Class<? extends StringConvertible> targetClass;
JavaType type = beanProperty.getType(); // -> beanProperty is null when the StringConvertible type is a root value
targetClass = (Class<? extends StringConvertible>) type.getRawClass();
To this:
// ==== Determine target type =====
final Class<? extends StringConvertible> targetClass;
{
// ==== Get the contextual type info =====
final JavaType type;
if (beanProperty != null)
type = beanProperty.getType(); // -> beanProperty is null when the StringConvertible type is a root value
else {
type = deserializationContext.getContextualType();
}
// ==== Get raw Class from type info =====
targetClass = (Class<? extends StringConvertible>) type.getRawClass();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With