Jackson enum Serializing y DeSerializer

225

Estoy usando JAVA 1.6 y Jackson 1.9.9 Tengo una enumeración

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

He agregado un @JsonValue, esto parece hacer el trabajo en el que serializa el objeto:

{"event":"forgot password"}

pero cuando trato de deserializar me sale un

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

¿Que me estoy perdiendo aqui?

pookieman
fuente
44
¿Lo has intentado {"Event":"FORGOT_PASSWORD"}? Tenga en cuenta los límites en el evento y FORGOT_PASSWORD.
OldCurmudgeon
Quien vino aquí también verifica la sintaxis de getter setter si sigue una convención de nomenclatura diferente, es decir, en lugar de getValueesto GetValueno funciona
Davut Gürbüz

Respuestas:

287

La solución de serializador / deserializador señalada por @xbakesx es excelente si desea desacoplar completamente su clase enum de su representación JSON.

Alternativamente, si se prefiere una solución en sí misma, una implementación basada en @JsonCreatory @JsonValueanotaciones serían más conveniente.

Aprovechando el ejemplo de @Stanley, la siguiente es una solución completa e independiente (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
fuente
@Agusti favor, eche un vistazo a mi pregunta, ¿Qué me falta no stackoverflow.com/questions/30525986/enum-is-not-binding
Prabjot Singh
25
puede ser obvio para algunos, pero tenga en cuenta que @ JsonValue se usa para la serialización y @ JsonCreator para la deserialización. Si no estás haciendo ambas cosas, solo necesitarás una u otra.
acvcu
66
Realmente no me gusta esta solución por el simple hecho de que introduces dos fuentes de verdad. El desarrollador siempre tendrá que recordar agregar el nombre en dos lugares. Prefiero una solución que simplemente haga lo correcto sin decorar las partes internas de una enumeración con un mapa.
mttdbrd
2
@ttdbrd ¿qué tal esto para unificar verdades? gist.github.com/Scuilion/036c53fd7fee2de89701a95822c0fb60
KevinO
1
En lugar de un mapa estático, puede usar YourEnum.values ​​(), que proporciona la matriz de YourEnum e iterar en él
Valeriy K.
209

Tenga en cuenta que a partir de este compromiso en junio de 2015 (Jackson 2.6.2 y superior) ahora puede simplemente escribir:

public enum Event {
    @JsonProperty("forgot password")
    FORGOT_PASSWORD;
}
tif
fuente
1
Buena solución Es una pena que esté atascado con 2.6.0 incluido en Dropwizard :-(
Clint Eastwood
1
Desafortunadamente, esto no devuelve la propiedad al convertir su enumeración en una cadena.
Nicholas
44
Esta característica ha quedado en desuso desde 2.8.
pqian
2
Esta solución funciona tanto para serializar como para deserializar en Enum. Probado en 2.8.
Downhillski
1
Parece que no está en desuso en absoluto: github.com/FasterXML/jackson-annotations/blob/master/src/main/…
pablo
88

Debe crear un método de fábrica estático que tome un solo argumento y lo anote con @JsonCreator(disponible desde Jackson 1.2)

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

Lea más sobre la anotación JsonCreator aquí .

Stanley
fuente
10
Esta es la solución más limpia y concisa, ¡el resto son solo toneladas de repeticiones que podrían (y deberían!) Evitarse a toda costa!
Clint Eastwood
44
@JSONValueserializar y @JSONCreatordeserializar.
Chiranjib
@JsonCreator public static Event valueOf(int intValue) { ... }para deserializar intal Eventenumerador.
Eido95
1
@ClintEastwood si las otras soluciones deben evitarse depende de si desea separar las preocupaciones de serialización / deserialización de la enumeración o no.
Asa
44

Respuesta real:

El deserializador predeterminado para enums usa .name()para deserializar, por lo que no está usando el @JsonValue. Entonces, como señaló @OldCurmudgeon, tendrías que pasar {"event": "FORGOT_PASSWORD"}para que coincida con el .name()valor.

Otra opción (suponiendo que desea que los valores json de escritura y lectura sean los mismos) ...

Más información:

Hay (aún) otra forma de gestionar el proceso de serialización y deserialización con Jackson. Puede especificar estas anotaciones para usar su propio serializador y deserializador personalizados:

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

Luego tienes que escribir MySerializery MyDeserializerque se ve así:

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 hacer esto en una enumeración JsonEnumque serializa con el método getYourValue(), su serializador y deserializador podrían verse así:

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
fuente
3
El uso del serializador personalizado (de) mata la simplicidad (que vale la pena usar Jackson, por cierto), por lo que esto es necesario en situaciones realmente pesadas. Use @JsonCreator, como se describe a continuación, y revise este comentario
Dmitry Gryazin
1
Esta solución es la mejor para el problema un tanto loco introducido en la pregunta de los OP. El verdadero problema aquí es que el OP quiere devolver los datos estructurados en un formato renderizado . Es decir, están devolviendo datos que ya incluyen una cadena fácil de usar. Pero para volver a convertir el formulario representado en un identificador, necesitamos un código que pueda revertir la transformación. La respuesta aceptada hacky quiere usar un mapa para manejar la transformación, pero requiere más mantenimiento. Con esta solución, puede agregar nuevos tipos enumerados y luego sus desarrolladores pueden continuar con sus trabajos.
mttdbrd
34

He encontrado una solución muy agradable y concisa, especialmente útil cuando no puede modificar las clases de enumeración como era en mi caso. Luego, debe proporcionar un ObjectMapper personalizado con una determinada característica habilitada. Esas características están disponibles desde Jackson 1.6. Entonces solo necesita escribir el toString()método en su enumeración.

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

Hay más funciones relacionadas con la enumeración disponibles, consulte aquí:

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

lagivan
fuente
10
No estoy seguro de por qué necesita extender la clase. puede habilitar esta función en una instancia de ObjectMapper.
mttdbrd
+1 porque me señaló el [READ | WRITE] _ENUMS_USING_TO_STRING que puedo usar en Spring application.yml
HelLViS69
8

Prueba esto.

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
fuente
6

Puede personalizar la deserialización para cualquier atributo.

Declare su clase deserialize usando annotationJsonDeserialize ( import com.fasterxml.jackson.databind.annotation.JsonDeserialize) para el atributo que se procesará. Si esto es una enumeración:

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

De esta manera, su clase se utilizará para deserializar el atributo. Este es un ejemplo 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
fuente
Nathaniel Ford, ¿mejoró?
Fernando Gomes
1
Sí, esta es una respuesta mucho mejor; Proporciona algo de contexto. Sin embargo, iría aún más lejos y discutiría por qué agregar deserialización de esta manera aborda el obstáculo específico del OP.
Nathaniel Ford
5

Hay varios enfoques que puede tomar para lograr la deserialización de un objeto JSON en una enumeración. Mi estilo favorito es hacer una clase 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
fuente
4

Aquí hay otro ejemplo que usa valores de cadena en lugar de un 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
fuente
4

En el contexto de una enumeración, usar @JsonValuenow (since 2.0) funciona para la serialización y deserialización.

Según las anotaciones de jackson javadoc para@JsonValue :

NOTA: cuando se usa para enumeraciones Java, una característica adicional es que el valor devuelto por el método anotado también se considera el valor para deserializar, no solo JSON String para serializar como. Esto es posible ya que el conjunto de valores de Enum es constante y es posible definir la asignación, pero no se puede hacer en general para los tipos POJO; como tal, esto no se usa para la deserialización de POJO.

Por lo tanto, tener la Eventenumeración anotada tal como funciona anteriormente (tanto para la serialización como para la deserialización) con jackson 2.0+.

Brice Roncace
fuente
3

Además de usar @JsonSerialize @JsonDeserialize, también puede usar SerializationFeature y DeserializationFeature (enlace jackson) en el mapeador de objetos.

Como DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, que dan el tipo de enumeración predeterminado si el proporcionado no está definido en la clase de enumeración.

Yuantao
fuente
0

La forma más simple que encontré es usar la anotación @ JsonFormat.Shape.OBJECT para la enumeración.

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

En mi caso, esto es lo que se resolvió:

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 y deserializa los siguientes json:

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

¡Espero que ayude!

Flavio Sousa
fuente