Jackson databind enum nie uwzględnia wielkości liter

106

Jak mogę deserializować ciąg JSON, który zawiera wartości wyliczenia, w których wielkość liter nie jest rozróżniana? (używając Jackson Databind)

Ciąg JSON:

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

i mój Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

w takim przypadku deserializacja JSON z "type":"json"nie powiedzie się w miejscu, w którym "type":"JSON"działałoby. Ale chcę "json"pracować również ze względu na konwencję nazewnictwa.

Serializacja POJO skutkuje również pisaniem wielkich liter "type":"JSON"

Myślałem o użyciu @JsonCreatori @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();
    }

I zadziałało. Ale zastanawiałem się, czy istnieje lepsze rozwiązanie, ponieważ dla mnie wygląda to na włamanie.

Mogę również napisać niestandardowy deserializator, ale mam wiele różnych POJO, które używają wyliczeń i byłoby to trudne do utrzymania.

Czy ktoś może zaproponować lepszy sposób serializacji i deserializacji wyliczeń z odpowiednią konwencją nazewnictwa?

Nie chcę, aby moje wyliczenia w Javie były pisane małymi literami!

Oto kod testowy, którego użyłem:

    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
źródło
W której wersji Jacksona grasz? Spójrz na to JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov
Używam Jacksona 2.2.3
tom91136
OK, właśnie zaktualizowałem do wersji 2.4.0-RC3
tom91136

Odpowiedzi:

38

W wersji 2.4.0 możesz zarejestrować własny serializator dla wszystkich typów Enum ( link do sprawy na githubie). Możesz również samodzielnie zastąpić standardowy deserializator wyliczenia, który będzie świadomy typu wyliczenia. Oto przykład:

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

Wynik:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
źródło
1
Dzięki, teraz mogę usunąć wszystkie boilerplate z mojego POJO :)
tom91136
Osobiście opowiadam się za tym w moich projektach. Jeśli spojrzysz na mój przykład, wymaga on wielu standardowych kodów. Jedną z zalet używania oddzielnych atrybutów do de / serializacji jest to, że rozdziela nazwy ważnych dla Javy wartości (nazwy wyliczeń) od wartości ważnych dla klienta (ładny wydruk). Na przykład, jeśli pożądana byłaby zmiana HTML DataType na HTML_DATA_TYPE, można to zrobić bez wpływu na zewnętrzne API, jeśli określono klucz.
Sam Berry,
1
To dobry początek, ale zakończy się niepowodzeniem, jeśli wyliczenie używa JsonProperty lub JsonCreator. Dropwizard ma FuzzyEnumModule, który jest bardziej niezawodną implementacją.
Pixel Elephant
141

Jackson 2.9

Jest to teraz bardzo proste przy użyciu wersji jackson-databind2.9.0 i nowszych

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

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

