杰克逊数据绑定枚举大小写不敏感

156

如何使用Jackson Databind反序列化包含大小写不敏感枚举值的JSON字符串?

JSON字符串:

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

以及我的 Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}
在这种情况下,使用"type":"json"反序列化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 https://jira.codehaus.org/browse/JACKSON-861 - Alexey Gavrilov
我正在使用Jackson 2.2.3。 - tom91136
好的,我刚刚更新到了2.4.0-RC3版本。 - tom91136
13个回答

204

Jackson 2.9

现在,使用jackson-databind 2.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();
    }
  }
}

5
这个非常棒! - Vikas Prasad
10
我使用的是2.9.2版本,但它无法正常工作。原因是:com.fasterxml.jackson.databind.exc.InvalidFormatException: 无法从字符串 "male" 反序列化为类型 ....Gender,因为该值不属于枚举实例名称之一:[FAMALE,MALE]。 - Jordan Silva
@JordanSilva 当然可以在v2.9.2中工作。我已添加了一个完整的代码示例和测试以进行验证。我不知道在你的情况下可能发生了什么,但是在特定情况下使用jackson-databind 2.9.2运行示例代码会按预期工作。 - davnicwil
20
使用Spring Boot,您可以简单地添加属性 spring.jackson.mapper.accept-case-insensitive-enums=true - Arne Burmeister
1
@JordanSilva 或许你正在尝试在获取参数中反序列化枚举,就像我一样 =)我已经解决了我的问题并在这里回答。希望能帮到你。 - Konstantin Zyubin
4
在Jackson 2.10.0中,他们弃用了 configure(MapperFeature f, boolean state) 方法,现在推荐使用以下格式: ObjectMapper mapper = JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true).build() - RobertR

108

我在我的项目中遇到了同样的问题,我们决定使用字符串键构建枚举,并分别使用 @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;
    }
}

2
这应该是 DataType.valueOf(key.toUpperCase()) - 否则,你并没有真正改变任何东西。为了避免 NPE 的编码防御性:return (null == key ? null : DataType.valueOf(key.toUpperCase())) - sarumont
2
很好的发现 @sarumont。我已经做了修改。并且,将方法重命名为“fromString”以便与JAX-RS友好地协作(http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-DealingwithParameters)。 - Sam Berry
1
我喜欢这种方法,但选择了更简洁的变体,请参见下文。 - linqu
2
显然,key字段是不必要的。在getKey中,你可以只返回name().toLowerCase() - yair
2
我喜欢在需要将枚举命名为与 JSON 不同的名称时使用键字段。在我的情况下,一个遗留系统发送了一个非常缩写和难以记忆的值名称,我可以使用这个字段将其翻译成更好的 Java 枚举名称。 - grinch
显示剩余5条评论

73

自从Jackson 2.6版本以后,你可以这样做:

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

完整样例请参见此代码片段


55
请注意,这样做将会颠倒问题。现在Jackson只接受小写,并拒绝任何大写或混合大小写的值。 - Pixel Elephant

42

在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
在我的项目中,我个人正在倡导这种方法。如果您查看我的示例,它需要大量的样板代码。使用单独的属性进行序列化/反序列化的一个好处是将Java重要值(枚举名称)的名称与客户端重要值(漂亮的打印)分离。例如,如果希望将HTML DataType更改为HTML_DATA_TYPE,则可以在不影响外部API的情况下执行此操作,如果有指定的键。 - Sam Berry
1
这是一个不错的开始,但如果您的枚举使用JsonProperty或JsonCreator,则会失败。Dropwizard有FuzzyEnumModule,这是一个更强大的实现。 - Pixel Elephant

41

如果您正在使用Spring Boot 2.1.x和Jackson 2.9,您只需使用以下应用程序属性:

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


请参阅有关从Spring Boot自定义Jackson的文档,以及Jackson文档中作为枚举类型的映射器自定义点列表,详见com.fasterxml.jackson.databind.MapperFeature API doc - Ahmad Hoghooghi
2
运行Spring Boot 2.4.5和Jackson 2.11 - 不起作用。 - Geyser14
运行 Spring Boot 2.5.5 成功了!!! - James Dube

37

我采用了 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异常,而你的代码避免了这种情况,所以如果你更喜欢空值检查而不是异常检查,那么这是一个好处。 - Patrick M

9

如果你尝试在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());
    }
}

答案和代码示例来自这里。该文讨论了如何在Spring MVC中自定义数据绑定。

5
为了让jackson对枚举类型进行不区分大小写的反序列化,只需在Spring Boot项目的application.properties文件中添加以下属性。
spring.jackson.mapper.accept-case-insensitive-enums=true

如果你有yaml格式的属性文件,将下列属性添加到你的application.yml文件中。
spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true

2

对于@Konstantin Zyubin的答案,非常抱歉,它接近我所需的内容,但我并不理解,因此以下是我的想法:

如果您想将一个枚举类型反序列化为不区分大小写 - 即您不想或无法修改整个应用程序的行为,则可以创建一个仅针对一个类型的自定义反序列化器 - 通过子类化StdConverter 并使用JsonDeserialize注释强制Jackson仅在相关字段上使用它。

例如:

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

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接