Jackson databind enum insensible à la casse

98

Comment désérialiser une chaîne JSON qui contient des valeurs d'énumération insensibles à la casse? (en utilisant Jackson Databind)

La chaîne JSON:

[{"url": "foo", "type": "json"}]

et mon Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

dans ce cas, la désérialisation du JSON avec "type":"json"échouerait là où "type":"JSON"cela fonctionnerait. Mais je veux "json"aussi travailler pour des raisons de convention de dénomination.

La sérialisation du POJO entraîne également des majuscules "type":"JSON"

J'ai pensé à utiliser @JsonCreatoret @JsonGetter:

    @JsonCreator
    private Endpoint(@JsonProperty("name") String url, @JsonProperty("type") String type) {
        this.url = url;
        this.type = DataType.valueOf(type.toUpperCase());
    }

    //....
    @JsonGetter
    private String getType() {
        return type.name().toLowerCase();
    }

Et ça a marché. Mais je me demandais s'il y avait une meilleure solutuon parce que cela me semble être un hack.

Je peux aussi écrire un désérialiseur personnalisé mais j'ai beaucoup de POJO différents qui utilisent des énumérations et ce serait difficile à maintenir.

Quelqu'un peut-il suggérer une meilleure façon de sérialiser et de désérialiser les énumérations avec une convention de dénomination appropriée?

Je ne veux pas que mes énumérations en java soient en minuscules!

Voici un code de test que j'ai utilisé:

    String data = "[{\"url\":\"foo\", \"type\":\"json\"}]";
    Endpoint[] arr = new ObjectMapper().readValue(data, Endpoint[].class);
        System.out.println("POJO[]->" + Arrays.toString(arr));
        System.out.println("JSON ->" + new ObjectMapper().writeValueAsString(arr));
tom91136
la source
Sur quelle version de Jackson êtes-vous? Jetez un oeil à ce JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov
J'utilise Jackson 2.2.3
tom91136
OK, je viens de mettre à jour vers 2.4.0-RC3
tom91136

Réponses:

38

Dans la version 2.4.0, vous pouvez enregistrer un sérialiseur personnalisé pour tous les types Enum ( lien vers le problème github). Vous pouvez également remplacer vous-même le désérialiseur Enum standard qui sera au courant du type Enum. Voici un exemple:

public class JacksonEnum {

    public static enum DataType {
        JSON, HTML
    }

    public static void main(String[] args) throws IOException {
        List<DataType> types = Arrays.asList(JSON, HTML);
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<Enum> modifyEnumDeserializer(DeserializationConfig config,
                                                              final JavaType type,
                                                              BeanDescription beanDesc,
                                                              final JsonDeserializer<?> deserializer) {
                return new JsonDeserializer<Enum>() {
                    @Override
                    public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
                        Class<? extends Enum> rawClass = (Class<Enum<?>>) type.getRawClass();
                        return Enum.valueOf(rawClass, jp.getValueAsString().toUpperCase());
                    }
                };
            }
        });
        module.addSerializer(Enum.class, new StdSerializer<Enum>(Enum.class) {
            @Override
            public void serialize(Enum value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
                jgen.writeString(value.name().toLowerCase());
            }
        });
        mapper.registerModule(module);
        String json = mapper.writeValueAsString(types);
        System.out.println(json);
        List<DataType> types2 = mapper.readValue(json, new TypeReference<List<DataType>>() {});
        System.out.println(types2);
    }
}

Production:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
la source
1
Merci, maintenant je peux retirer tout le passe-partout dans mon POJO :)
tom91136
Je préconise personnellement cela dans mes projets. Si vous regardez mon exemple, cela nécessite beaucoup de code standard. L'un des avantages de l'utilisation d'attributs séparés pour la dés / sérialisation est qu'il découple les noms des valeurs Java-importantes (noms enum) en valeurs client-importantes (pretty print). Par exemple, si vous souhaitez modifier le type de données HTML en HTML_DATA_TYPE, vous pouvez le faire sans affecter l'API externe, si une clé est spécifiée.
Sam Berry
1
C'est un bon début mais cela échouera si votre énumération utilise JsonProperty ou JsonCreator. Dropwizard a FuzzyEnumModule qui est une implémentation plus robuste.
Pixel Elephant
133

