Jackson enum Serializing and DeSerializer

237

Estou usando JAVA 1.6 e Jackson 1.9.9. Tenho um enum

public enum Event {
    FORGOT_PASSWORD("forgot password");

    private final String value;

    private Event(final String description) {
        this.value = description;
    }

    @JsonValue
    final String value() {
        return this.value;
    }
}

Eu adicionei um @JsonValue, ele parece fazer o trabalho de serializar o objeto em:

{"event":"forgot password"}

mas quando tento desserializar recebo um

Caused by: org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.globalrelay.gas.appsjson.authportal.Event from String value 'forgot password': value not one of declared Enum instance names

O que estou perdendo aqui?

pookieman
fonte
4
Você já tentou {"Event":"FORGOT_PASSWORD"}? Observe os limites em Evento e FORGOT_PASSWORD.
OldCurmudgeon
Quem veio aqui também verifica a sintaxe do getter setter se você seguir uma convenção de nomenclatura diferente, ou seja, em vez getValuedisso GetValuenão funciona
Davut Gürbüz

Respostas:

297

A solução de serializador / desserializador apontada por @xbakesx é excelente se você deseja desacoplar completamente sua classe enum de sua representação JSON.

Como alternativa, se você preferir uma solução independente, uma implementação baseada em @JsonCreatore @JsonValueanotações seria mais conveniente.

Portanto, aproveitando o exemplo de @Stanley, o seguinte é uma solução independente completa (Java 6, Jackson 1.9):

public enum DeviceScheduleFormat {

    Weekday,
    EvenOdd,
    Interval;

    private static Map<String, DeviceScheduleFormat> namesMap = new HashMap<String, DeviceScheduleFormat>(3);

    static {
        namesMap.put("weekday", Weekday);
        namesMap.put("even-odd", EvenOdd);
        namesMap.put("interval", Interval);
    }

    @JsonCreator
    public static DeviceScheduleFormat forValue(String value) {
        return namesMap.get(StringUtils.lowerCase(value));
    }

    @JsonValue
    public String toValue() {
        for (Entry<String, DeviceScheduleFormat> entry : namesMap.entrySet()) {
            if (entry.getValue() == this)
                return entry.getKey();
        }

        return null; // or fail
    }
}
Agustí Sánchez
fonte
29
pode ser óbvio para alguns, mas observe que @ JsonValue é usado para serialização e @ JsonCreator para desserialização. Se você não estiver fazendo os dois, precisará apenas de um ou de outro.
acvcu
6
Realmente não gosto dessa solução pelo simples fato de você introduzir duas fontes de verdade. O desenvolvedor sempre terá que se lembrar de adicionar o nome em dois lugares. Eu prefiro uma solução que simplesmente faça a coisa certa sem decorar o interior de uma enum com um mapa.
mttdbrd
1
@mttdbrd você pode evitar isso adicionando os objetos ao mapa durante o construtor
Langley
2
@ttdbrd que tal isso para unificar verdades? gist.github.com/Scuilion/036c53fd7fee2de89701a95822c0fb60
KevinO
3
Em vez de um mapa estático, você pode usar YourEnum.values ​​() que fornece Array de YourEnum e iterar nele
Valeriy K.
218

Observe que, a partir deste commit em junho de 2015 (Jackson 2.6.2 e superior), agora você pode simplesmente escrever:

public enum Event {
    @JsonProperty("forgot password")
    FORGOT_PASSWORD;
}

O comportamento está documentado aqui: https://fasterxml.github.io/jackson-annotations/javadoc/2.11/com/fasterxml/jackson/annotation/JsonProperty.html

A partir de Jackson 2.6, essa anotação também pode ser usada para alterar a serialização de Enum como:

 public enum MyEnum {
      @JsonProperty("theFirstValue") THE_FIRST_VALUE,
      @JsonProperty("another_value") ANOTHER_VALUE;
 }

como alternativa ao uso da anotação JsonValue.

