Jackson databind enum insensível a maiúsculas

97

Como posso desserializar a string JSON que contém valores enum que não diferenciam maiúsculas de minúsculas? (usando Jackson Databind)

A string JSON:

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

e meu Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

neste caso, desserializar o JSON com "type":"json"falharia onde "type":"JSON"funcionaria. Mas eu quero "json"trabalhar também por motivos de convenção de nomenclatura.

Serializar o POJO também resulta em letras maiúsculas "type":"JSON"

Pensei em usar @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 funcionou. Mas eu queria saber se há uma solução melhor, porque isso parece um hack para mim.

Também posso escrever um desserializador personalizado, mas tenho muitos POJOs diferentes que usam enums e seria difícil de manter.

Alguém pode sugerir uma maneira melhor de serializar e desserializar enums com a convenção de nomenclatura adequada?

Não quero que meus enums em java sejam minúsculos!

Aqui está um código de teste que usei:

    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
fonte
Em qual versão de Jackson você está? Dê uma olhada neste JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov
Estou usando o Jackson 2.2.3
tom91136
OK, acabei de atualizar para 2.4.0-RC3
tom91136

Respostas:

38

Na versão 2.4.0, você pode registrar um serializador personalizado para todos os tipos de Enum ( link para o problema do github). Além disso, você pode substituir o desserializador Enum padrão por conta própria, que estará ciente do tipo de Enum. Aqui está um exemplo:

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

Resultado:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
fonte
1
Obrigado, agora posso remover todo o clichê do meu POJO :)
tom91136
Pessoalmente, estou defendendo isso em meus projetos. Se você olhar meu exemplo, ele requer muito código clichê. Um benefício de usar atributos separados para desserialização é que ele separa os nomes dos valores importantes do Java (nomes de enum) para valores importantes do cliente (impressão bonita). Por exemplo, se fosse desejado alterar o DataType HTML para HTML_DATA_TYPE, você poderia fazer isso sem afetar a API externa, se houver uma chave especificada.
Sam Berry,
1
Este é um bom começo, mas falhará se seu enum estiver usando JsonProperty ou JsonCreator. O Dropwizard tem FuzzyEnumModule, que é uma implementação mais robusta.
Pixel Elephant
131

Jackson 2.9

Isso agora é muito simples, usando jackson-databind2.9.0 e superior

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

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

