Jackson enum Sérialisation et désérialisation

225

J'utilise JAVA 1.6 et Jackson 1.9.9 J'ai une énumération

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

J'ai ajouté un @JsonValue, cela semble faire le travail dans lequel il sérialise l'objet:

{"event":"forgot password"}

mais quand j'essaie de désérialiser je reçois 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

Qu'est-ce que j'oublie ici?

pookieman
la source
4
As-tu essayé {"Event":"FORGOT_PASSWORD"}? Notez les plafonds à la fois sur Event et FORGOT_PASSWORD.
OldCurmudgeon
Qui est venu ici vérifie également la syntaxe du setter getter si vous suivez des conventions de dénomination différentes, c'est-à-dire que getValuecela GetValuene fonctionne pas
Davut Gürbüz

Réponses:

287

La solution de sérialisation / désérialisation indiquée par @xbakesx est excellente si vous souhaitez découpler complètement votre classe enum de sa représentation JSON.

Alternativement, si vous préférez une solution autonome, une implémentation basée sur @JsonCreatoret des @JsonValueannotations serait plus pratique.

Ainsi, en s'appuyant sur l'exemple de @Stanley, voici une solution complète et autonome (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
la source
@Agusti s'il vous plaît jetez un oeil à ma question, que me manque-t-il stackoverflow.com/questions/30525986/enum-is-not-binding
Prabjot Singh
25
peut être évident pour certains, mais notez que @ JsonValue est utilisé pour la sérialisation et @ JsonCreator pour la désérialisation. Si vous ne faites pas les deux, vous n'aurez besoin que de l'un ou de l'autre.
acvcu
6
Je n'aime vraiment pas cette solution pour le simple fait que vous introduisez deux sources de vérité. Le développeur devra toujours se rappeler d'ajouter le nom à deux endroits. Je préfère de loin une solution qui fait juste ce qu'il faut sans décorer les éléments internes d'une énumération avec une carte.
mttdbrd
2
@ttdbrd que diriez-vous de cela pour unifier les vérités? gist.github.com/Scuilion/036c53fd7fee2de89701a95822c0fb60
KevinO
1
Au lieu d'une carte statique, vous pouvez utiliser YourEnum.values ​​() qui donne Array of YourEnum et itère dessus
Valeriy K.
209

Notez qu'à partir de ce commit en juin 2015 (Jackson 2.6.2 et supérieur), vous pouvez maintenant simplement écrire:

public enum Event {
    @JsonProperty("forgot password")
    FORGOT_PASSWORD;
}
tif
la source
1
belle solution. C'est dommage que je sois coincé avec 2.6.0 inclus dans Dropwizard :-(
Clint Eastwood
1
Malheureusement, cela ne renvoie pas la propriété lors de la conversion de votre énumération en chaîne.
Nicholas
4
Cette fonctionnalité est déconseillée depuis la version 2.8.
pqian
2
Cette solution fonctionne à la fois pour sérialiser et désérialiser sur Enum. Testé en 2.8.
Downhillski
88

Vous devez créer une méthode d'usine statique qui accepte un seul argument et l'annoter avec @JsonCreator(disponible depuis Jackson 1.2)

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

En savoir plus sur l'annotation JsonCreator ici .

Stanley
la source
10
Il s'agit de la solution la plus propre et la plus concise, le reste n'est que des tonnes de passe-partout qui pourraient (et devraient!) Être évités à tout prix!
Clint Eastwood
4
@JSONValuesérialiser et @JSONCreatordésérialiser.
Chiranjib
@JsonCreator public static Event valueOf(int intValue) { ... }désérialiser inten Eventénumérateur.
Eido95
1
@ClintEastwood si les autres solutions doivent être évitées dépend si vous souhaitez séparer les problèmes de sérialisation / désérialisation de l'énumération ou non.
Asa
44

Réponse réelle:

Le désérialiseur par défaut pour les enums utilise .name()pour désérialiser, donc il n'utilise pas le @JsonValue. Donc, comme l'a souligné @OldCurmudgeon, vous devez passer {"event": "FORGOT_PASSWORD"}pour correspondre à la .name()valeur.

Une autre option (en supposant que vous souhaitiez que les valeurs json d'écriture et de lecture soient identiques) ...

Plus d'informations:

Il existe (encore) une autre façon de gérer le processus de sérialisation et de désérialisation avec Jackson. Vous pouvez spécifier ces annotations pour utiliser vos propres sérialiseur et désérialiseur personnalisés:

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

Ensuite, vous devez écrire MySerializeret MyDeserializerqui ressemblent à ceci:

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

}

Dernier petit détail, en particulier pour faire cela à une énumération JsonEnumqui sérialise avec la méthode getYourValue(), votre sérialiseur et désérialiseur pourrait ressembler à ceci:

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
la source
3
L'utilisation de (dé) sérialiseur personnalisé tue la simplicité (qui vaut pour Jackson, btw), donc cela est nécessaire dans des situations très lourdes. Utilisez @JsonCreator, comme décrit ci-dessous, et vérifiez ce commentaire
Dmitry Gryazin
1
Cette solution est la meilleure pour le problème quelque peu fou présenté dans la question OP. Le vrai problème ici est que l'OP souhaite renvoyer les données structurées sous une forme rendue . Autrement dit, ils retournent des données qui incluent déjà une chaîne conviviale. Mais pour transformer le formulaire rendu en identifiant, nous avons besoin d'un code qui peut inverser la transformation. La réponse hacky acceptée veut utiliser une carte pour gérer la transformation, mais nécessite plus de maintenance. Avec cette solution, vous pouvez ajouter de nouveaux types énumérés, puis vos développeurs peuvent poursuivre leur travail.
mttdbrd
34

J'ai trouvé une solution très agréable et concise, particulièrement utile lorsque vous ne pouvez pas modifier les classes enum comme c'était le cas dans mon cas. Ensuite, vous devez fournir un ObjectMapper personnalisé avec une certaine fonctionnalité activée. Ces fonctionnalités sont disponibles depuis Jackson 1.6. Il vous suffit donc d'écrire la toString()méthode dans votre énumération.

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

D'autres fonctionnalités liées à l'énumération sont disponibles, voir ici:

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

lagivan
la source
10
Je ne sais pas pourquoi vous devez prolonger la classe. vous pouvez activer cette fonctionnalité sur une instance de l'ObjectMapper.
mttdbrd
+1 parce qu'il m'a dirigé vers le [LIRE | ÉCRIRE] _ENUMS_USING_TO_STRING que je peux utiliser dans Spring application.yml
HelLViS69
8

Essaye ça.

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
la source
6

Vous pouvez personnaliser la désérialisation pour n'importe quel attribut.

Déclarez votre classe de désérialisation à l'aide de l'annotationJsonDeserialize ( import com.fasterxml.jackson.databind.annotation.JsonDeserialize) pour l'attribut qui sera traité. S'il s'agit d'un Enum:

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

De cette façon, votre classe sera utilisée pour désérialiser l'attribut. Voici un exemple complet:

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
la source
Nathaniel Ford, ça va mieux?
Fernando Gomes
1
Oui, c'est une bien meilleure réponse; il fournit un certain contexte. J'irais même plus loin, cependant, et j'expliquerais pourquoi l'ajout de la désérialisation de cette manière résout l'obstacle spécifique du PO.
Nathaniel Ford
5

Il existe différentes approches que vous pouvez adopter pour effectuer la désérialisation d'un objet JSON en une énumération. Mon style préféré est de créer une classe intérieure:

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
la source
4

Voici un autre exemple qui utilise des valeurs de chaîne au lieu d'une carte.

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
la source
4

Dans le contexte d'une énumération, l'utilisation de @JsonValuenow (depuis 2.0) fonctionne pour la sérialisation et la désérialisation.

Selon le javadoc annotations jackson pour@JsonValue :

REMARQUE: lors de l'utilisation pour les énumérations Java, une caractéristique supplémentaire est que la valeur renvoyée par la méthode annotée est également considérée comme la valeur à désérialiser, pas seulement la chaîne JSON à sérialiser. Ceci est possible car l'ensemble des valeurs Enum est constant et il est possible de définir le mappage, mais ne peut pas être fait en général pour les types POJO; en tant que tel, il n'est pas utilisé pour la désérialisation POJO.

Donc, avoir l' Eventénumération annotée comme ci-dessus fonctionne (pour la sérialisation et la désérialisation) avec jackson 2.0+.

Brice Roncace
la source
3

Outre l'utilisation de @JsonSerialize @JsonDeserialize, vous pouvez également utiliser SerializationFeature et DeserializationFeature (liaison jackson) dans le mappeur d'objets.

Tels que DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, qui donnent le type d'énumération par défaut si celui fourni n'est pas défini dans la classe enum.

Yuantao
la source
0

La manière la plus simple que j'ai trouvée consiste à utiliser l'annotation @ JsonFormat.Shape.OBJECT pour l'énumération.

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

Dans mon cas, c'est ce qui a résolu:

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

Sérialise et désérialise le json suivant:

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

J'espère que ça aide!

Flavio Sousa
la source