Jackson Databind枚举不区分大小写


97

如何反序列化包含不区分大小写的枚举值的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();
    }

而且有效。但是我想知道是否有更好的解决方案,因为这对我来说似乎是hack。

我也可以编写一个自定义的反序列化器,但是我有许多使用枚举的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));

您正在使用哪个版本的Jackson?看看这个JIRA jira.codehaus.org/browse/JACKSON-861
Alexey Gavrilov

我正在使用Jackson 2.2.3
tom91136

好吧,我刚刚更新为2.4.0-RC3
tom91136

Answers:


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 2014年

我个人在我的项目中主张这样做。如果看我的示例,它需要很多样板代码。使用单独的属性进行反序列化的一个好处是,它将Java重要值的名称(枚举名称)与客户端重要值的名称(漂亮打印)解耦。例如,如果需要将HTML数据类型更改为HTML_DATA_TYPE,则可以在不影响外部API的情况下进行操作(如果指定了密钥)。
山姆·贝里

1
这是一个很好的开始,但是如果您的枚举使用JsonProperty或JsonCreator,它将失败。Dropwizard具有FuzzyEnumModule,这是一个更强大的实现。
Pixel Elephant

131

杰克逊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
这是金!
Vikas Prasad

8
我正在使用2.9.2,它不起作用。原因:com.fasterxml.jackson.databind.exc.InvalidFormatException:无法从字符串“ male”中反序列化类型.... Gender`的值:值不是声明的Enum实例名称之一:[FAMALE,MALE]
Jordan Silva

@JordanSilva它确实适用于v2.9.2。我添加了带有验证测试的完整代码示例。我不知道您的情况可能会发生什么,但是使用jackson-databind2.9.2 运行示例代码的确可以按预期工作。
davnicwil

4
使用Spring Boot,您只需添加属性spring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister

1
@JordanSilva也许您正像我一样尝试对get参数中的枚举进行反序列化?=)我已经解决了我的问题,并在这里回答了。希望能对您有所帮助
Konstantin Zyubin '19

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很好地播放
山姆·贝里2015年

1
我喜欢这种方法,但是采用了较为详细的变体,请参见下文。
linqu '16

2
显然,该key字段是不必要的。在中getKey,您可以return name().toLowerCase()
yair

1
在您想给枚举命名不同于json的名称的情况下,我喜欢键字段。在我的案例中,遗留系统会为其发送的值发送一个真正缩写且难以记住的名称,我可以使用此字段将其转换为Java枚举的更好名称。
grinch

45

从Jackson 2.6开始,您可以简单地执行以下操作:

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

有关完整示例,请参见本要点


25
请注意,这样做可以解决问题。现在,杰克逊将只接受小写字母,并拒绝任何大写或大小写混合的值。
Pixel Elephant

30

我寻求Sam B.的解决方案,但是是一个简单的变体。

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,您的代码可以避免这种情况,因此,如果您更喜欢将null检查而不是异常检查,那么这将是一个很好的好处。
Patrick M

20

如果您将Spring Boot 2.1.x与Jackson一起2.9使用,则可以简单地使用以下应用程序属性:

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


3

对于那些试图忽略GET参数中的大小写反序列化Enum的用户,启用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和强制为一种类型创建自定义反序列化器Jackson只能在使用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

我使用了IagoFernández和Paul解决方案的修改。

我的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 
 }
}

0

为了允许杰克逊中的枚举不区分大小写反序列化,只需将以下属性添加到application.properties您的spring boot项目文件中。

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的枚举时,需要一个构造函数,一个getter和一个setter给您的枚举。使用该枚举的类还必须具有一个以DataType作为参数而不是String的setter来接收:

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后,您可以使用Jackson的ObjectMapper反序列化为Endpoint类:

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();
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.