web-dev-qa-db-fra.com

Comment désérialiser une classe avec des constructeurs surchargés à l'aide de JsonCreator

J'essaie de désérialiser une instance de cette classe à l'aide de 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
}
}

Quand j'essaye ça j'obtiens ce qui suit

Créateurs en propriété en conflit: avaient déjà ... {interface org.codehaus.jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}], rencontrés ..., annotations: {interface org.codehaus. jackson.annotate.JsonCreator @ org.codehaus.jackson.annotate.JsonCreator ()}]

Existe-t-il un moyen de désérialiser une classe avec des constructeurs surchargés utilisant Jackson?

Merci

71
geejay

Bien que cela ne soit pas correctement documenté, vous ne pouvez avoir qu'un seul créateur par type. Vous pouvez avoir autant de constructeurs que vous le souhaitez dans votre type, mais un seul d'entre eux doit avoir un @JsonCreator annotation dessus.

105
Perception

Cela reste vrai pour Jackson databind 2.7.0.

The Jackson @JsonCreator Annotation 2.5 javadoc ou documentation sur les annotations de Jackson grammaire ( constructeur s et méthode d'usine s) supposons en effet que l'on peut marquer plusieurs constructeurs.

Les annotations de marqueur peuvent être utilisées pour définir les constructeurs et les méthodes de fabrique comme méthodes à utiliser pour instancier de nouvelles instances de la classe associée.

En regardant le code où les créateurs sont identifiés, il semble que Jackson CreatorCollector ignore constructeurs surchargés car il ne fait que vérifie la premier argument du constructeur .

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 est le premier constructeur identifié.
  • newOne est le créateur du constructeur surchargé.

Cela signifie que le code comme ça ne fonctionnera pas

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

Mais ce code fonctionnera:

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

Ceci est un peu hacky et peut ne pas être une preuve pour le futur.


La documentation est vague sur le fonctionnement de la création d'objet; D'après ce que je comprends du code, il est possible de mélanger différentes méthodes:

Par exemple, une méthode de fabrique statique peut être annotée avec @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");

Cela fonctionne mais ce n’est pas idéal. En fin de compte, cela pourrait avoir un sens, par exemple si json est que dynamic, alors vous devriez peut-être utiliser un constructeur délégué pour gérer les variations de charge utile de manière beaucoup plus élégante que avec plusieurs constructeurs annotés.

Notez également que Jackson commande les créateurs par priorité , par exemple dans ce 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");

Cette fois, Jackson ne générera pas d'erreur, mais n'utilisera que le constructeur delegatePhone(Map<String, Object> properties), ce qui signifie que Phone(@JsonProperty("value") String value) n'est jamais utilisé.

53
Brice

Si j'ai bien compris ce que vous essayez d'atteindre, vous pouvez le résoudre sans surcharger le constructeur .

Si vous souhaitez simplement mettre des valeurs NULL dans les attributs non présents dans un JSON ou une carte, vous pouvez procéder comme suit:

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

C'était mon cas lorsque j'ai trouvé votre question. Il m'a fallu du temps pour comprendre comment résoudre ce problème, c'est peut-être ce que vous aviez envie de faire. @ solution de Brice n'a pas fonctionné pour moi.

Si cela ne vous dérange pas de faire un peu plus de travail, vous pouvez désérialiser l'entité manuellement:

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