Jackson databind enum case insensitive


99

Come posso deserializzare una stringa JSON che contiene valori enum che non fanno distinzione tra maiuscole e minuscole? (utilizzando Jackson Databind)

La stringa JSON:

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

e il mio Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

in questo caso, la deserializzazione del JSON con "type":"json"fallirà dove "type":"JSON"funzionerebbe. Ma voglio "json"lavorare anche per nominare motivi di convenzione.

Serializzare il POJO risulta anche in lettere maiuscole "type":"JSON"

Ho pensato di usare @JsonCreatore @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();
    }

E ha funzionato. Ma mi chiedevo se esista una soluzione migliore perché a me sembra un trucco.

Posso anche scrivere un deserializzatore personalizzato, ma ho molti POJO diversi che utilizzano enumerazioni e sarebbe difficile da mantenere.

Qualcuno può suggerire un modo migliore per serializzare e deserializzare le enumerazioni con una convenzione di denominazione appropriata?

Non voglio che le mie enumerazioni in java siano minuscole!

Ecco un po 'di codice di prova che ho usato:

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

In quale versione di Jackson sei? Dai un'occhiata a questo JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov

Sto usando Jackson 2.2.3
tom91136

OK, ho appena aggiornato a 2.4.0-RC3
tom91136

Risposte:


38

Nella versione 2.4.0 è possibile registrare un serializzatore personalizzato per tutti i tipi di Enum ( collegamento al problema di github). Inoltre puoi sostituire da solo il deserializzatore Enum standard che sarà a conoscenza del tipo Enum. Ecco un esempio:

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

Produzione:

["json","html"]
[JSON, HTML]

1
Grazie, ora posso rimuovere tutto il boilerplate nel mio POJO :)
tom91136

Personalmente lo sto sostenendo nei miei progetti. Se guardi il mio esempio, richiede molto codice boilerplate. Un vantaggio dell'utilizzo di attributi separati per la de / serializzazione è che disaccoppia i nomi dei valori importanti per Java (nomi enum) in valori importanti per il client (piuttosto stampato). Ad esempio, se si desidera modificare il DataType HTML in HTML_DATA_TYPE, è possibile farlo senza influire sull'API esterna, se è stata specificata una chiave.
Sam Berry

1
Questo è un buon inizio ma fallirà se la tua enumerazione utilizza JsonProperty o JsonCreator. Dropwizard ha FuzzyEnumModule che è un'implementazione più robusta.
Pixel Elephant

134

Jackson 2.9

Ora è molto semplice, utilizzando jackson-databind2.9.0 e versioni successive

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

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

Esempio completo con test

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

3
Questo è d'oro!
Vikas Prasad

