Джексон не враховує регістр перепису даних


99

Як я можу десеріалізувати рядок JSON, що містить значення переліку, які не враховують регістр? (за допомогою Jackson Databind)

Рядок JSON:

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

і моя Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

у цьому випадку десериалізація JSON за допомогою "type":"json"не спрацює, де "type":"JSON"б це працювало. Але я хочу "json"також працювати над причинами прийняття імен.

Серіалізація POJO також призводить до верхнього регістру "type":"JSON"

Я думав використовувати @JsonCreatorта @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();
    }

І це спрацювало. Але мені було цікаво, чи є кращий варіант, тому що це для мене виглядає як хак.

Я також можу написати власний десериалізатор, але я отримав багато різних POJO, які використовують перерахування, і це було б важко підтримувати.

Хтось може запропонувати кращий спосіб серіалізації та десеріалізації перелічень за допомогою правильної конвенції про іменування?

Я не хочу, щоб мої переліки в Java були малими!

Ось декілька тестових кодів, які я використовував:

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

На якій версії Джексона ви? Погляньте на цей JIRA jira.codehaus.org/browse/JACKSON-861
Олексій Гаврилов

Я використовую Jackson 2.2.3
tom91136

Добре, я щойно оновився до 2.4.0-RC3
tom91136,

Відповіді:


38

У версії 2.4.0 ви можете зареєструвати власний серіалізатор для всіх типів Enum ( посилання на випуск github). Також ви можете самостійно замінити стандартний десериалізатор Enum, який знатиме про тип Enum. Ось приклад:

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

Вихід:

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

1
Дякую, тепер я можу зняти весь шаблон в своєму POJO :)
tom91136

Я особисто виступаю за це у своїх проектах. Якщо ви подивитесь на мій приклад, він вимагає багато типового коду. Однією з переваг використання окремих атрибутів для де / серіалізації є те, що він відокремлює імена важливих для Java значень (імена перерахувань) до важливих для клієнта значень (гарний принт). Наприклад, якщо потрібно було змінити тип даних HTML на HTML_DATA_TYPE, ви можете зробити це, не впливаючи на зовнішній API, якщо вказаний ключ.
Сем Беррі

1
Це хороший початок, але він не вдасться, якщо ваш перелік використовує JsonProperty або JsonCreator. Dropwizard має FuzzyEnumModule, який є більш надійною реалізацією.
Pixel Elephant

134

Джексон 2,9

Зараз це дуже просто, використовуючи jackson-databind2.9.0 і вище

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

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

Повний приклад з тестами

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
Цей - золото!
Вікас Прасад