Exemplo completo com testes

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
fonte
3
Este aqui é ouro!
Vikas Prasad
8
Estou usando 2.9.2 e não funciona. Causado por: com.fasterxml.jackson.databind.exc.InvalidFormatException: Não é possível desserializar o valor do tipo .... Gender` from String "male": o valor não é um dos nomes de instância de Enum declarados: [FAMALE, MALE]
Jordan Silva
@JordanSilva certamente funciona com a v2.9.2. Eu adicionei um exemplo de código completo com testes para verificação. Não sei o que pode ter acontecido no seu caso, mas executar o código de exemplo com jackson-databind2.9.2 funciona especificamente como esperado.
davnicwil
4
usando Spring Boot, você pode simplesmente adicionar a propriedadespring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva talvez você esteja tentando desserializar enum em get parameters como eu fiz? =) Resolvi meu problema e respondi aqui. Espero que possa ajudar
Konstantin Zyubin
83

Eu encontrei esse mesmo problema em meu projeto, decidimos construir nossos enums com uma chave de string e usar @JsonValuee um construtor estático para serialização e desserialização, respectivamente.

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
fonte
1
Deve ser assim DataType.valueOf(key.toUpperCase())- caso contrário, você realmente não mudou nada. Codificando defensivamente para evitar um NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont
2
Boa captura @sarumont. Eu fiz a edição. Além disso, o método foi renomeado para "fromString" para funcionar bem com JAX-RS .
Sam Berry,
1
Eu gostei dessa abordagem, mas optei por uma variante menos prolixa, veja abaixo.
linqu
2
aparentemente, o keycampo é desnecessário. Em getKey, você poderia apenasreturn name().toLowerCase()
yair
1
Eu gosto do campo-chave no caso em que você deseja nomear o enum com algo diferente do que o json terá. No meu caso, um sistema legado envia um nome realmente abreviado e difícil de lembrar para o valor que envia, e posso usar esse campo para traduzir para um nome melhor para meu enum java.
grinch
45

Desde o Jackson 2.6, você pode simplesmente fazer isso:

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

Para um exemplo completo, veja esta essência .

Andrew Bickerton
fonte
25
Observe que isso reverterá o problema. Agora, Jackson só aceitará letras minúsculas e rejeitará quaisquer valores em maiúsculas ou misturas.
Pixel Elephant
30

Optei pela solução de Sam B., mas uma variante mais simples.

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
fonte
Não acho que seja mais simples. DataType.valueOf(key.toUpperCase())é uma instanciação direta onde você tem um loop. Isso poderia ser um problema para um enum muito numeroso. Claro, valueOfpode lançar uma IllegalArgumentException, que seu código evita, portanto, é um bom benefício se você preferir a verificação nula à verificação de exceção.
Patrick M
20

Se estiver usando Spring Boot 2.1.xcom Jackson, 2.9você pode simplesmente usar esta propriedade do aplicativo:

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

etSingh
fonte
3

Para aqueles que tentam desserializar Enum ignorando maiúsculas e minúsculas nos parâmetros GET , habilitar ACCEPT_CASE_INSENSITIVE_ENUMS não vai adiantar nada. Não vai ajudar porque essa opção só funciona para desserialização do corpo . Em vez disso, tente isto:

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

e depois

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

A resposta e os exemplos de código são daqui

Konstantin Zyubin
fonte
1

Com minhas desculpas a @Konstantin Zyubin, sua resposta foi quase o que eu precisava - mas não entendi, então é assim que acho que deveria ser:

Se você deseja desserializar um tipo de enum como não diferencia maiúsculas de minúsculas - ou seja, você não deseja, ou não pode modificar o comportamento de todo o aplicativo, você pode criar um desserializador personalizado apenas para um tipo - por subclassificação StdConvertere força Jackson para usá-lo apenas nos campos relevantes usando a JsonDeserializeanotação.

Exemplo:

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
fonte
0

O problema é relatado para com.fasterxml.jackson.databind.util.EnumResolver . ele usa HashMap para manter valores enum e HashMap não oferece suporte a chaves que não diferenciam maiúsculas de minúsculas.

nas respostas acima, todos os caracteres devem ser maiúsculos ou minúsculos. mas corrigi todos os problemas (in) sensíveis para enums com isso:

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

Uso:

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
fonte
0

Usei uma modificação da solução de Iago Fernández e Paul.

Eu tinha um enum em meu objeto de solicitação que não diferenciava maiúsculas de minúsculas

@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 
 }
}
trooper 31
fonte
0

Para permitir a desserialização sem distinção entre maiúsculas e minúsculas de enums em jackson, basta adicionar a propriedade abaixo ao application.propertiesarquivo do seu projeto de inicialização.

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

Se você tiver a versão yaml do arquivo de propriedades, adicione a propriedade abaixo ao seu application.ymlarquivo.

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

É assim que às vezes lido com enums quando desejo desserializar sem fazer distinção entre maiúsculas e minúsculas (com base no código postado na pergunta):

@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 o enum não for trivial e, portanto, estiver em sua própria classe, geralmente adiciono um método de análise estática para lidar com Strings em minúsculas.

Paulo
fonte
-1

Desserializar enum com jackson é simples. Quando você deseja desserializar enum com base em String, precisa de um construtor, um getter e um setter para seu enum. Além disso, a classe que usa esse enum deve ter um setter que recebe DataType como parâmetro, não 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 você tem seu json, pode desserializar para a classe Endpoint usando 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
fonte