Using Jackson, I want to deserialize some values into generic wrapper objects for which I have a specific static factory method for each type.
However, Jackson does not seem to pick up on this layer of indirection, even if I annotate the factory methods with @JsonCreator
.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
Wrapped
(no Creators, like default constructor, exist): no String-argument constructor/factory method to deserialize from String value ('Carl')
How can I make Jackson use the factory methods that return wrappers with a generic type?
This self-contained code illustrates my problem:
class Request {
// I want to deserialize into these fields
@JsonProperty Wrapped<Person> person;
@JsonProperty Wrapped<Score> score;
}
class Wrapped<T> {
// This is my generic wrapper type.
// Its construction is non-trivial: it is impossible to first construct the value before wrapping it.
// Therefor, construction is performed by the factory methods of the concrete value classes (Person, Score, ...).
// Let's say for simplicity that it did have a simple constructor:
T value;
public Wrapped(T value) {
this.value = value;
}
}
class Person {
@JsonCreator
public static Wrapped<Person> createWrapped(String name) {
// complex construction of the wrapped person
return new Wrapped<>(new Person(name));
}
@JsonValue
String name;
public Person(String name) {
this.name = name;
}
}
class Score {
@JsonCreator
public static Wrapped<Score> createWrapped(int score) {
// complex construction of the wrapped score
return new Wrapped<>(new Score(score));
}
@JsonValue
int score;
public Score(int score) {
this.score = score;
}
}
class Example {
private static final String JSON_REQUEST =
"""
{
"person":"Carl",
"score":20
}
""";
public static void main(String[] args) throws Exception {
Request request = new ObjectMapper()
.readValue(JSON_REQUEST, Request.class);
System.out.println(request.person.value.name);
System.out.println(request.score.value.score);
}
}
It is important to note that type information is only in the java classes, it should not be in the json.
@p3consulting's answer sent me in the right direction, but it lead to something completely different.
Jackson has something called a Converter that does exactly what I want.
I created converters for each wrapped value type,
and then annotated the properties in the request to use those converters:
class Request {
@JsonDeserialize(converter = WrappedPersonConverter.class)
Wrapped<Person> person;
@JsonDeserialize(converter = WrappedScoreConverter.class)
Wrapped<Score> score;
}
class WrappedPersonConverter
extends StdConverter<String, Wrapped<Person>> {
@Override
public Wrapped<Person> convert(String value) {
return Person.createWrapped(value);
}
}
class WrappedScoreConverter
extends StdConverter<Integer, Wrapped<Score>> {
@Override
public Wrapped<Score> convert(Integer score) {
return Score.createWrapped(score);
}
}
For factory methods with more complex signatures, you can make this work by using a DTO, e.g.:
class WrappedPersonConverter2
extends StdConverter<WrappedPersonConverter2.DTO, Wrapped<Person>> {
@Override
public Wrapped<Person> convert(WrappedPersonConverter2.DTO dto) {
return Person.createWrapped(dto.first, dto.last);
}
public static class DTO {
public int first;
public int last;
}
}
I cannot believe this was so simple but took me so long to find.