Pełny przykład z testami

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
źródło
3
Ten jest złoty!
Vikas Prasad
8
Używam wersji 2.9.2 i to nie działa. Przyczyna: com.fasterxml.jackson.databind.exc.InvalidFormatException: nie można deserializować wartości typu .... Płeć` z ciągu znaków „mężczyzna”: wartość nie jest jedną z zadeklarowanych nazw instancji Enum: [FAMALE, MALE]
Jordan Silva
@JordanSilva, to z pewnością działa z wersją 2.9.2. Dodałem pełny przykład kodu z testami do weryfikacji. Nie wiem, co mogło się stać w twoim przypadku, ale uruchomienie przykładowego kodu w wersji jackson-databind2.9.2 działa zgodnie z oczekiwaniami.
davnicwil
8
używając Spring Boot, możesz po prostu dodać właściwośćspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva może próbujesz deserializować wyliczenie w parametrach pobierania, tak jak ja? =) Rozwiązałem mój problem i odpowiedziałem tutaj. Mam nadzieję, że to pomoże
Konstantin Zyubin
86

Natknąłem się na ten sam problem w moim projekcie, zdecydowaliśmy się zbudować nasze wyliczenia z kluczem ciągu i użyć @JsonValueoraz konstruktora statycznego odpowiednio do serializacji i deserializacji.

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
źródło
1
Tak powinno być DataType.valueOf(key.toUpperCase())- w przeciwnym razie tak naprawdę nic nie zmieniłeś. Kodowanie defensywne, aby uniknąć NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont
2
Dobry chwyt @sarumont. Dokonałem edycji. Ponadto zmieniono nazwę metody na „fromString”, aby ładnie grać z JAX-RS .
Sam Berry
1
Podobało mi się to podejście, ale wybrałem mniej szczegółowy wariant, patrz poniżej.
linqu
2
najwyraźniej keypole jest niepotrzebne. W getKey, możesz po prostureturn name().toLowerCase()
yair
1
Podoba mi się kluczowe pole w przypadku, gdy chcesz nazwać enum coś innego niż to, co będzie miało json. W moim przypadku starszy system wysyła naprawdę skróconą i trudną do zapamiętania nazwę dla wysyłanej wartości i mogę użyć tego pola, aby przetłumaczyć na lepszą nazwę dla mojego wyliczenia java.
grinch
49

Od wersji Jacksona 2.6 możesz po prostu zrobić to:

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

Pełny przykład można znaleźć w tym skrócie .

Andrew Bickerton
źródło
26
Zauważ, że wykonanie tej czynności odwróci problem. Teraz Jackson akceptuje tylko małe litery i odrzuca wszystkie wielkie lub mieszane wartości.
Pixel Elephant,
30

Poszedłem na rozwiązanie Sama B., ale prostszy wariant.

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
źródło
Nie sądzę, żeby to było prostsze. DataType.valueOf(key.toUpperCase())to bezpośrednia instancja, w której masz pętlę. Może to stanowić problem w przypadku bardzo wielu wyliczeń. Oczywiście valueOfmoże zgłosić wyjątek IllegalArgumentException, którego kod unika, więc jest to dobra zaleta, jeśli wolisz sprawdzanie wartości null od sprawdzania wyjątków.
Patrick M
24

Jeśli używasz Spring Boot 2.1.xz Jacksonem 2.9, możesz po prostu użyć tej właściwości aplikacji:

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

etSingh
źródło
4

Dla tych, którzy próbują deserializować Enum, ignorując wielkość liter w parametrach GET , włączenie ACCEPT_CASE_INSENSITIVE_ENUMS nie przyniesie nic dobrego. To nie pomoże, ponieważ ta opcja działa tylko w przypadku deserializacji treści . Zamiast tego spróbuj tego:

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

i wtedy

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Odpowiedzi i próbki kodu pochodzą stąd

Konstantin Zyubin
źródło
1

Z przeprosinami dla @Konstantin Zyubin, jego odpowiedź była zbliżona do tego, czego potrzebowałem - ale tego nie rozumiałem, więc myślę, że powinno to wyglądać następująco:

Jeśli chcesz deserializować jeden typ wyliczenia jako niewrażliwy na wielkość liter - tj. Nie chcesz lub nie możesz modyfikować zachowania całej aplikacji, możesz utworzyć niestandardowy deserializator tylko dla jednego typu - przez podklasy StdConverteri wymuszenie Jackson, aby używać go tylko na odpowiednich polach przy użyciu rozszerzeniaJsonDeserialize adnotacji.

Przykład:

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
źródło
1

Aby umożliwić deserializację wyliczeń bez uwzględniania wielkości liter w języku jackson, po prostu dodaj poniższą właściwość do application.propertiespliku projektu Spring Boot.

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

Jeśli masz wersję YAML pliku properties, dodaj poniższą właściwość do swojego application.ymlpliku.

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Vivek
źródło
Dzięki, mogę potwierdzić, że to działa w Spring Boot.
Joonas Vali
0

Problem został zgłoszony do com.fasterxml.jackson.databind.util.EnumResolver . używa HashMap do przechowywania wartości wyliczeniowych, a HashMap nie obsługuje kluczy bez rozróżniania wielkości liter.

w powyższych odpowiedziach wszystkie znaki powinny być pisane dużymi lub małymi literami. ale naprawiłem wszystkie (nie) wrażliwe problemy dla wyliczeń z tym:

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

Stosowanie:

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
źródło
0

Użyłem modyfikacji rozwiązania Iago Fernándeza i Paula.

Miałem wyliczenie w moim obiekcie żądania, które musiało być niewrażliwe na wielkość liter

@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 
 }
}
trooper31
źródło
-1

Oto, jak czasami radzę sobie z wyliczeniami, gdy chcę deserializować bez uwzględniania wielkości liter (na podstawie kodu zamieszczonego w pytaniu):

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

Jeśli wyliczenie jest nietrywialne, a zatem w jego własnej klasie zwykle dodaję statyczną metodę analizy do obsługi ciągów z małymi literami.

Paweł
źródło
-1

Deserializacja wyliczenia z Jacksonem jest prosta. Jeśli chcesz deserializować wyliczenie oparte na String, potrzebujesz konstruktora, metody pobierającej i ustawiającej do twojego wyliczenia.Również klasa używająca tego wyliczenia musi mieć metodę ustawiającą, która odbiera DataType jako parametr, a nie 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;
     }

}

Gdy masz już swój json, możesz deserializować do klasy Endpoint za pomocą ObjectMapper of 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
źródło