8
Я використовую 2.9.2, і він не працює. Викликано: com.fasterxml.jackson.databind.exc.InvalidFormatException: Не вдається десеріалізувати значення типу .... Gender` з рядка "male": значення не одне з оголошених назв екземплярів Enum: [FAMALE, MALE]
Jordan Silva

@JordanSilva, безумовно, працює з v2.9.2. Я додав повний приклад коду з тестами для перевірки. Я не знаю, що могло статися у вашому випадку, але запуск прикладу коду з jackson-databind2.9.2 працює спеціально, як очікувалося.
davnicwil

5
використовуючи Spring Boot, ви можете просто додати властивістьspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister

1
@JordanSilva, можливо, ти намагаєшся десериалізувати перерахування в параметрах отримання, як я це зробив? =) Я вирішив свою проблему і відповів тут. Сподіваюся, це може допомогти
Костянтин Зюбін,

83

Я зіткнувся з цією ж проблемою у своєму проекті, ми вирішили побудувати наші переліки за допомогою рядкового ключа та використання @JsonValueта статичного конструктора для серіалізації та десеріалізації відповідно.

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
Це повинно бути DataType.valueOf(key.toUpperCase())- інакше ви насправді нічого не змінили. Кодування оборонно, щоб уникнути NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont

2
Хороший улов @sarumont. Я зробив редагування. Крім того, перейменований метод на "fromString", щоб гарно грати з JAX-RS .
Sam Berry

1
Мені сподобався такий підхід, але я вибрав менш детальний варіант, див. Нижче.
linqu

2
мабуть keyполе непотрібне. В getKey, можна простоreturn name().toLowerCase()
Яір

1
Мені подобається ключове поле у ​​випадку, коли ви хочете назвати перелічення інакшим, ніж те, що буде мати json. У моєму випадку застаріла система надсилає дійсно скорочене і важко запам'ятовується ім'я для значення, яке воно надсилає, і я можу використовувати це поле для перекладу на краще ім'я для мого переліку Java.
grinch

47

Починаючи з Jackson 2.6, ви можете просто зробити це:

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

Повний приклад див. У цій суті .


26
Зверніть увагу, що це призведе до зміни проблеми. Тепер Джексон приймає лише малі літери та відхиляє будь-які великі або змішані регістри.
Pixel Elephant,

30

Я пішов на рішення Сем Б., але більш простий варіант.

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

Я не думаю, що це простіше. DataType.valueOf(key.toUpperCase())це прямий екземпляр, де у вас є цикл. Це може бути проблемою для дуже численного переліку. Звичайно, ви valueOfможете викинути IllegalArgumentException, якого ваш код уникає, тому це гарна перевага, якщо ви віддаєте перевагу нульовій перевірці, а не винятковій.
Патрік М

22

Якщо ви використовуєте Spring Boot 2.1.xз Jackson, 2.9ви можете просто використовувати цю властивість програми:

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


3

Для тих, хто намагається десеріалізувати Enum, ігноруючи регістр у параметрах GET , увімкнення ACCEPT_CASE_INSENSITIVE_ENUMS не принесе користі. Це не допоможе, оскільки цей параметр працює лише для десеріалізації тіла . Натомість спробуйте це:

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

і потім

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Відповіді та зразки коду звідси


1

З вибаченнями перед @Konstantin Zyubin, його відповідь була близькою до того, що мені потрібно - але я цього не зрозумів, тож ось, як я думаю, це має йти:

Якщо ви хочете десеріалізувати один тип перелічення як чутливий до регістру - тобто ви не хочете або не можете змінити поведінку всієї програми, ви можете створити власний десериалізатор лише для одного типу - шляхом підкласування StdConverterта примусового використання Джексона використовувати його лише у відповідних полях за допомогою JsonDeserializeанотації.

Приклад:

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

Проблема надіслана до com.fasterxml.jackson.databind.util.EnumResolver . він використовує HashMap для зберігання значень переліку, а HashMap не підтримує чутливі до регістру ключі.

у відповідях вище всі символи повинні бути великими або малими. але я виправив усі (не) чутливі проблеми для перерахувань таким чином:

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

Використання:

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

Я використовував модифікацію рішення Яго Фернандеса та Пола.

У моєму запитному об’єкті було перерахування, яке мало враховувати регістр

@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

Щоб дозволити нечутливу до регістру десеріалізацію перелічень у jackson, просто додайте властивість, наведену нижче, до application.propertiesфайлу вашого проекту весняного завантаження.

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

Якщо у вас є файл властивостей yaml, додайте властивість нижче до свого application.ymlфайлу.

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

-1

Ось як я іноді обробляю перелічення, коли хочу десеріалізувати, не враховуючи регістр (спираючись на код, розміщений у питанні):

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

Якщо перерахування є нетривіальним і, отже, у власному класі я зазвичай додаю метод статичного синтаксичного аналізу для обробки рядків нижнього регістру.


-1

Десериалізувати перерахування за допомогою Джексона просто. Коли ви хочете десериалізувати перелічення, засноване на String, потрібен конструктор, геттер та сеттер для вашого переліку, а також клас, який використовує цей перелік, повинен мати сеттер, який отримує DataType як параметр, а не 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;
     }

}

Коли у вас є json, ви можете десериалізувати клас Endpoint, використовуючи ObjectMapper від 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();
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.