Jackson databind enum нечувствителен к регистру

106

Как десериализовать строку 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));
tom91136
источник
На какой версии Джексона ты? Взгляните на эту 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
Спасибо, теперь я могу удалить все
шаблоны из
Я лично выступаю за это в своих проектах. Если вы посмотрите на мой пример, он требует много шаблонного кода. Одним из преимуществ использования отдельных атрибутов для де / сериализации является то, что он отделяет имена важных для Java значений (имена перечислений) от важных для клиента значений (красивый вывод). Например, если нужно было изменить HTML DataType на HTML_DATA_TYPE, вы могли бы сделать это, не затрагивая внешний API, если указан ключ.
Сэм Берри,
1
Это хорошее начало, но оно потерпит неудачу, если ваше перечисление использует JsonProperty или JsonCreator. Dropwizard имеет FuzzyEnumModule, который является более надежной реализацией.
Pixel Elephant
143

Джексон 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` из String "male": значение не является одним из объявленных имен экземпляров Enum: [FAMALE, MALE]
Jordan Silva
@JordanSilva, конечно, работает с v2.9.2. Я добавил полный пример кода с тестами для проверки. Я не знаю, что могло бы случиться в вашем случае, но запуск кода примера с jackson-databind2.9.2 работает именно так, как ожидалось.
davnicwil
8
используя Spring Boot, вы можете просто добавить свойствоspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva, может быть, вы пытаетесь десериализовать перечисление в параметрах получения, как я? =) Я решил свою проблему и ответил здесь. Надеюсь, это поможет
Константин Зюбин
86

Я столкнулся с этой же проблемой в своем проекте, мы решили построить наши перечисления со строковым ключом и использовать @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 .
Сэм Берри
1
Мне понравился этот подход, но я выбрал менее подробный вариант, см. Ниже.
linqu
2
видимо keyполе не нужно. В getKey, можно простоreturn name().toLowerCase()
Яир
1
Мне нравится ключевое поле в случае, когда вы хотите назвать перечисление чем-то отличным от того, что будет в json. В моем случае устаревшая система отправляет действительно сокращенное и трудно запоминающееся имя для значения, которое она отправляет, и я могу использовать это поле для перевода на лучшее имя для моего java enum.
Гринч
50

Начиная с версии Jackson 2.6, вы можете просто сделать это:

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

Для полного примера см. Эту суть .

Эндрю Бикертон
источник
27
Обратите внимание, что это решит проблему. Теперь Джексон будет принимать только строчные буквы и отклонять любые значения верхнего или смешанного регистра.
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;
    }
}
Linqu
источник
Не думаю, что это проще. DataType.valueOf(key.toUpperCase())является прямым экземпляром, в котором у вас есть цикл. Это может быть проблемой для очень большого числа перечислений. Конечно, valueOfможет возникнуть исключение IllegalArgumentException, которого ваш код избегает, так что это хорошее преимущество, если вы предпочитаете нулевую проверку проверке исключений.
Patrick M
24

Если вы используете Spring Boot 2.1.xс Jackson, 2.9вы можете просто использовать это свойство приложения:

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

etSingh
источник
4

Для тех, кто пытается десериализовать 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;
}
Гусс
источник
1

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

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

Если у вас есть yaml-версия файла свойств, добавьте в application.ymlфайл свойство ниже .

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Вивек
источник
Спасибо, можете подтвердить, что это работает в Spring Boot.
Йоонас Вали
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);
    }

}
bhdrk
источник
0

Я использовал модификацию решения Яго Фернандеса и Пола.

У меня было перечисление в моем Requestobject, которое должно было быть нечувствительным к регистру

@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 
 }
}
солдат31
источник
-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 Джексона:

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();
}
Яго Фернандес
источник