Cómo deserializar una clase con constructores sobrecargados usando JsonCreator

6 minutos de lectura

Estoy tratando de deserializar una instancia de esta clase usando 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
}
}

Cuando pruebo esto me sale lo siguiente

Creadores basados ​​en propiedades en conflicto: ya tenía… {interfaz org.codehaus.jackson.annotate.JsonCreator @org.codehaus.jackson.annotate.JsonCreator()}], encontró… , anotaciones: {interfaz org.codehaus. jackson.annotate.JsonCreator @org.codehaus.jackson.annotate.JsonCreator()}]

¿Hay alguna manera de deserializar una clase con constructores sobrecargados usando Jackson?

Gracias

  • Como señala la respuesta, no, debe especificar un único constructor. En su caso, deje el que toma múltiples argumentos, eso funcionará bien. Los argumentos “faltantes” tomarán un valor nulo (para objetos) o un valor predeterminado (para primitivos).

    – StaxMan

    10 de abril de 2013 a las 21:24

  • Gracias. Sin embargo, permitir múltiples constructores sería una buena característica. En realidad, mi ejemplo es un poco artificial. El objeto que estoy tratando de usar en realidad tiene listas de argumentos completamente diferentes, una se crea normalmente, la otra se crea con un Throwable… Veré qué puedo hacer, tal vez tener un constructor vacío y getter/setter para el arrojable

    – friki

    11 de abril de 2013 a las 7:25


  • Sí, estoy seguro de que estaría bien, pero las reglas pueden volverse bastante complejas con diferentes permutaciones. Siempre es posible presentar RFE para nuevas funcionalidades, características.

    – StaxMan

    15 de abril de 2013 a las 23:12

Aunque no está debidamente documentado, solo puede tener un creador por tipo. Puede tener tantos constructores como desee en su tipo, pero solo uno de ellos debe tener un @JsonCreator anotación en él.

avatar de usuario
Brice

EDITAR: He aquí, en un entrada en el blog por los mantenedores de Jackson, parece que 2.12 puede ver mejoras con respecto a la inyección de constructor. (La versión actual en el momento de esta edición es 2.11.1)

Mejore la detección automática de creadores de constructores, lo que incluye resolver/aliviar problemas con constructores ambiguos de 1 argumento (delegación frente a propiedades)


Esto sigue siendo válido para Jackson databind 2.7.0.

el jackson @JsonCreator anotación 2.5 javadoc o Documentación de anotaciones de Jackson gramática (constructors y método de fábricas) hagamos creer en verdad que se puede Marcos múltiples constructores.

Anotación de marcador que se puede usar para definir constructores y métodos de fábrica como uno para usar para instanciar nuevas instancias de la clase asociada.

Mirando el código donde el creadores están identificados, parece que los Jackson CreatorCollector está ignorando constructores sobrecargados porque solo comprueba el primer argumento del constructor.

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 es el primer creador de constructores identificado.
  • newOne es el creador del constructor sobrecargado.

Eso significa que ese código es así. no funcionará

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

Pero este código funcionará:

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

Esto es un poco raro y puede que no sea una prueba futura..


La documentación es vaga sobre cómo funciona la creación de objetos; Sin embargo, por lo que deduzco del código, es posible mezclar diferentes métodos:

Por ejemplo, uno puede tener un método de fábrica estático anotado con @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");

Funciona pero no es lo ideal. Al final, podría tener sentido, por ejemplo, si el JSON es ese dinámica entonces tal vez uno debería buscar usar un constructor delegado para manejar las variaciones de carga útil de manera mucho más elegante que con múltiples constructores anotados.

También tenga en cuenta que Jackson ordena a los creadores por prioridadpor ejemplo en este código:

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

Esta vez Jackson no generará un error, pero Jackson solo usará el delegar constructor Phone(Map<String, Object> properties)lo que significa que Phone(@JsonProperty("value") String value) nunca se usa.

  • En mi humilde opinión, esta debería ser la respuesta aceptada porque proporciona una explicación completa con un buen ejemplo

    – matiou

    23 de marzo de 2017 a las 20:08

avatar de usuario
Tiago Stapenhorst Martins

Si acerté en lo que estás tratando de lograr, puedes resolverlo. sin una sobrecarga de constructor.

Si solo desea poner valores nulos en los atributos que no están presentes en un JSON o un Mapa, puede hacer lo siguiente:

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

Ese fue mi caso cuando encontré tu pregunta. Me tomó un tiempo descubrir cómo resolverlo, tal vez eso es lo que estabas tratando de hacer. La solución de @Brice no funcionó para mí.

  • Mejor respuesta en mi humilde opinión

    – Jakob

    19 de marzo de 2019 a las 10:31

Si no le importa hacer un poco más de trabajo, puede deserializar la entidad manualmente:

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

¿Ha sido útil esta solución?