Jak de / serializować niezmienny obiekt bez domyślnego konstruktora przy użyciu ObjectMapper?

106

Chcę serializować i deserializować niezmienny obiekt przy użyciu com.fasterxml.jackson.databind.ObjectMapper.

Niezmienna klasa wygląda następująco (tylko 3 atrybuty wewnętrzne, metody pobierające i konstruktory):

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

Jednak kiedy uruchamiam ten test jednostkowy:

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

Mam ten wyjątek:

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

Ten wyjątek prosi mnie o utworzenie domyślnego konstruktora, ale jest to niezmienny obiekt, więc nie chcę go mieć. Jak ustawi wewnętrzne atrybuty? Całkowicie zmyliłoby to użytkownika API.

Moje pytanie brzmi: czy mogę w jakiś sposób de / serializować niezmienne obiekty bez domyślnego konstruktora?

Michał
źródło
Podczas desrializacji desrializator nie wie o żadnym z konstruktorów, więc trafia w domyślny konstruktor. Z tego powodu musisz stworzyć domyślny konstruktor, który nie zmieni niezmienności. Widzę też, że zajęcia nie są ostateczne, dlaczego tak? każdy może zastąpić tę funkcjonalność, prawda?
Abhinaba Basu
Zajęcia nie są ostateczne, bo zapomniałem je tam napisać, dziękuję za zauważenie :)
Michał

Odpowiedzi:

149

Aby poinformować Jacksona, jak utworzyć obiekt do deserializacji, użyj adnotacji @JsonCreatori @JsonPropertydla swoich konstruktorów, na przykład:

@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;
}
Siergiej
źródło
1
+1 dzięki Sergey. Zadziałało ... jednak miałem problem nawet po użyciu adnotacji JsonCreator ... ale po pewnym przeglądzie zdałem sobie sprawę, że zapomniałem użyć adnotacji RequestBody w swoim zasobie. Teraz to działa ... Dzięki jeszcze raz.
Rahul
4
Co jeśli nie możesz dodać domyślnego konstruktora lub adnotacji @JsonCreatori, @JsonPropertyponieważ nie masz dostępu do POJO, aby go zmienić? Czy nadal istnieje sposób na deserializację obiektu?
Jack Straw
1
@JackStraw 1) podklasy z obiektu i przesłania wszystkie metody pobierające i ustawiające. 2) napisz własny serializator / deserializator dla klasy i użyj go (wyszukaj „niestandardowy serializator” 3) zawiń obiekt i napisz serializator / deserializator tylko dla jednej właściwości (może to pozwolić ci wykonać mniej pracy niż pisanie serializatora dla sama klasa, ale może nie).
ErikE
Daj +1 swojemu komentarzowi uratował mnie! Każde rozwiązanie, które znalazłem, to utworzenie domyślnego konstruktora ...
Nilson Aguiar
34

Możesz użyć prywatnego konstruktora domyślnego, a Jackson wypełni pola za pomocą refleksji, nawet jeśli są one ostateczne.

EDYCJA: I użyj domyślnego konstruktora chronionego / chronionego pakietem dla klas nadrzędnych, jeśli masz dziedziczenie.

tkruse
źródło
Aby zbudować na tej odpowiedzi, jeśli klasa, którą chcesz deserializować, rozszerza inną klasę, możesz ustawić domyślny konstruktor superklasy (lub obu klas) jako chroniony.
KWILLIAMS
3

Pierwsza odpowiedź Siergieja Petunina jest słuszna. Możemy jednak uprościć kod, usuwając nadmiarowe adnotacje @JsonProperty na każdym parametrze konstruktora.

Można to zrobić, dodając com.fasterxml.jackson.module.paramnames.ParameterNamesModule do ObjectMapper:

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

(Btw: ten moduł jest domyślnie zarejestrowany w SpringBoot. Jeśli używasz beana ObjectMapper z JacksonObjectMapperConfiguration lub jeśli tworzysz własny ObjectMapper z fasolą Jackson2ObjectMapperBuilder, możesz pominąć ręczną rejestrację modułu)

Na przykład:

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

i ObjectMapper deserializuje ten plik json bez żadnych błędów:

{
  "field": "email",
  "error": "some text"
}
Vladyslav Diachenko
źródło