Jackson 2.9

C'est maintenant très simple, en utilisant jackson-databind2.9.0 et plus

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);

// objectMapper now deserializes enums in a case-insensitive manner

Exemple complet avec tests

import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {

  private enum TestEnum { ONE }
  private static class TestObject { public TestEnum testEnum; }

  public static void main (String[] args) {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);

    try {
      TestObject uppercase = 
        objectMapper.readValue("{ \"testEnum\": \"ONE\" }", TestObject.class);
      TestObject lowercase = 
        objectMapper.readValue("{ \"testEnum\": \"one\" }", TestObject.class);
      TestObject mixedcase = 
        objectMapper.readValue("{ \"testEnum\": \"oNe\" }", TestObject.class);

      if (uppercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize uppercase value");
      if (lowercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize lowercase value");
      if (mixedcase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize mixedcase value");

      System.out.println("Success: all deserializations worked");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
Davnicwil
la source
3
Celui-ci est en or!
Vikas Prasad
8
J'utilise 2.9.2 et cela ne fonctionne pas. Provoqué par: com.fasterxml.jackson.databind.exc.InvalidFormatException: impossible de désérialiser la valeur de type .... Gender` from String "male": valeur ne faisant pas partie des noms d'instance Enum déclarés: [FAMALE, MALE]
Jordan Silva
@JordanSilva cela fonctionne certainement avec la v2.9.2. J'ai ajouté un exemple de code complet avec des tests de vérification. Je ne sais pas ce qui s'est passé dans votre cas, mais exécuter l'exemple de code avec jackson-databind2.9.2 fonctionne spécifiquement comme prévu.
davnicwil
5
en utilisant Spring Boot, vous pouvez simplement ajouter la propriétéspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva peut-être que vous essayez de désérialiser enum dans get parameters comme je l'ai fait? =) J'ai résolu mon problème et j'ai répondu ici. J'espère que ça peut aider
Konstantin Zyubin
83

J'ai rencontré ce même problème dans mon projet, nous avons décidé de construire nos énumérations avec une clé de chaîne et une utilisation @JsonValueet un constructeur statique pour la sérialisation et la désérialisation respectivement.

public enum DataType {
    JSON("json"), 
    HTML("html");

    private String key;

    DataType(String key) {
        this.key = key;
    }

    @JsonCreator
    public static DataType fromString(String key) {
        return key == null
                ? null
                : DataType.valueOf(key.toUpperCase());
    }

    @JsonValue
    public String getKey() {
        return key;
    }
}
Sam Berry
la source
1
Cela devrait être DataType.valueOf(key.toUpperCase())- sinon, vous n'avez vraiment rien changé. Codage défensif pour éviter une NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont
2
Bonne prise @sarumont. J'ai fait le montage. Aussi, méthode renommée en "fromString" pour bien jouer avec JAX-RS .
Sam Berry
1
J'ai aimé cette approche, mais j'ai opté pour une variante moins verbeuse, voir ci-dessous.
linqu
2
apparemment, le keychamp est inutile. Dans getKey, vous pourriez justereturn name().toLowerCase()
yair
1
J'aime le champ clé dans le cas où vous voulez nommer l'énumération quelque chose de différent de ce que le json aura. Dans mon cas, un système hérité envoie un nom vraiment abrégé et difficile à retenir pour la valeur qu'il envoie, et je peux utiliser ce champ pour traduire en un meilleur nom pour mon énumération java.
grinch
46

Depuis Jackson 2.6, vous pouvez simplement faire ceci:

    public enum DataType {
        @JsonProperty("json")
        JSON,
        @JsonProperty("html")
        HTML
    }

Pour un exemple complet, voir l'essentiel .

Andrew Bickerton
la source
25
Notez que cela inversera le problème. Maintenant, Jackson n'acceptera que les minuscules et rejettera toutes les valeurs majuscules ou mixtes.
Pixel Elephant
30

J'ai opté pour la solution de Sam B. mais une variante plus simple.

public enum Type {
    PIZZA, APPLE, PEAR, SOUP;

    @JsonCreator
    public static Type fromString(String key) {
        for(Type type : Type.values()) {
            if(type.name().equalsIgnoreCase(key)) {
                return type;
            }
        }
        return null;
    }
}
linqu
la source
Je ne pense pas que ce soit plus simple. DataType.valueOf(key.toUpperCase())est une instanciation directe où vous avez une boucle. Cela pourrait être un problème pour un très grand nombre d'énumérations. Bien sûr, vous valueOfpouvez lancer une IllegalArgumentException, ce que votre code évite, c'est donc un bon avantage si vous préférez la vérification des null à la vérification des exceptions.
Patrick M
20

Si vous utilisez Spring Boot 2.1.xavec Jackson, 2.9vous pouvez simplement utiliser cette propriété d'application:

spring.jackson.mapper.accept-case-insensitive-enums=true

etSingh
la source
3

Pour ceux qui essaient de désérialiser Enum en ignorant la casse dans les paramètres GET , activer ACCEPT_CASE_INSENSITIVE_ENUMS ne servira à rien. Cela n'aidera pas car cette option ne fonctionne que pour la désérialisation du corps . Essayez plutôt ceci:

public class StringToEnumConverter implements Converter<String, Modes> {
    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from.toUpperCase());
    }
}

puis

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

La réponse et les exemples de code sont d' ici

Konstantin Zyubin
la source
1

Avec mes excuses à @Konstantin Zyubin, sa réponse était proche de ce dont j'avais besoin - mais je ne l'ai pas comprise, alors voici comment je pense que cela devrait se passer:

Si vous souhaitez désérialiser un type d'énumération comme insensible à la casse - c'est-à-dire que vous ne voulez pas ou ne pouvez pas modifier le comportement de l'ensemble de l'application, vous pouvez créer un désérialiseur personnalisé pour un seul type - en sous-classant StdConverteret en forçant Jackson de ne l'utiliser que sur les champs pertinents en utilisant l' JsonDeserializeannotation.

Exemple:

public class ColorHolder {

  public enum Color {
    RED, GREEN, BLUE
  }

  public static final class ColorParser extends StdConverter<String, Color> {
    @Override
    public Color convert(String value) {
      return Arrays.stream(Color.values())
        .filter(e -> e.getName().equalsIgnoreCase(value.trim()))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Invalid value '" + value + "'"));
    }
  }

  @JsonDeserialize(converter = ColorParser.class)
  Color color;
}
Guss
la source
0

Le problème est à nouveau associé à com.fasterxml.jackson.databind.util.EnumResolver . il utilise HashMap pour contenir les valeurs d'énumération et HashMap ne prend pas en charge les clés insensibles à la casse.

dans les réponses ci-dessus, tous les caractères doivent être en majuscules ou en minuscules. mais j'ai résolu tous les problèmes (in) sensibles pour les énumérations avec ça:

https://gist.github.com/bhdrk/02307ba8066d26fa1537

CustomDeserializers.java

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
import com.fasterxml.jackson.databind.util.EnumResolver;

import java.util.HashMap;
import java.util.Map;


public class CustomDeserializers extends SimpleDeserializers {

    @Override
    @SuppressWarnings("unchecked")
    public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
        return createDeserializer((Class<Enum>) type);
    }

    private <T extends Enum<T>> JsonDeserializer<?> createDeserializer(Class<T> enumCls) {
        T[] enumValues = enumCls.getEnumConstants();
        HashMap<String, T> map = createEnumValuesMap(enumValues);
        return new EnumDeserializer(new EnumCaseInsensitiveResolver<T>(enumCls, enumValues, map));
    }

    private <T extends Enum<T>> HashMap<String, T> createEnumValuesMap(T[] enumValues) {
        HashMap<String, T> map = new HashMap<String, T>();
        // from last to first, so that in case of duplicate values, first wins
        for (int i = enumValues.length; --i >= 0; ) {
            T e = enumValues[i];
            map.put(e.toString(), e);
        }
        return map;
    }

    public static class EnumCaseInsensitiveResolver<T extends Enum<T>> extends EnumResolver<T> {
        protected EnumCaseInsensitiveResolver(Class<T> enumClass, T[] enums, HashMap<String, T> map) {
            super(enumClass, enums, map);
        }

        @Override
        public T findEnum(String key) {
            for (Map.Entry<String, T> entry : _enumsById.entrySet()) {
                if (entry.getKey().equalsIgnoreCase(key)) { // magic line <--
                    return entry.getValue();
                }
            }
            return null;
        }
    }
}

Usage:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;


public class JSON {

    public static void main(String[] args) {
        SimpleModule enumModule = new SimpleModule();
        enumModule.setDeserializers(new CustomDeserializers());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(enumModule);
    }

}
bhdrk
la source
0

J'ai utilisé une modification de la solution Iago Fernández et Paul.

J'avais une énumération dans mon requestobject qui devait être insensible à la casse

@POST
public Response doSomePostAction(RequestObject object){
 //resource implementation
}



class RequestObject{
 //other params 
 MyEnumType myType;

 @JsonSetter
 public void setMyType(String type){
   myType = MyEnumType.valueOf(type.toUpperCase());
 }
 @JsonGetter
 public String getType(){
   return myType.toString();//this can change 
 }
}
soldat31
la source
0

Pour permettre la désérialisation insensible à la casse des énumérations dans jackson, ajoutez simplement la propriété ci-dessous au application.propertiesfichier de votre projet Spring Boot.

spring.jackson.mapper.accept-case-insensitive-enums=true

Si vous disposez de la version yaml du fichier de propriétés, ajoutez la propriété ci-dessous à votre application.ymlfichier.

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Vivek
la source
-1

Voici comment je gère parfois les énumérations lorsque je souhaite désérialiser de manière insensible à la casse (en s'appuyant sur le code publié dans la question):

@JsonIgnore
public void setDataType(DataType dataType)
{
  type = dataType;
}

@JsonProperty
public void setDataType(String dataType)
{
  // Clean up/validate String however you want. I like
  // org.apache.commons.lang3.StringUtils.trimToEmpty
  String d = StringUtils.trimToEmpty(dataType).toUpperCase();
  setDataType(DataType.valueOf(d));
}

Si l'énumération n'est pas triviale et donc dans sa propre classe, j'ajoute généralement une méthode d'analyse statique pour gérer les chaînes en minuscules.

Paul
la source
-1

Désérialiser l'énumération avec jackson est simple. Lorsque vous souhaitez désérialiser une énumération basée sur String, vous avez besoin d'un constructeur, d'un getter et d'un setter pour votre enum.Also classe qui utilise cette enum doit avoir un setter qui reçoit DataType en tant que paramètre, pas String:

public class Endpoint {

     public enum DataType {
        JSON("json"), HTML("html");

        private String type;

        @JsonValue
        public String getDataType(){
           return type;
        }

        @JsonSetter
        public void setDataType(String t){
           type = t.toLowerCase();
        }
     }

     public String url;
     public DataType type;

     public Endpoint() {

     }

     public void setType(DataType dataType){
        type = dataType;
     }

}

Lorsque vous avez votre json, vous pouvez désérialiser en classe Endpoint à l'aide d'ObjectMapper de Jackson:

ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
    Endpoint endpoint = mapper.readValue("{\"url\":\"foo\",\"type\":\"json\"}", Endpoint.class);
} catch (IOException e1) {
        // TODO Auto-generated catch block
    e1.printStackTrace();
}
Iago Fernández
la source