156

How can I deserialize JSON string that contains enum values that are case insensitive? (using Jackson Databind)

The JSON string:

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

and my Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

in this case,deserializing the JSON with "type":"json" would fail where as "type":"JSON" would work. But I want "json" to work as well for naming convention reasons.

Serializing the POJO also results in upper case "type":"JSON"

I thought of using @JsonCreator and @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();
    }

And it worked. But I was wondering whether there's a better solutuon because this looks like a hack to me.

I can also write a custom deserializer but I got many different POJOs that use enums and it would be hard to maintain.

Can anyone suggest a better way to serialize and deserialize enums with proper naming convention?

I don't want my enums in java to be lowercase!

Here is some test code that I used:

    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));
Paul
  • 19,704
  • 14
  • 78
  • 96
tom91136
  • 8,662
  • 12
  • 58
  • 74

13 Answers13

204

Jackson 2.9

This is now very simple, using jackson-databind 2.9.0 and above

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

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

Full example with tests

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
  • 28,487
  • 16
  • 107
  • 123
  • 5
    This one is gold! – Vikas Prasad Sep 21 '17 at 07:14
  • 10
    I'm using 2.9.2 and it doesn't work. Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type ....Gender` from String "male": value not one of declared Enum instance names: [FAMALE, MALE] – Jordan Silva Oct 22 '17 at 17:19
  • @JordanSilva it certainly does work with v2.9.2. I have added a full code example with tests for verification. I don't know what might have happened in your case, but running the example code with `jackson-databind` 2.9.2 specifically works as expected. – davnicwil Feb 06 '18 at 08:23
  • 19
    using Spring Boot, you can simply add the property `spring.jackson.mapper.accept-case-insensitive-enums=true` – Arne Burmeister Jun 12 '19 at 08:06
  • 1
    @JordanSilva maybe you are trying to deserialize enum in get parameters as I did?=) I've solved my problem and answered here. Hope it can help – Konstantin Ziubin Sep 04 '19 at 17:22
  • 4
    In Jackson 2.10.0 they deprecated `configure(MapperFeature f, boolean state` and the preferred pattern looks like this: `ObjectMapper mapper = JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true).build()` – RobertR May 03 '22 at 14:15
107

I ran into this same issue in my project, we decided to build our enums with a string key and use @JsonValue and a static constructor for serialization and deserialization respectively.

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
  • 7,394
  • 6
  • 40
  • 58
  • 2
    This should be `DataType.valueOf(key.toUpperCase())` - otherwise, you have not really changed anything. Coding defensively to avoid an NPE: `return (null == key ? null : DataType.valueOf(key.toUpperCase()))` – sarumont Mar 05 '15 at 20:13
  • 2
    Good catch @sarumont. I have made the edit. Also, renamed method to "fromString" to [play nicely with JAX-RS](http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-DealingwithParameters). – Sam Berry Mar 06 '15 at 01:29
  • 1
    I liked this approach, but went for a less verbose variant, see below. – linqu Apr 26 '16 at 10:56
  • @linqu that is a valid approach. Some like the idea of decoupling the JSON representation from the syntax of your code so that changing a property name has less refactor scope. I think it either way is good. – Sam Berry Apr 26 '16 at 11:14
  • you are right, it depends on what is more important: simplicity or control – linqu Apr 26 '16 at 14:25
  • 2
    apparently the `key` field is unnecessary. In `getKey`, you could just `return name().toLowerCase()` – yair Jun 20 '16 at 10:39
  • Can someone explain why are `fromString` method `public` and `static`, plus similarly, why is `getKey` method `public`? – Simple-Solution Oct 09 '16 at 13:57
  • 2
    I like the key field in the case where you want to name the enum something different than what the json will have. In my case a legacy system sends a really abbreviated and hard to remember name for the value it sends, and I can use this field to translate to a better name for my java enum. – grinch Jun 08 '17 at 03:48
  • @sarumont I just ran into this and I'm wondering is returning a null in fromString appropriate. I know it is "defensive" but it could also be considered passing the NPE to anyone using this method. Isn't returning null bad in some cases? Is it bad in this case? – John Mercier May 07 '20 at 14:04
  • 1
    @JohnMercier I suppose that's up to you. If a `null` `String` is acceptable for your use-case, then go for it. If not, you can always return `""`. In our case, `null` was a valid value, as we also had decoration with `@NotNull` and `@Nullable` for a bit more clarity there. – sarumont Jan 12 '21 at 22:40
72

Since Jackson 2.6, you can simply do this:

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

For a full example, see this gist.

  • 52
    Note that doing this will reverse the problem. Now Jackson will only accept lowercase, and reject any uppercase or mixed-case values. – Pixel Elephant Jul 26 '16 at 14:57
42

In version 2.4.0 you can register a custom serializer for all the Enum types (link to the github issue). Also you can replace the standard Enum deserializer on your own that will be aware about the Enum type. Here is an example:

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

Output:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48
  • 1
    Thanks, now I can remove all the boilerplate in my POJO :) – tom91136 Jun 13 '14 at 08:10
  • I personally am advocating for this in my projects. If you look at my example, it requires a lot of boilerplate code. One benefit of using a separate attributes for de/serialization is that it decouples the names of the Java-important values (enum names) to client-important values (pretty print). E.g., if it was desired to change the HTML DataType to HTML_DATA_TYPE, you could do so without affecting the external API, if there is a key specified. – Sam Berry Mar 09 '15 at 07:24
  • 1
    This is a good start but it will fail if your enum is using JsonProperty or JsonCreator. Dropwizard has [FuzzyEnumModule](https://github.com/dropwizard/dropwizard/blob/master/dropwizard-jackson/src/main/java/io/dropwizard/jackson/FuzzyEnumModule.java) which is a more robust implementation. – Pixel Elephant Feb 22 '17 at 20:10
41

If you're using Spring Boot 2.1.x with Jackson 2.9 you can simply use this application property:

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

harpresing
  • 603
  • 6
  • 10
  • See documentation on [Customize Jackson from Spring boot](https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-customize-the-jackson-objectmapper). and list of customization point for mapper as Enum in Jackson documentation on [com.fasterxml.jackson.databind.MapperFeature API doc](https://fasterxml.github.io/jackson-databind/javadoc/2.9/) – Ahmad Hoghooghi Feb 20 '21 at 10:34
  • 2
    Running Spring Boot 2.4.5 and Jackson 2.11 - doesn't work – Geyser14 Sep 27 '21 at 06:49
  • Running Spring Boot 2.5.5 works !!! – James Dube Feb 23 '22 at 14:12
37

I went for the solution of Sam B. but a simpler variant.

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
  • 11,320
  • 8
  • 55
  • 67
  • I don't think this is simpler. `DataType.valueOf(key.toUpperCase())` is a direct instantiation where you have a loop. This could be an issue for a very numerous enum. Of course, `valueOf` can throw an IllegalArgumentException, which your code avoids, so that's a good benefit if you prefer null checking to exception checking. – Patrick M Dec 13 '19 at 15:40
9

For those who tries to deserialize Enum ignoring case in GET parameters, enabling ACCEPT_CASE_INSENSITIVE_ENUMS will not do any good. It won't help because this option only works for body deserialization. Instead try this:

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

and then

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

The answer and code samples are from here

Konstantin Ziubin
  • 660
  • 10
  • 17
5

To allow case insensitive deserialization of enums in jackson, simply add the below property to the application.properties file of your spring boot project.

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

If you have the yaml version of properties file, add below property to your application.yml file.

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Vivek
  • 11,938
  • 19
  • 92
  • 127
2

With apologies to @Konstantin Zyubin, his answer was close to what I needed - but I didn't understand it, so here's how I think it should go:

If you want to deserialize one enum type as case insensitive - i.e. you don't want to, or can't, modify the behavior of the entire application, you can create a custom deserializer just for one type - by sub-classing StdConverter and force Jackson to use it only on the relevant fields using the JsonDeserialize annotation.

Example:

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
  • 30,470
  • 17
  • 104
  • 128
0

Problem is releated to com.fasterxml.jackson.databind.util.EnumResolver. it uses HashMap to hold enum values and HashMap doesn't support case insensitive keys.

in answers above, all chars should be uppercase or lowercase. but I fixed all (in)sensitive problems for enums with that:

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

Usage:

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
  • 3,415
  • 26
  • 20
0

I used a modification of Iago Fernández and Paul solution .

I had an enum in my requestobject which needed to be case insensitive

@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 
 }
}
Community
  • 1
  • 1
trooper31
  • 182
  • 1
  • 2
  • 9
-1

Here's how I sometimes handle enums when I want to deserialize in a case-insensitive manner (building on the code posted in the question):

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

If the enum is non-trivial and thus in its own class I usually add a static parse method to handle lowercase Strings.

Paul
  • 19,704
  • 14
  • 78
  • 96
-1

Deserialize enum with jackson is simple. When you want deserialize enum based in String need a constructor, a getter and a setter to your enum.Also class that use that enum must have a setter which receive DataType as param, not 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;
     }

}

When you have your json, you can deserialize to Endpoint class using 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();
}