8
Sto usando 2.9.2 e non funziona. Causato da: com.fasterxml.jackson.databind.exc.InvalidFormatException: Impossibile deserializzare il valore di tipo .... Gender` dalla stringa "male": valore non uno dei nomi di istanza Enum dichiarati: [FAMALE, MALE]
Jordan Silva

@JordanSilva sicuramente funziona con la v2.9.2. Ho aggiunto un esempio di codice completo con test per la verifica. Non so cosa potrebbe essere successo nel tuo caso, ma l'esecuzione del codice di esempio con jackson-databind2.9.2 funziona specificamente come previsto.
davnicwil

5
utilizzando Spring Boot, puoi semplicemente aggiungere la proprietàspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister

1
@JordanSilva forse stai cercando di deserializzare enum in get parameters come ho fatto io? =) Ho risolto il mio problema e ho risposto qui. Spero che possa aiutare
Konstantin Zyubin

83

Mi sono imbattuto in questo stesso problema nel mio progetto, abbiamo deciso di creare le nostre enumerazioni con una chiave di stringa e use @JsonValuee un costruttore statico rispettivamente per la serializzazione e la deserializzazione.

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

1
Questo dovrebbe essere DataType.valueOf(key.toUpperCase())- altrimenti, non hai davvero cambiato nulla. Codifica difensiva per evitare un NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont

2
Buona cattura @sarumont. Ho effettuato la modifica. Inoltre, metodo rinominato in "fromString" per giocare bene con JAX-RS .
Sam Berry

1
Mi è piaciuto questo approccio, ma ho optato per una variante meno prolissa, vedi sotto.
Linqu

2
a quanto pare il keycampo non è necessario. In getKey, potresti semplicementereturn name().toLowerCase()
yair

1
Mi piace il campo chiave nel caso in cui vuoi nominare l'enum in modo diverso da quello che avrà il json. Nel mio caso un sistema legacy invia un nome davvero abbreviato e difficile da ricordare per il valore che invia, e posso usare questo campo per tradurre in un nome migliore per la mia enumerazione java.
grinch

47

A partire da Jackson 2.6, puoi semplicemente fare questo:

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

Per un esempio completo, vedi questo succo .


26
Tieni presente che questa operazione risolverà il problema. Ora Jackson accetterà solo lettere minuscole e rifiuterà qualsiasi valore maiuscolo o misto.
Pixel Elephant

30

Ho optato per la soluzione di Sam B. ma una variante più semplice.

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

Non credo che sia più semplice. DataType.valueOf(key.toUpperCase())è un'istanza diretta in cui hai un ciclo. Questo potrebbe essere un problema per una enumerazione molto numerosa. Naturalmente, valueOfpuò generare un'eccezione IllegalArgumentException, che il codice evita, quindi è un buon vantaggio se si preferisce il controllo null al controllo delle eccezioni.
Patrick M

22

Se stai usando Spring Boot 2.1.xcon Jackson 2.9puoi semplicemente usare questa proprietà dell'applicazione:

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


3

Per coloro che tentano di deserializzare Enum ignorando le maiuscole e le minuscole nei parametri GET , abilitare ACCEPT_CASE_INSENSITIVE_ENUMS non servirà a nulla. Non aiuta perché questa opzione funziona solo per la deserializzazione del corpo . Prova invece questo:

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

e poi

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

La risposta e gli esempi di codice provengono da qui


1

Con le scuse a @Konstantin Zyubin, la sua risposta era vicina a ciò di cui avevo bisogno, ma non l'ho capito, quindi ecco come penso che dovrebbe andare:

Se desideri deserializzare un tipo di enumerazione senza distinzione tra maiuscole e minuscole, ovvero non vuoi o non puoi modificare il comportamento dell'intera applicazione, puoi creare un deserializzatore personalizzato solo per un tipo, sottoclassando StdConvertere forzando Jackson di usarlo solo nei campi pertinenti utilizzando l' JsonDeserializeannotazione.

Esempio:

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

0

Il problema è relativo a com.fasterxml.jackson.databind.util.EnumResolver . usa HashMap per contenere i valori enum e HashMap non supporta chiavi senza distinzione tra maiuscole e minuscole.

nelle risposte precedenti, tutti i caratteri devono essere maiuscoli o minuscoli. ma ho risolto tutti i problemi (in) sensibili per le enumerazioni con questo:

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

Utilizzo:

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

}

0

Ho usato una modifica della soluzione di Iago Fernández e Paul.

Avevo un enum nel mio requestobject che doveva essere senza distinzione tra maiuscole e minuscole

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

0

Per consentire la deserializzazione senza distinzione tra maiuscole e minuscole delle enumerazioni in jackson, aggiungi semplicemente la proprietà seguente al application.propertiesfile del tuo progetto di avvio primaverile.

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

Se hai la versione yaml del file delle proprietà, aggiungi la proprietà di seguito al tuo application.ymlfile.

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true

-1

Ecco come a volte gestisco le enumerazioni quando voglio deserializzare senza distinzione tra maiuscole e minuscole (basandosi sul codice pubblicato nella domanda):

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

Se l'enum non è banale e quindi nella sua classe di solito aggiungo un metodo di analisi statico per gestire le stringhe minuscole.


-1

Deserializzare enum con jackson è semplice. Quando si desidera deserializzare l'enumerazione basata su String, è necessario un costruttore, un getter e un setter per l'enum. Anche la classe che usa quell'enumerazione deve avere un setter che riceve DataType come parametro, non 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;
     }

}

Quando hai il tuo json, puoi deserializzare alla classe Endpoint utilizzando ObjectMapper di 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();
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.