javagenericsjacksonjackson-databindfactory-method

Using Jackson, how can I deserialize values using static factory methods that return wrappers with a generic type?


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.


Solution

  • @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.