Jackson databind enum no distingue entre mayúsculas y minúsculas

99

¿Cómo puedo deserializar una cadena JSON que contiene valores de enumeración que no distinguen entre mayúsculas y minúsculas? (usando Jackson Databind)

La cadena JSON:

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

y mi POJO de Java:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

en este caso, la deserialización del JSON con "type":"json"fallaría donde "type":"JSON"funcionaría. Pero quiero "json"trabajar también por razones de convenciones de nombres.

Serializar el POJO también resulta en mayúsculas "type":"JSON"

Pensé en usar @JsonCreatory @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();
    }

Y funcionó. Pero me preguntaba si hay una mejor solución porque esto me parece un truco.

También puedo escribir un deserializador personalizado, pero tengo muchos POJO diferentes que usan enumeraciones y sería difícil de mantener.

¿Alguien puede sugerir una mejor manera de serializar y deserializar enumeraciones con la convención de nomenclatura adecuada?

¡No quiero que mis enumeraciones en Java estén en minúsculas!

Aquí hay un código de prueba que utilicé:

    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
fuente
¿En qué versión de Jackson estás? Echa un vistazo a este JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov
Estoy usando Jackson 2.2.3
tom91136
OK, acabo de actualizar a 2.4.0-RC3
tom91136

Respuestas:

38

En la versión 2.4.0 puede registrar un serializador personalizado para todos los tipos de Enum ( enlace al problema de github). También puede reemplazar el deserializador estándar de Enum por su cuenta que estará al tanto del tipo de Enum. Aquí hay un ejemplo:

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

Salida:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
fuente
1
Gracias, ahora puedo eliminar todo el texto estándar en mi POJO :)
tom91136
Yo personalmente abogo por esto en mis proyectos. Si miras mi ejemplo, requiere mucho código repetitivo. Un beneficio de usar atributos separados para la deserialización es que desacopla los nombres de los valores importantes de Java (nombres de enumeración) a los valores importantes del cliente (letra bonita). Por ejemplo, si desea cambiar el tipo de datos HTML a HTML_DATA_TYPE, puede hacerlo sin afectar la API externa, si hay una clave especificada.
Sam Berry
1
Este es un buen comienzo, pero fallará si su enumeración está usando JsonProperty o JsonCreator. Dropwizard tiene FuzzyEnumModule, que es una implementación más robusta.
Pixel Elephant
134

Jackson 2.9

Esto ahora es muy simple, usando jackson-databind2.9.0 y superior

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

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

