Jak deserializować klasę z przeciążonymi konstruktorami przy użyciu JsonCreator

83

Próbuję deserializować instancję tej klasy przy użyciu Jacksona 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
}
}

Kiedy próbuję tego, otrzymuję następujące informacje

Sprzeczne twórcy oparte na właściwościach: już mieli ... {interfejs org.codehaus.jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}], napotkano ..., adnotacje: {interface org.codehaus. jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}]

Czy istnieje sposób na deserializację klasy z przeciążonymi konstruktorami przy użyciu Jacksona?

Dzięki

geejay
źródło
4
Jak wskazuje odpowiedź, nie, musisz określić jeden i jedyny konstruktor. W twoim przypadku zostaw ten, który przyjmuje wiele argumentów, to zadziała dobrze. „Brakujące” argumenty przyjmą wartość null (dla obiektów) lub wartość domyślną (dla prymitywów).
StaxMan
Dzięki. Zezwolenie na wiele konstruktorów byłoby jednak fajną funkcją. Właściwie mój przykład jest trochę wymyślony. Obiekt, którego próbuję użyć, ma zupełnie inne listy argumentów, jeden jest tworzony normalnie, drugi jest tworzony za pomocą Throwable ... Zobaczę, co mogę zrobić, może mając pusty konstruktor i getter / setter dla Throwable
geejay
Tak, jestem pewien, że byłoby miło, ale zasady mogą stać się dość skomplikowane w przypadku różnych permutacji. Zawsze można złożyć RFE o nowe funkcje, funkcje.
StaxMan

Odpowiedzi:

119

Chociaż nie jest to odpowiednio udokumentowane, możesz mieć tylko jednego twórcę na typ. Możesz mieć dowolną liczbę konstruktorów w swoim typie, ale tylko jeden z nich powinien mieć @JsonCreatoradnotację.

Postrzeganie
źródło
72

EDYCJA : Oto w poście na blogu opiekunów Jacksona wygląda na to, że 2.12 może zobaczyć ulepszenia w odniesieniu do wstrzykiwania konstruktora. (Aktualna wersja w momencie tej edycji to 2.11.1)

Ulepsz automatyczne wykrywanie twórców konstruktorów, w tym rozwiązywanie / łagodzenie problemów z niejednoznacznymi konstruktorami 1-argumentowymi (delegowanie a właściwości)


To nadal jest prawdą w przypadku indeksu Jackson w wersji 2.7.0.

The Jackson @JsonCreatoradnotacja 2,5 javadoc lub Jacksona adnotacje dokumentacja gramatyki ( konstruktor s i metoda fabryki s ) Pozwól uwierzyć, że rzeczywiście można oznaczyć wielu konstruktorów.

Adnotacja znacznika, której można użyć do zdefiniowania konstruktorów i metod fabrycznych jako jednej do użycia do tworzenia wystąpień nowych wystąpień skojarzonej klasy.

Patrząc na kod, w którym zidentyfikowani są twórcy , wygląda na to, że Jackson CreatorCollectorignoruje przeciążone konstruktory, ponieważ sprawdza tylko pierwszy argument konstruktora .

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 jest pierwszym zidentyfikowanym twórcą konstruktora.
  • newOne jest przeciążonym twórcą konstruktora.

Oznacza to, że taki kod nie zadziała

@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");

Ale ten kod zadziała:

@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");

Jest to trochę hakerskie i może nie być przyszłościowe .


Dokumentacja jest niejasna o tym, jak działa tworzenie obiektów; z tego, co wyciągam z kodu, wynika jednak, że można mieszać różne metody:

Na przykład można mieć statyczną metodę fabryki z adnotacją @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");

Działa, ale nie jest idealny. Ostatecznie może to mieć sens, np. Jeśli JSON jest tak dynamiczny, to może należałoby spróbować użyć konstruktora delegata do obsługi zmian ładunku znacznie bardziej elegancko niż w przypadku wielu konstruktorów z adnotacjami.

Zwróć też uwagę, że Jackson porządkuje twórców według priorytetów , na przykład w tym kodzie:

// 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");

Tym razem Jackson nie zgłosi błędu, ale Jackson użyje tylko konstruktora delegataPhone(Map<String, Object> properties) , co oznacza, że Phone(@JsonProperty("value") String value)nigdy nie zostanie użyty.

Brice
źródło
8
IMHO to powinna być akceptowana odpowiedź, ponieważ zapewnia pełne wyjaśnienie z ładnym przykładem
matiou
7

Jeśli mam rację, co próbujesz osiągnąć, możesz rozwiązać to bez przeciążania konstruktora .

Jeśli chcesz po prostu umieścić wartości null w atrybutach nieobecnych w JSON lub Mapie, możesz wykonać następujące czynności:

@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;
    }
}

To był mój przypadek, kiedy znalazłem twoje pytanie. Zajęło mi trochę czasu, zanim wymyśliłem, jak to rozwiązać, może to właśnie chciałeś zrobić. Rozwiązanie @Brice nie działa dla mnie.

Tiago Stapenhorst Martins
źródło
1
Best anwer imho
Jakob
3

Jeśli nie masz nic przeciwko wykonaniu trochę więcej pracy, możesz ręcznie deserializować encję:

@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");
            }
        }
    }
}
Malcolm Crum
źródło