tif
fonte
1
boa solução. É uma pena que estou preso com o 2.6.0 empacotado no Dropwizard :-(
Clint Eastwood
1
Infelizmente, isso não retorna a propriedade ao converter seu enum em uma string.
Nicholas
4
Este recurso foi descontinuado desde 2.8.
pqian de
2
Esta solução funciona para serializar e desserializar no Enum. Testado em 2.8.
Downhillski
4
Não parece ter sido descontinuado: github.com/FasterXML/jackson-annotations/blob/master/src/main/…
pablo
92

Você deve criar um método de fábrica estático que leva um único argumento e anota-o com @JsonCreator(disponível desde Jackson 1.2)

@JsonCreator
public static Event forValue(String value) { ... }

Leia mais sobre a anotação JsonCreator aqui .

Stanley
fonte
10
Esta é a solução mais limpa e concisa, o resto são apenas toneladas de clichês que poderiam (e deveriam!) Ser evitados a todo custo!
Clint Eastwood
4
@JSONValuepara serializar e @JSONCreatordesserializar.
Chiranjib
@JsonCreator public static Event valueOf(int intValue) { ... }para desserializar intpara o Eventenumerador.
Eido95,
1
@ClintEastwood se as outras soluções devem ser evitadas depende se você deseja separar as questões de serialização / desserialização do enum ou não.
Asa
45

Resposta real:

O desserializador padrão para enums usa .name()para desserializar, portanto, não está usando o @JsonValue. Portanto, como @OldCurmudgeon apontou, você precisa passar {"event": "FORGOT_PASSWORD"}para corresponder ao .name()valor.

Uma outra opção (supondo que você deseja que os valores de gravação e leitura json sejam os mesmos) ...

Mais informações:

Existe (ainda) outra maneira de gerenciar o processo de serialização e desserialização com Jackson. Você pode especificar essas anotações para usar seu próprio serializador e desserializador personalizado:

@JsonSerialize(using = MySerializer.class)
@JsonDeserialize(using = MyDeserializer.class)
public final class MyClass {
    ...
}

Então você tem que escrever MySerializere MyDeserializerque se parece com isto:

MySerializer

public final class MySerializer extends JsonSerializer<MyClass>
{
    @Override
    public void serialize(final MyClass yourClassHere, final JsonGenerator gen, final SerializerProvider serializer) throws IOException, JsonProcessingException
    {
        // here you'd write data to the stream with gen.write...() methods
    }

}

MyDeserializer

public final class MyDeserializer extends org.codehaus.jackson.map.JsonDeserializer<MyClass>
{
    @Override
    public MyClass deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException
    {
        // then you'd do something like parser.getInt() or whatever to pull data off the parser
        return null;
    }

}

Por último, especialmente para fazer isso em um enum JsonEnumque serializa com o método getYourValue(), seu serializador e desserializador podem ter a seguinte aparência:

public void serialize(final JsonEnum enumValue, final JsonGenerator gen, final SerializerProvider serializer) throws IOException, JsonProcessingException
{
    gen.writeString(enumValue.getYourValue());
}

public JsonEnum deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException
{
    final String jsonValue = parser.getText();
    for (final JsonEnum enumValue : JsonEnum.values())
    {
        if (enumValue.getYourValue().equals(jsonValue))
        {
            return enumValue;
        }
    }
    return null;
}
xbakesx
fonte
3
O uso do (des) serializador customizado mata a simplicidade (que vale a pena usar Jackson, aliás), então isso é necessário em situações realmente pesadas. Use @JsonCreator, conforme descrito abaixo, e verifique este comentário
Dmitry Gryazin
1
Esta solução é a melhor para o problema um tanto maluco apresentado na questão dos OPs. O verdadeiro problema aqui é que o OP deseja retornar os dados estruturados em um formulário renderizado . Ou seja, eles estão retornando dados que já incluem uma string amigável. Mas, para transformar o formulário renderizado de volta em um identificador, precisamos de algum código que possa reverter a transformação. A resposta aceita pelo hacky deseja usar um mapa para lidar com a transformação, mas requer mais manutenção. Com essa solução, você pode adicionar novos tipos enumerados e, em seguida, seus desenvolvedores podem continuar com seus trabalhos.
mttdbrd
35

Eu encontrei uma solução muito boa e concisa, especialmente útil quando você não pode modificar as classes enum como era no meu caso. Em seguida, você deve fornecer um ObjectMapper customizado com um determinado recurso ativado. Esses recursos estão disponíveis desde Jackson 1.6. Portanto, você só precisa escrever o toString()método em seu enum.

public class CustomObjectMapper extends ObjectMapper {
    @PostConstruct
    public void customConfiguration() {
        // Uses Enum.toString() for serialization of an Enum
        this.enable(WRITE_ENUMS_USING_TO_STRING);
        // Uses Enum.toString() for deserialization of an Enum
        this.enable(READ_ENUMS_USING_TO_STRING);
    }
}

Existem mais recursos relacionados a enum disponíveis, veja aqui:

https://github.com/FasterXML/jackson-databind/wiki/Serialization-Features https://github.com/FasterXML/jackson-databind/wiki/Deserialization-Features

Lagivan
fonte
10
não sei por que você precisa estender a classe. você pode ativar esse recurso em uma instância do ObjectMapper.
mttdbrd
+1 porque ele me apontou para [LEIA | ESCREVA] _ENUMS_USING_TO_STRING que posso usar em Spring application.yml
HelLViS69 de
9

Experimente isso.

public enum Event {

    FORGOT_PASSWORD("forgot password");

    private final String value;

    private Event(final String description) {
        this.value = description;
    }

    private Event() {
        this.value = this.name();
    }

    @JsonValue
    final String value() {
        return this.value;
    }
}
Durga
fonte
6

Você pode personalizar a desserialização para qualquer atributo.

Declare sua classe de desserialização usando annotationJsonDeserialize ( import com.fasterxml.jackson.databind.annotation.JsonDeserialize) para o atributo que será processado. Se este for um Enum:

@JsonDeserialize(using = MyEnumDeserialize.class)
private MyEnum myEnum;

Dessa forma, sua classe será usada para desserializar o atributo. Este é um exemplo completo:

public class MyEnumDeserialize extends JsonDeserializer<MyEnum> {

    @Override
    public MyEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        MyEnum type = null;
        try{
            if(node.get("attr") != null){
                type = MyEnum.get(Long.parseLong(node.get("attr").asText()));
                if (type != null) {
                    return type;
                }
            }
        }catch(Exception e){
            type = null;
        }
        return type;
    }
}
Fernando gomes
fonte
Nathaniel Ford, melhorou?
Fernando Gomes
1
Sim, esta é uma resposta muito melhor; fornece algum contexto. Eu iria ainda mais longe, porém, e discutiria por que adicionar desserialização dessa maneira aborda o obstáculo específico do OP.
Nathaniel Ford
5

