Como de / serializar um objeto imutável sem construtor padrão usando ObjectMapper?

106

Eu quero serializar e desserializar um objeto imutável usando com.fasterxml.jackson.databind.ObjectMapper.

A classe imutável se parece com isto (apenas 3 atributos internos, getters e construtores):

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

No entanto, quando eu executo este teste de unidade:

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

Eu recebo esta exceção:

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 exceção me pede para criar um construtor padrão, mas este é um objeto imutável, então não quero tê-lo. Como isso definiria os atributos internos? Isso confundiria totalmente o usuário da API.

Portanto, minha pergunta é: Posso de alguma forma desserializar objetos imutáveis ​​sem o construtor padrão?

Michal
fonte
Durante a dessrialização, o dessrializador não conhece nenhum de seu construtor, então ele atinge o construtor padrão. Devido a isso, você deve criar um construtor padrão, que não altere a imutabilidade. Também vejo que a aula não é final, por quê? qualquer um pode substituir a funcionalidade, não é?
Abhinaba Basu
A aula não é final, porque esqueci de escrevê-la lá, obrigado por notar :)
Michal

Respostas:

149

Para permitir que Jackson saiba como criar um objeto para desserialização, use as anotações @JsonCreatore @JsonPropertypara seus construtores, como este:

@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
fonte
1
+1 obrigado Sergey. Funcionou .. no entanto, eu estava tendo problemas mesmo depois de usar a anotação JsonCreator .. mas após alguma revisão percebi que esqueci de usar a anotação RequestBody no meu recurso. Agora funciona .. Thanx mais uma vez.
Rahul
4
E se você não puder adicionar um construtor padrão ou as anotações @JsonCreatore @JsonPropertyporque não tem acesso ao POJO para alterá-lo? Ainda existe uma maneira de desserializar o objeto?
Jack Straw
1
@JackStraw 1) subclasse do objeto e substitua todos os getters e setters. 2) escrever seu próprio serializador / desserializador para a classe e usá-lo (pesquisar por "serializador personalizado" 3) embrulhar o objeto e escrever um serializador / desserializador apenas para uma propriedade (isso pode permitir que você faça menos trabalho do que escrever um serializador para a própria classe, mas talvez não).
ErikE de
+1 seu comentário me salvou! Cada solução que eu estava encontrando até agora era criar o construtor padrão ...
Nilson Aguiar
34

Você pode usar um construtor padrão privado, Jackson irá preencher os campos por meio de reflexão, mesmo se eles forem finais privados.

EDIT: E use um construtor padrão protegido / protegido por pacote para classes pai se você tiver herança.

tkruse
fonte
Apenas para desenvolver essa resposta, se a classe que você deseja desserializar estende outra classe, você pode tornar o construtor padrão da superclasse (ou de ambas as classes) protegido.
KWILLIAMS
3

A primeira resposta de Sergei Petunin está certa. No entanto, poderíamos simplificar o código removendo anotações @JsonProperty redundantes em cada parâmetro do construtor.

Isso pode ser feito adicionando com.fasterxml.jackson.module.paramnames.ParameterNamesModule em ObjectMapper:

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

(Btw: este módulo é registrado por padrão no SpringBoot. Se você usar o bean ObjectMapper de JacksonObjectMapperConfiguration ou se você criar seu próprio ObjectMapper com o bean Jackson2ObjectMapperBuilder, então você pode pular o registro manual do módulo)

Por exemplo:

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

e ObjectMapper desserializa este json sem erros:

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