Как десериализовать класс с перегруженными конструкторами с помощью 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
Как указано в ответе, нет, вы должны указать один-единственный конструктор. В вашем случае оставьте тот, который принимает несколько аргументов, он будет работать нормально. «Отсутствующие» аргументы будут иметь значение null (для объектов) или значение по умолчанию (для примитивов).
StaxMan
Благодарю. Однако было бы неплохо разрешить использование нескольких конструкторов. На самом деле мой пример немного надуманный. У объекта, который я пытаюсь использовать, на самом деле есть совершенно разные списки аргументов, один создается нормально, другой создается с помощью Throwable ... Я посмотрю, что я могу сделать, возможно, имея пустой конструктор и получатель / установщик для Throwable
geejay
Да, я уверен, что это было бы хорошо, но правила могут стать довольно сложными с различными перестановками. Всегда можно подать RFE для новых функций, возможностей.
StaxMan

Ответы:

119

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

Восприятие
источник
72

РЕДАКТИРОВАТЬ : вот, в сообщении в блоге разработчиков Джексона, кажется, что 2.12 может увидеть улучшения в отношении внедрения конструктора. (Текущая версия на момент редактирования - 2.11.1)

Улучшение автоматического обнаружения создателей конструкторов, включая решение / смягчение проблем с неоднозначными конструкторами с одним аргументом (делегирование по сравнению со свойствами)


Это все еще верно для базы данных Джексона 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
Лучший ответ, имхо
Якоб
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");
            }
        }
    }
}
Малькольм Крам
источник