Как де / сериализовать неизменяемый объект без конструктора по умолчанию с помощью ObjectMapper?

106

Я хочу сериализовать и десериализовать неизменяемый объект с помощью com.fasterxml.jackson.databind.ObjectMapper.

Неизменяемый класс выглядит так (всего 3 внутренних атрибута, геттеры и конструкторы):

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

Однако, когда я запускаю этот модульный тест:

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

Я получаю это исключение:

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

Это исключение просит меня создать конструктор по умолчанию, но это неизменяемый объект, поэтому я не хочу его иметь. Как бы он установил внутренние атрибуты? Это полностью запутает пользователя API.

Итак, мой вопрос: могу ли я каким-то образом де / сериализовать неизменяемые объекты без конструктора по умолчанию?

Михал
источник
Во время десериализации десериализатор не знает ни о одном из ваших конструкторов, поэтому он попадает в конструктор по умолчанию. В связи с этим вам необходимо создать конструктор по умолчанию, который не изменит неизменяемость. Также я вижу, что класс не окончательный, почему так? кто-нибудь может переопределить функциональность, не так ли?
Абхинаба Басу,
Класс не финальный, потому что я забыл написать его там, спасибо, что заметили :)
Михал

Ответы:

149

Для того, чтобы Джексон знает , как создать объект для десериализации, используйте @JsonCreatorи @JsonPropertyаннотации для конструкторов, как это:

@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;
}
Сергей
источник
1
+1 спасибо Сергею. Это сработало ... однако у меня были проблемы даже после использования аннотации JsonCreator ... но после некоторого обзора я понял, что забыл использовать аннотацию RequestBody в моем ресурсе. Теперь это работает .. Еще раз спасибо.
Rahul
4
Что делать , если вы не можете добавить конструктор по умолчанию, или @JsonCreatorи @JsonPropertyаннотаций , потому что у вас нет доступа к POJO , чтобы изменить его? Есть ли еще способ десериализации объекта?
Джек Стро
1
@JackStraw 1) подкласс объекта и переопределить все геттеры и сеттеры. 2) напишите свой собственный сериализатор / десериализатор для класса и используйте его (поиск «пользовательского сериализатора» 3) оберните объект и напишите сериализатор / десериализатор только для одного свойства (это может позволить вам сделать меньше работы, чем написание сериализатора для сам класс, а может и нет).
ErikE
+1 ваш комментарий спас меня! До сих пор каждое решение, которое я находил, создавало конструктор по умолчанию ...
Нильсон Агияр,
34

Вы можете использовать частный конструктор по умолчанию, затем Джексон заполнит поля через отражение, даже если они являются закрытыми окончательными.

РЕДАКТИРОВАТЬ: И используйте защищенный / защищенный пакетом конструктор по умолчанию для родительских классов, если у вас есть наследование.

ткрузе
источник
Чтобы основываться на этом ответе, если класс, который вы хотите десериализовать, расширяет другой класс, вы можете сделать конструктор по умолчанию суперкласса (или обоих классов) защищенным.
КУИЛЬЯМС
4

Первый ответ Сергея Петунина правильный. Однако мы могли бы упростить код, удалив избыточные аннотации @JsonProperty для каждого параметра конструктора.

Это можно сделать, добавив com.fasterxml.jackson.module.paramnames.ParameterNamesModule в ObjectMapper:

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

(Кстати: этот модуль по умолчанию зарегистрирован в SpringBoot. Если вы используете bean-компонент ObjectMapper из JacksonObjectMapperConfiguration или если вы создаете свой собственный ObjectMapper с bean-компонентом Jackson2ObjectMapperBuilder, вы можете пропустить ручную регистрацию модуля)

Например:

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

и ObjectMapper десериализует этот json без ошибок:

{
  "field": "email",
  "error": "some text"
}
Владислав Дьяченко
источник
Отличный ответ, хорошо работает
Али