Як десеріалізувати клас із перевантаженими конструкторами за допомогою JsonCreator


83

Я намагаюся десеріалізувати екземпляр цього класу за допомогою Jackson 1.9.10:

public class Person {

@JsonCreator
public Person(@JsonProperty("name") String name,
        @JsonProperty("age") int age) {
    // ... person with both name and age
}

@JsonCreator
public Person(@JsonProperty("name") String name) {
    // ... person with just a name
}
}

Коли я спробую це, я отримую наступне

Конфліктні творці, засновані на властивостях: уже мали ... {interface org.codehaus.jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}], зустрічалися ..., анотації: {interface org.codehaus. jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}]

Чи є спосіб десеріалізувати клас із перевантаженими конструкторами за допомогою Джексона?

Дякую


4
Як вказує відповідь, ні, ви повинні вказати один і єдиний конструктор. У вашому випадку залиште той, який бере кілька аргументів, і це буде справно працювати. "Відсутні" аргументи прийматимуть нуль (для Об'єктів) або значення за замовчуванням (для примітивів).
StaxMan

Дякую. Дозволити декілька конструкторів було б приємною особливістю. Насправді, мій приклад трохи надуманий. Об'єкт, який я намагаюся використовувати, насправді має абсолютно різні списки аргументів, один створюється нормально, інший створюється за допомогою Throwable ... Я подивлюся, що я можу зробити, можливо, маючи порожній конструктор та getter / setter для Кидальний
geejay

Так, я впевнений, що це було б непогано, але правила можуть стати досить складними з різними перестановками. Завжди можна подати RFE для отримання нових функціональних можливостей.
StaxMan

Відповіді:


119

Хоча це не належним чином задокументовано, ви можете мати лише одного автора для кожного типу. Ви можете мати стільки конструкторів, скільки захочете для вашого типу, але лише один із них повинен мати @JsonCreatorанотацію на ньому.


72

РЕДАГУВАТИ : Ось, у дописі блогу , який підтримують Джексон, здається, 2.12 може побачити покращення щодо введення конструктора. (Поточна версія на момент редагування - 2.11.1)

Покращення автоматичного виявлення творців конструктора, включаючи вирішення / полегшення проблем із двозначними конструкторами з 1 аргументом (делегування проти властивостей)


Це все ще справедливо для Jackson databind 2.7.0.

Джексон @JsonCreatorанотації 2,5 Javadoc або Джексон анотацій документації граматик ( конструктор s і завод метод s ) Нехай вважає , що на насправді можна відзначити кілька конструкторів.

Анотація маркера, яка може бути використана для визначення конструкторів та фабричних методів як одного для створення нових екземплярів відповідного класу.

Дивлячись на код, де ідентифікуються творці , схоже, що Джексон CreatorCollectorігнорує перевантажені конструктори, оскільки перевіряє лише перший аргумент конструктора .

Class<?> oldType = oldOne.getRawParameterType(0);
Class<?> newType = newOne.getRawParameterType(0);

if (oldType == newType) {
    throw new IllegalArgumentException("Conflicting "+TYPE_DESCS[typeIndex]
           +" creators: already had explicitly marked "+oldOne+", encountered "+newOne);
}
  • oldOne є першим визначеним творцем конструктора.
  • newOne - це перевантажений конструктор конструктора.

Це означає, що подібний код не працюватиме

@JsonCreator
public Phone(@JsonProperty("value") String value) {
    this.value = value;
    this.country = "";
}

@JsonCreator
public Phone(@JsonProperty("country") String country, @JsonProperty("value") String value) {
    this.value = value;
    this.country = country;
}

assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336"); // raise error here
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");

Але цей код буде працювати:

@JsonCreator
public Phone(@JsonProperty("value") String value) {
    this.value = value;
    enabled = true;
}

@JsonCreator
public Phone(@JsonProperty("enabled") Boolean enabled, @JsonProperty("value") String value) {
    this.value = value;
    this.enabled = enabled;
}

assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");

Це трохи невдало і може не бути доказом у майбутньому .


Документація нечітка щодо того, як працює створення об’єкта; з того, що я збираю з коду, це те, що можна поєднувати різні методи:

Наприклад, можна мати статичний заводський метод, анотований @JsonCreator

@JsonCreator
public Phone(@JsonProperty("value") String value) {
    this.value = value;
    enabled = true;
}

@JsonCreator
public Phone(@JsonProperty("enabled") Boolean enabled, @JsonProperty("value") String value) {
    this.value = value;
    this.enabled = enabled;
}

@JsonCreator
public static Phone toPhone(String value) {
    return new Phone(value);
}

assertThat(new ObjectMapper().readValue("\"+336\"", Phone.class).value).isEqualTo("+336");
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");

Це працює, але це не ідеально. Зрештою, це може мати сенс, наприклад, якщо JSON такий динамічний, то, можливо, слід поглянути на використання конструктора делегатів, щоб обробляти зміни корисного навантаження набагато елегантніше, ніж із декількома анотованими конструкторами.

Також зверніть увагу, що Джексон упорядковує творців за пріоритетом , наприклад у цьому коді:

// Simple
@JsonCreator
public Phone(@JsonProperty("value") String value) {
    this.value = value;
}

// more
@JsonCreator
public Phone(Map<String, Object> properties) {
    value = (String) properties.get("value");
    
    // more logic
}

assertThat(new ObjectMapper().readValue("\"+336\"", Phone.class).value).isEqualTo("+336");
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\"}", Phone.class).value).isEqualTo("+336");
assertThat(new ObjectMapper().readValue("{\"value\":\"+336\",\"enabled\":true}", Phone.class).value).isEqualTo("+336");

