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.
CodePudding user response:
One solution, add a DTO:
public class RequestDTO {
@JsonValue
String person;
@JsonValue
Integer score;
/**
* @return the person
*/
public String getPerson() {
return person;
}
/**
* @return the score
*/
public Integer getScore() {
return score;
}
/**
* @param person the person to set
*/
public void setPerson(String person) {
this.person = person;
}
/**
* @param score the score to set
*/
public void setScore(Integer score) {
this.score = score;
}
public RequestDTO() {
}
public RequestDTO(String person, Integer score) {
this.person = person;
this.score = score;
}
}
And change Request definition to use Mode.DELEGATING
public class Request {
// I want to deserialize into these fields
@JsonProperty Wrapped<Person> person;
@JsonProperty Wrapped<Score> score;
@JsonCreator(mode=Mode.DELEGATING)
public static Request createWrapped(RequestDTO requestDTO) {
// complex construction of the wrapped person
Request req = new Request();
req.person = new Wrapped<>(new Person(requestDTO.getPerson()));
req.score = new Wrapped<>(new Score(requestDTO.getScore()));
return req ;
}
}
CodePudding user response:
@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 = WrappedPersonDeserializer.class)
Wrapped<Person> person;
@JsonDeserialize(converter = WrappedScoreDeserializer.class)
Wrapped<Score> score;
}
class PersonConverter
extends StdConverter<String, Wrapped<Person>> {
@Override
public Wrapped<Person> convert(String value) {
return Person.createWrapped(value);
}
}
class ScoreConverter
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.