Home > Blockchain >  Using Jackson, how can I deserialize values using static factory methods that return wrappers with a
Using Jackson, how can I deserialize values using static factory methods that return wrappers with a

Time:01-09

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.

  • Related