Deserialisieren einer Klasse mit überladenen Konstruktoren mithilfe von JsonCreator

82

Ich versuche, eine Instanz dieser Klasse mit Jackson 1.9.10 zu deserialisieren:

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

Wenn ich das versuche, bekomme ich folgendes

Widersprüchliche eigenschaftsbasierte Ersteller: hatten bereits ... {Schnittstelle org.codehaus.jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}], angetroffen ..., Anmerkungen: {Schnittstelle org.codehaus. jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}]

Gibt es eine Möglichkeit, eine Klasse mit überladenen Konstruktoren mit Jackson zu deserialisieren?

Vielen Dank

geejay
quelle
4
Wie die Antwort zeigt, müssen Sie einen einzigen Konstruktor angeben. Lassen Sie in Ihrem Fall dasjenige, das mehrere Argumente akzeptiert, das funktioniert einwandfrei. "Fehlende" Argumente nehmen null (für Objekte) oder einen Standardwert (für Grundelemente) an.
StaxMan
Vielen Dank. Das Zulassen mehrerer Konstruktoren wäre jedoch eine nette Funktion. Eigentlich ist mein Beispiel ein bisschen erfunden. Das Objekt, das ich verwenden möchte, hat tatsächlich völlig andere Argumentlisten, eine wird normal erstellt, die andere wird mit einem Throwable erstellt ... Ich werde sehen, was ich tun kann, vielleicht mit einem leeren Konstruktor und einem Getter / Setter für das Werfen
geejay
Ja, ich bin sicher, es wäre schön, aber Regeln können mit unterschiedlichen Permutationen ziemlich komplex werden. Es ist immer möglich, RFEs für neue Funktionen und Merkmale einzureichen.
StaxMan

Antworten:

118

Obwohl es nicht richtig dokumentiert ist, können Sie nur einen Ersteller pro Typ haben. Sie können so viele Konstruktoren in Ihrem Typ haben, wie Sie möchten, aber nur einer von ihnen sollte eine @JsonCreatorAnmerkung enthalten.

Wahrnehmung
quelle
69

EDIT: Siehe, in einem Blog-Beitrag der Betreuer von Jackson scheint es, dass 2.12 Verbesserungen in Bezug auf die Konstruktorinjektion sehen könnte. (Die aktuelle Version zum Zeitpunkt dieser Bearbeitung ist 2.11.1)

Verbessern Sie die automatische Erkennung von Konstruktorerstellern, einschließlich der Lösung / Linderung von Problemen mit mehrdeutigen 1-Argument-Konstruktoren (Delegieren gegen Eigenschaften).


Dies gilt weiterhin für Jackson Databind 2.7.0.

Die Jackson @JsonCreatorAnmerkung 2.5 javadoc oder Jackson Anmerkungen Dokumentation Grammatik ( Konstruktor s und Factory - Methode s ) können in der Tat glauben , dass man markieren mehrere Konstrukteure.

Markierungsanmerkung, mit der Konstruktoren und Factory-Methoden definiert werden können, um neue Instanzen der zugeordneten Klasse zu instanziieren.

Wenn man sich den Code ansieht, in dem die Ersteller identifiziert werden, sieht es so aus, als würde Jackson überladene KonstruktorenCreatorCollector ignorieren, da nur das erste Argument des Konstruktors überprüft wird .

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 ist der erste identifizierte Konstruktorersteller.
  • newOne ist der überladene Konstruktorersteller.

Das bedeutet, dass ein solcher Code nicht funktioniert

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

Aber dieser Code wird funktionieren:

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

Dies ist ein bisschen hacky und möglicherweise nicht zukunftssicher .


Die Dokumentation ist vage, wie die Objekterstellung funktioniert. Nach dem, was ich aus dem Code entnehme, ist es jedoch möglich, verschiedene Methoden zu mischen:

Zum Beispiel kann eine statische Factory-Methode mit Anmerkungen versehen werden @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");

Es funktioniert, aber es ist nicht ideal. Am Ende könnte es sinnvoll sein, z. B. wenn der JSON so dynamisch ist, sollte man vielleicht versuchen, einen Delegate-Konstruktor zu verwenden, um Nutzlastvariationen viel eleganter zu handhaben als mit mehreren kommentierten Konstruktoren.

Beachten Sie auch, dass Jackson die Ersteller nach Priorität ordnet , beispielsweise in diesem Code:

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

Dieses Mal wird Jackson keinen Fehler auslösen, aber Jackson wird nur den Delegate- Konstruktor verwenden Phone(Map<String, Object> properties), das heißt, der Phone(@JsonProperty("value") String value)wird nie verwendet.

Brice
quelle
7
IMHO sollte dies die akzeptierte Antwort sein, da es eine vollständige Erklärung mit einem schönen Beispiel liefert
matiou
7

Wenn ich richtig verstanden habe, was Sie erreichen möchten, können Sie es ohne Konstruktorüberlastung lösen .

Wenn Sie nur Nullwerte in die Attribute einfügen möchten, die in einem JSON oder einer Map nicht vorhanden sind, können Sie Folgendes tun:

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

Das war mein Fall, als ich Ihre Frage fand. Ich habe einige Zeit gebraucht, um herauszufinden, wie ich es lösen kann. Vielleicht war es das, was Sie versucht haben. @ Brice-Lösung hat bei mir nicht funktioniert.

Tiago Stapenhorst Martins
quelle
1
Beste Antwort imho
Jakob
3

Wenn es Ihnen nichts ausmacht, etwas mehr Arbeit zu erledigen, können Sie die Entität manuell deserialisieren:

@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
quelle