¿Cómo eliminar / serializar un objeto inmutable sin un constructor predeterminado usando ObjectMapper?

106

Quiero serializar y deserializar un objeto inmutable usando com.fasterxml.jackson.databind.ObjectMapper.

La clase inmutable se ve así (solo 3 atributos internos, captadores y constructores):

public final class ImportResultItemImpl implements ImportResultItem {

    private final ImportResultItemType resultType;

    private final String message;

    private final String name;

    public ImportResultItemImpl(String name, ImportResultItemType resultType, String message) {
        super();
        this.resultType = resultType;
        this.message = message;
        this.name = name;
    }

    public ImportResultItemImpl(String name, ImportResultItemType resultType) {
        super();
        this.resultType = resultType;
        this.name = name;
        this.message = null;
    }

    @Override
    public ImportResultItemType getResultType() {
        return this.resultType;
    }

    @Override
    public String getMessage() {
        return this.message;
    }

    @Override
    public String getName() {
        return this.name;
    }
}

Sin embargo, cuando ejecuto esta prueba unitaria:

@Test
public void testObjectMapper() throws Exception {
    ImportResultItemImpl originalItem = new ImportResultItemImpl("Name1", ImportResultItemType.SUCCESS);
    String serialized = new ObjectMapper().writeValueAsString((ImportResultItemImpl) originalItem);
    System.out.println("serialized: " + serialized);

    //this line will throw exception
    ImportResultItemImpl deserialized = new ObjectMapper().readValue(serialized, ImportResultItemImpl.class);
}

Obtengo esta excepción:

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class eu.ibacz.pdkd.core.service.importcommon.ImportResultItemImpl]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: {"resultType":"SUCCESS","message":null,"name":"Name1"}; line: 1, column: 2]
    at 
... nothing interesting here

Esta excepción me pide que cree un constructor predeterminado, pero este es un objeto inmutable, por lo que no quiero tenerlo. ¿Cómo establecería los atributos internos? Confundiría totalmente al usuario de la API.

Entonces, mi pregunta es: ¿Puedo de alguna manera deserializar objetos inmutables sin el constructor predeterminado?

Michal
fuente
Mientras se desrializa, el desrializador no conoce ninguno de sus constructores, por lo que llega al constructor predeterminado. Debido a esto, debe crear un constructor predeterminado, que no cambiará la inmutabilidad. También veo que la clase no es definitiva, ¿por qué? cualquiera puede anular la funcionalidad, ¿no?
Abhinaba Basu
La clase no es definitiva, porque olvidé escribirla allí, gracias por darte cuenta :)
Michal

Respuestas:

149

Para que Jackson sepa cómo crear un objeto para la deserialización, use las anotaciones @JsonCreatory @JsonPropertypara sus constructores, así:

@JsonCreator
public ImportResultItemImpl(@JsonProperty("name") String name, 
        @JsonProperty("resultType") ImportResultItemType resultType, 
        @JsonProperty("message") String message) {
    super();
    this.resultType = resultType;
    this.message = message;
    this.name = name;
}
Sergei
fuente
1
+1 gracias Sergey. Funcionó ... sin embargo, estaba teniendo problemas incluso después de usar la anotación JsonCreator ... pero después de una revisión me di cuenta de que olvidé usar la anotación RequestBody en mi recurso. Ahora funciona .. Gracias una vez más.
Rahul
4
¿Qué sucede si no puede agregar un constructor predeterminado o las anotaciones @JsonCreatory @JsonPropertyporque no tiene acceso al POJO para cambiarlo? ¿Todavía hay una forma de deserializar el objeto?
Jack Straw
1
@JackStraw 1) subclase del objeto y anula todos los captadores y definidores. 2) escriba su propio serializador / deserializador para la clase y utilícelo (busque "serializador personalizado" 3) envuelva el objeto y escriba un serializador / deserializador solo para una propiedad (esto puede permitirle hacer menos trabajo que escribir un serializador para la clase en sí, pero tal vez no).
ErikE
¡Haz +1 en tu comentario! Hasta ahora, cada solución que estaba encontrando fue crear el constructor predeterminado ...
Nilson Aguiar
34

Puede usar un constructor predeterminado privado, Jackson luego completará los campos mediante la reflexión, incluso si son finales privados.

EDITAR: Y use un constructor predeterminado protegido / protegido por paquete para las clases principales si tiene herencia.

tkruse
fuente
Solo para construir sobre esta respuesta, si la clase que desea deserializar extiende otra clase, entonces puede hacer que el constructor predeterminado de la superclase (o ambas clases) esté protegido.
KWILLIAMS
3

La primera respuesta de Sergei Petunin es correcta. Sin embargo, podríamos simplificar el código eliminando las anotaciones @JsonProperty redundantes en cada parámetro del constructor.

Se puede hacer agregando com.fasterxml.jackson.module.paramnames.ParameterNamesModule en ObjectMapper:

new ObjectMapper()
        .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))

(Por cierto: este módulo está registrado de forma predeterminada en SpringBoot. Si usa el bean ObjectMapper de JacksonObjectMapperConfiguration o si crea su propio ObjectMapper con el bean Jackson2ObjectMapperBuilder, puede omitir el registro manual del módulo)

Por ejemplo:

public class FieldValidationError {
  private final String field;
  private final String error;

  @JsonCreator
  public FieldValidationError(String field,
                              String error) {
    this.field = field;
    this.error = error;
  }

  public String getField() {
    return field;
  }

  public String getError() {
    return error;
  }
}

y ObjectMapper deserializa este json sin ningún error:

{
  "field": "email",
  "error": "some text"
}
Vladyslav Diachenko
fuente