Джексон, deserialize класс с частными полями и arg-конструктор без аннотаций

Можно десериализовать в класс с закрытыми полями и собственным конструктором аргумента, не используя аннотации и не модифицируя класс, используя Джексона?

Я знаю, что это возможно в Джексоне при использовании этой комбинации: 1) Java 8, 2) компилировать с опцией "-parameters" и 3) имена параметров соответствуют JSON. Но это также возможно в GSON по умолчанию без всех этих ограничений.

Например:

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public static void main(String[] args) throws IOException {
        String json = "{firstName: \"Foo\", lastName: \"Bar\", age: 30}";

        System.out.println("GSON: " + deserializeGson(json)); // works fine
        System.out.println("Jackson: " + deserializeJackson(json)); // error
    }

    public static Person deserializeJackson(String json) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper.readValue(json, Person.class);
    }

    public static Person deserializeGson(String json) {
        Gson gson = new GsonBuilder().create();
        return gson.fromJson(json, Person.class);
    }
}

Который отлично работает для GSON, но Джексон бросает:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of 'jacksonParametersTest.Person' (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{firstName: "Foo", lastName: "Bar", age: 30}"; line: 1, column: 2]
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)

Это возможно в GSON, поэтому я ожидаю, что в Джексоне должен быть какой-то путь без изменения класса Person, без Java 8 и без явного пользовательского десериализатора. Кто-нибудь знает решение?

- Обновление, дополнительная информация

Gson, кажется, пропускает конструктор аргументов, поэтому он должен создавать конструктор без аргументов за кулисами, используя отражения.

Также существует модуль Kotlin Jackson, который может сделать это для классов данных Kotlin, даже без флага компилятора "-parameters". Так что странно, что такого решения, похоже, не существует для Java Jackson.

Это (приятное и чистое) решение, доступное в Kotlin Jackson (которое IMO должно также стать доступным в Java Jackson через пользовательский модуль):

val mapper = ObjectMapper()
    .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
    .registerModule(KotlinModule())     

val person: Person = mapper.readValue(json, Person::class.java)

Ответ 1

Решение с дополнительными аннотациями

Вы можете использовать смешанные аннотации. Это отличная альтернатива, когда изменение классов не вариант. Вы можете думать об этом как о некотором аспектно-ориентированном способе добавления большего количества аннотаций во время выполнения, чтобы дополнить статически определенные.

Предполагая, что ваш класс Person определен следующим образом:

public class Person {

    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    // Getters omitted
}

Сначала определите смешанный аннотационный абстрактный класс:

public abstract class PersonMixIn {

    PersonMixIn(@JsonProperty("firstName") String firstName,
                @JsonProperty("lastName") String lastName,
                @JsonProperty("age") int age) {
    }
}

Затем настройте ObjectMapper для использования определенного класса в качестве дополнения для вашего POJO:

ObjectMapper mapper = new ObjectMapper();
mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
mapper.addMixIn(Person.class, PersonMixIn.class);

И десериализовать JSON:

String json = "{firstName: \"Foo\", lastName: \"Bar\", age: 30}";
Person person = mapper.readValue(json, Person.class);

Ответ 2

Поскольку не существует конструктора по умолчанию, джексон или gson хотят создать экземпляр там самостоятельно. вы должны сообщить API, как создать такой экземпляр, предоставив custom deserialize.

здесь код фрагмента

public class PersonDeserializer extends StdDeserializer<Person> { 
    public PersonDeserializer() {
        super(Person.class);
    } 

    @Override
    public Person deserialize(JsonParser jp, DeserializationContext ctxt) 
            throws IOException, JsonProcessingException {
        try {
            final JsonNode node = jp.getCodec().readTree(jp);
            final ObjectMapper mapper = new ObjectMapper();
            final Person person = (Person) mapper.readValue(node.toString(),
                    Person.class);
            return person;
        } catch (final Exception e) {
            throw new IOException(e);
        }
    }
}

Затем зарегистрируйте простой модуль для обработки вашего типа

final ObjectMapper mapper = jacksonBuilder().build();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new PersonDeserializer());

Ответ 3

Jackson предоставляет модуль jackson-modules-java8 для решения вашей проблемы.

Вы должны создать свой ObjectMapper следующим образом:

ObjectMapper mapper = new ObjectMapper()
        .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
        .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));

Вы должны добавить -parameters в качестве аргумента компилятора.

Пример для maven:

 <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-compiler-plugin</artifactId>
       <version>3.5.1</version>
       <configuration>
           <!--somecode-->

           <compilerArgument>-parameters</compilerArgument>

       </configuration>
 </plugin>

Для gradle:

compileJava {
  options.compilerArgs << "-parameters"
}

Ответ 4

Только если вы можете изменить реализацию своего класса, приведенное ниже решение работает

Простое решение - просто создать конструктор по умолчанию

Person() в классе Person

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person() {
    }

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    ...
}