Existem várias abordagens que você pode usar para realizar a desserialização de um objeto JSON para um enum. Meu estilo favorito é fazer uma classe interna:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.fasterxml.jackson.annotation.JsonFormat.Shape.OBJECT;

@JsonFormat(shape = OBJECT)
public enum FinancialAccountSubAccountType {
  MAIN("Main"),
  MAIN_DISCOUNT("Main Discount");

  private final static Map<String, FinancialAccountSubAccountType> ENUM_NAME_MAP;
  static {
    ENUM_NAME_MAP = Arrays.stream(FinancialAccountSubAccountType.values())
      .collect(Collectors.toMap(
        Enum::name,
        Function.identity()));
  }

  private final String displayName;

  FinancialAccountSubAccountType(String displayName) {
    this.displayName = displayName;
  }

  @JsonCreator
  public static FinancialAccountSubAccountType fromJson(Request request) {
    return ENUM_NAME_MAP.get(request.getCode());
  }

  @JsonProperty("name")
  public String getDisplayName() {
    return displayName;
  }

  private static class Request {
    @NotEmpty(message = "Financial account sub-account type code is required")
    private final String code;
    private final String displayName;

    @JsonCreator
    private Request(@JsonProperty("code") String code,
                    @JsonProperty("name") String displayName) {
      this.code = code;
      this.displayName = displayName;
    }

