Cómo deserializar una clase con constructores sobrecargados usando JsonCreator

82

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 intento esto, obtengo lo siguiente

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

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

Gracias

geejay
fuente
4
Como señala la respuesta, no, debe especificar un único constructor. En su caso, deje el que tenga múltiples argumentos, eso funcionará bien. Los argumentos "faltantes" tomarán un valor nulo (para objetos) o un valor predeterminado (para primitivas).
StaxMan
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 un getter / setter para el throwable
Geejay
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 funciones y características.
StaxMan

Respuestas:

118

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 una @JsonCreatoranotación.

Percepción
fuente
69

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

Mejore la detección automática de los creadores de constructores, incluida la resolución / alivio de problemas con constructores ambiguos de 1 argumento (delegar frente a propiedades)


Esto sigue siendo válido para Jackson databind 2.7.0.

The Jackson @JsonCreatoranotación 2.5 javadoc o Jackson anotaciones documentación de la gramática ( constructor s y método de fábrica s ) dejar que creen en verdad que uno puede marcar varios constructores.

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

Al observar el código donde se identifican los creadores , parece que Jackson CreatorCollectorestá ignorando los constructores sobrecargados porque solo verifica 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 un código como ese 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 engañoso y puede que no sea a prueba de futuro .


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

Por ejemplo, se 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 ideal. Al final, podría tener sentido, por ejemplo, si el json es así de dinámico, entonces quizás 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 prioridad , por 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 constructor delegadoPhone(Map<String, Object> properties) , lo que significa que Phone(@JsonProperty("value") String value)nunca se usa.

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

Si entendí bien lo que está tratando de lograr, puede resolverlo sin una sobrecarga de constructores .

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ó algo de tiempo descubrir cómo resolverlo, tal vez eso es lo que estabas intentando hacer. La solución @Brice no funcionó para mí.

Tiago Stapenhorst Martins
fuente
1
Mejor respuesta en mi humilde opinión
Jakob
3

Si no le importa trabajar un poco más, 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");
            }
        }
    }
}
Malcolm Crum
fuente