Цього разу Джексон не викличе помилки, але Джексон буде використовувати лише конструктор делегатівPhone(Map<String, Object> properties) , що означає, що Phone(@JsonProperty("value") String value)ніколи не використовується.


8
ІМХО це має бути прийнятою відповіддю, оскільки вона надає повне пояснення на гарному прикладі
matiou

7

Якщо я зрозумів правильно, чого ви намагаєтесь досягти, ви можете вирішити це без перевантаження конструктора .

Якщо ви просто хочете помістити нульові значення в атрибути, відсутні в JSON або Map, ви можете зробити наступне:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
    private String name;
    private Integer age;
    public static final Integer DEFAULT_AGE = 30;

    @JsonCreator
    public Person(
        @JsonProperty("name") String name,
        @JsonProperty("age") Integer age) 
        throws IllegalArgumentException {
        if(name == null)
            throw new IllegalArgumentException("Parameter name was not informed.");
        this.age = age == null ? DEFAULT_AGE : age;
        this.name = name;
    }
}

Це був мій випадок, коли я знайшов ваше запитання. Мені знадобився деякий час, щоб зрозуміти, як це вирішити, можливо, це те, що ти прагнув зробити. Рішення @Brice для мене не спрацювало.


1
Найкращий anwer imho
Якоб

3

Якщо ви не проти зробити трохи більше роботи, ви можете десеріалізувати сутність вручну:

@JsonDeserialize(using = Person.Deserializer.class)
public class Person {

    public Person(@JsonProperty("name") String name,
            @JsonProperty("age") int age) {
        // ... person with both name and age
    }

    public Person(@JsonProperty("name") String name) {
        // ... person with just a name
    }

    public static class Deserializer extends StdDeserializer<Person> {
        public Deserializer() {
            this(null);
        }

        Deserializer(Class<?> vc) {
            super(vc);
        }

        @Override
        public Person deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            JsonNode node = jp.getCodec().readTree(jp);
            if (node.has("name") && node.has("age")) {
                String name = node.get("name").asText();
                int age = node.get("age").asInt();
                return new Person(name, age);
            } else if (node.has("name")) {
                String name = node.get("name").asText();
                return new Person("name");
            } else {
                throw new RuntimeException("unable to parse");
            }
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.