Ejemplo completo con pruebas

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
fuente
3
¡Este es de oro!
Vikas Prasad
8
Estoy usando 2.9.2 y no funciona. Causado por: com.fasterxml.jackson.databind.exc.InvalidFormatException: No se puede deserializar el valor del tipo .... Gender` from String "male": el valor no es uno de los nombres de instancia declarados de Enum: [FAMALE, MALE]
Jordan Silva
@JordanSilva ciertamente funciona con v2.9.2. Agregué un ejemplo de código completo con pruebas para verificación. No sé qué podría haber sucedido en su caso, pero ejecutar el código de ejemplo con jackson-databind2.9.2 funciona específicamente como se esperaba.
davnicwil
5
usando Spring Boot, simplemente puede agregar la propiedadspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva ¿tal vez estás tratando de deserializar la enumeración en obtener parámetros como lo hice? =) He resuelto mi problema y respondí aquí. Espero que pueda ayudar
Konstantin Zyubin
83

Me encontré con este mismo problema en mi proyecto, decidimos construir nuestras enumeraciones con una clave de cadena y uso @JsonValuey un constructor estático para serialización y deserialización 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
fuente
1
Esto debería ser DataType.valueOf(key.toUpperCase()); de lo contrario, realmente no ha cambiado nada. Codificación defensiva para evitar una NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont
2
Buen partido @sarumont. He hecho la edición. Además, se cambió el nombre del método a "fromString" para jugar bien con JAX-RS .
Sam Berry
1
Me gustó este enfoque, pero opté por una variante menos detallada, ver más abajo.
linqu
2
aparentemente el keycampo es innecesario. En getKey, podría simplementereturn name().toLowerCase()
yair
1
Me gusta el campo clave en el caso en el que desee nombrar la enumeración de manera diferente a lo que tendrá el json. En mi caso, un sistema heredado envía un nombre realmente abreviado y difícil de recordar para el valor que envía, y puedo usar este campo para traducir a un nombre mejor para mi enumeración de Java.
Grinch
47

Desde Jackson 2.6, simplemente puede hacer esto:

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

Para obtener un ejemplo completo, consulte esta esencia .

Andrew Bickerton
fuente
26
Tenga en cuenta que hacer esto revertirá el problema. Ahora Jackson solo aceptará minúsculas y rechazará cualquier valor en mayúsculas o mayúsculas.
Pixel Elephant
30

Fui por la solución de Sam B. pero una variante más simple.

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
fuente
No creo que esto sea más sencillo. DataType.valueOf(key.toUpperCase())es una instanciación directa en la que tienes un bucle. Esto podría ser un problema para una enumeración muy numerosa. Por supuesto, valueOfpuede lanzar una IllegalArgumentException, que su código evita, por lo que es un buen beneficio si prefiere la verificación nula a la verificación de excepciones.
Patrick M
22

Si está usando Spring Boot 2.1.xcon Jackson 2.9, simplemente puede usar esta propiedad de la aplicación:

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

etSingh
fuente
3

Para aquellos que intentan deserializar Enum ignorando el caso en los parámetros GET , habilitar ACCEPT_CASE_INSENSITIVE_ENUMS no servirá de nada. No ayudará porque esta opción solo funciona para la deserialización del cuerpo . En su lugar, intente esto:

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

y entonces

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Las muestras de código y respuesta son de aquí

Konstantin Zyubin
fuente
1

Con disculpas para @Konstantin Zyubin, su respuesta estuvo cerca de lo que necesitaba, pero no lo entendí, así que así es como creo que debería ser:

Si desea deserializar un tipo de enumeración sin distinción entre mayúsculas y minúsculas, es decir, no desea, o no puede, modificar el comportamiento de toda la aplicación, puede crear un deserializador personalizado solo para un tipo, subclasificando StdConvertery forzando Jackson para usarlo solo en los campos relevantes que usan la JsonDeserializeanotación.

Ejemplo:

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

El problema se relaciona con com.fasterxml.jackson.databind.util.EnumResolver . usa HashMap para contener valores de enumeración y HashMap no admite claves que no distinguen entre mayúsculas y minúsculas.

en las respuestas anteriores, todos los caracteres deben estar en mayúsculas o minúsculas. pero arreglé todos los problemas (in) sensibles para enumeraciones con eso:

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

Usé una modificación de la solución de Iago Fernández y Paul.

Tenía una enumeración en mi objeto de solicitud que debía distinguir entre mayúsculas y 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 
 }
}
trooper31
fuente
0

Para permitir la deserialización de enumeraciones que no distingue entre mayúsculas y minúsculas en jackson, simplemente agregue la siguiente propiedad al application.propertiesarchivo de su proyecto de arranque de primavera.

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

Si tiene la versión yaml del archivo de propiedades, agregue la siguiente propiedad a su application.ymlarchivo.

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

Así es como a veces manejo las enumeraciones cuando quiero deserializar de manera que no distingue entre mayúsculas y minúsculas (basándose en el código publicado en la pregunta):

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

Si la enumeración no es trivial y, por lo tanto, en su propia clase, generalmente agrego un método de análisis estático para manejar cadenas en minúsculas.

Pablo
fuente
-1

Deserializar enum con jackson es simple. Cuando desee deserializar una enumeración basada en String, necesita un constructor, un getter y un setter para su enum.También la clase que usa esa enumeración debe tener un setter que reciba DataType como param, no 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;
     }

}

Cuando tenga su json, puede deserializar a la clase Endpoint usando 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
fuente