    public String getCode() {
      return code;
    }

    @JsonProperty("name")
    public String getDisplayName() {
      return displayName;
    }
  }
}
Sam Berry
fonte
4

Aqui está outro exemplo que usa valores de string em vez de um mapa.

public enum Operator {
    EQUAL(new String[]{"=","==","==="}),
    NOT_EQUAL(new String[]{"!=","<>"}),
    LESS_THAN(new String[]{"<"}),
    LESS_THAN_EQUAL(new String[]{"<="}),
    GREATER_THAN(new String[]{">"}),
    GREATER_THAN_EQUAL(new String[]{">="}),
    EXISTS(new String[]{"not null", "exists"}),
    NOT_EXISTS(new String[]{"is null", "not exists"}),
    MATCH(new String[]{"match"});

    private String[] value;

    Operator(String[] value) {
        this.value = value;
    }

    @JsonValue
    public String toStringOperator(){
        return value[0];
    }

    @JsonCreator
    public static Operator fromStringOperator(String stringOperator) {
        if(stringOperator != null) {
            for(Operator operator : Operator.values()) {
                for(String operatorString : operator.value) {
                    if (stringOperator.equalsIgnoreCase(operatorString)) {
                        return operator;
                    }
                }
            }
        }
        return null;
    }
}
Gremash
fonte
4

No contexto de um enum, usar @JsonValuenow (desde 2.0) funciona para serialização e desserialização.

De acordo com o javadoc de jackson-annotations para@JsonValue :

NOTA: quando usado para enums Java, um recurso adicional é que o valor retornado pelo método anotado também é considerado o valor para desserializar, não apenas String JSON para serializar como. Isso é possível porque o conjunto de valores Enum é constante e é possível definir o mapeamento, mas não pode ser feito em geral para os tipos POJO; como tal, isso não é usado para desserialização POJO.

Portanto, ter o Eventenum anotado como acima funciona (para serialização e desserialização) com jackson 2.0+.

Brice Roncace
fonte
3

Além de usar @JsonSerialize @JsonDeserialize, você também pode usar SerializationFeature e DeserializationFeature (vinculação de jackson) no mapeador de objetos.

Como DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, que fornece o tipo de enum padrão se aquele fornecido não estiver definido na classe de enum.

Yuantao
fonte
1

No meu caso, foi isso que resolveu:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PeriodEnum {

    DAILY(1),
    WEEKLY(2),
    ;

    private final int id;

    PeriodEnum(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

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

    @JsonCreator
    public static PeriodEnum fromJson(@JsonProperty("name") String name) {
        return valueOf(name);
    }
}

Serializa e desserializa o seguinte json:

{
  "id": 2,
  "name": "WEEKLY"
}

Espero que ajude!

Flavio Sousa
fonte
0

A maneira mais simples que encontrei é usando a anotação @ JsonFormat.Shape.OBJECT para o enum.

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyEnum{
    ....
}
yrazlik
fonte
0

Eu fiz assim:

// Your JSON
{"event":"forgot password"}

// Your class to map 
public class LoggingDto {
    @JsonProperty(value = "event")
    private FooEnum logType;
}

//Your enum
public enum FooEnum {

    DATA_LOG ("Dummy 1"),
    DATA2_LOG ("Dummy 2"),
    DATA3_LOG ("forgot password"),
    DATA4_LOG ("Dummy 4"),
    DATA5_LOG ("Dummy 5"),
    UNKNOWN ("");

    private String fullName;

    FooEnum(String fullName) {
        this.fullName = fullName;
    }

    public String getFullName() {
        return fullName;
    }

    @JsonCreator
    public static FooEnum getLogTypeFromFullName(String fullName) {
        for (FooEnum logType : FooEnum.values()) {
            if (logType.fullName.equals(fullName)) {
                return logType;
            }
        }
        return UNKNOWN;
    }


}

Portanto, o valor da propriedade "logType" para a classe LoggingDto será DATA3_LOG

Bastien
fonte