【问题标题】:Jackson databind enum case insensitive杰克逊数据绑定枚举不区分大小写
【发布时间】:2014-08-01 05:01:15
【问题描述】:

如何反序列化包含不区分大小写的枚举值的 JSON 字符串? (使用 Jackson 数据绑定)

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

它奏效了。但我想知道是否有更好的解决方案,因为这对我来说就像一个黑客。

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

【问题讨论】:

标签: java json serialization enums jackson


【解决方案1】:

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

【讨论】:

  • 这应该是DataType.valueOf(key.toUpperCase()) - 否则,你并没有真正改变任何东西。防御性编码以避免 NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
  • 好收获@sarumont。我已经进行了编辑。此外,将方法重命名为“fromString”为play nicely with JAX-RS
  • 我喜欢这种方法,但选择了一个不那么冗长的变体,见下文。
  • 显然key 字段是不必要的。在getKey,你可以只用return name().toLowerCase()
  • 我喜欢 key 字段,因为您想将枚举命名为不同于 json 的名称。在我的例子中,一个遗留系统为它发送的值发送一个非常简短且难以记住的名称,我可以使用这个字段为我的 java 枚举转换为一个更好的名称。
【解决方案2】:

在 2.4.0 版本中,您可以为所有 Enum 类型注册一个自定义序列化程序(link 到 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]

【讨论】:

  • 谢谢,现在我可以删除 POJO 中的所有样板了 :)
  • 我个人在我的项目中提倡这一点。如果你看我的例子,它需要大量的样板代码。使用单独的属性进行反序列化的一个好处是,它将 Java 重要值(枚举名称)的名称与客户端重要值(漂亮的打印)分离。例如,如果希望将 HTML DataType 更改为 HTML_DATA_TYPE,如果指定了密钥,您可以在不影响外部 API 的情况下这样做。
  • 这是一个好的开始,但如果您的枚举使用 JsonProperty 或 JsonCreator,它将失败。 Dropwizard 有FuzzyEnumModule,这是一个更强大的实现。
【解决方案3】:

问题与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);
    }

}

【讨论】:

    【解决方案4】:

    当我想以不区分大小写的方式反序列化时,我有时会这样处理枚举(基于问题中发布的代码):

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

    如果枚举是非平凡的,因此在它自己的类中,我通常添加一个静态解析方法来处理小写字符串。

    【讨论】:

      【解决方案5】:

      使用 jackson 反序列化枚举很简单。当你想反序列化基于 String 的枚举时,你的枚举需要一个构造函数、一个 getter 和一个 setter。此外,使用该枚举的类必须有一个 setter,它接收 DataType 作为参数,而不是 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;
           }
      
      }
      

      当你有你的 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();
      }
      

      【讨论】:

        【解决方案6】:

        我选择了 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,您的代码会避免这种情况,因此如果您更喜欢空检查而不是异常检查,这是一个很好的好处。
        【解决方案7】:

        从 Jackson 2.6 开始,您可以简单地这样做:

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

        有关完整示例,请参阅this gist

        【讨论】:

        • 请注意,这样做会扭转问题。现在 Jackson 将只接受小写字母,并拒绝任何大写字母或混合大小写的值。
        【解决方案8】:

        杰克逊 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();
            }
          }
        }
        

        【讨论】:

        • 这个是金子!
        • 我使用的是 2.9.2,但它不起作用。原因:com.fasterxml.jackson.databind.exc.InvalidFormatException:无法从字符串“male”反序列化 ....Gender` 类型的值:值不是声明的枚举实例名称之一:[FAMALE,MALE]
        • @JordanSilva 它确实适用于 v2.9.2。我添加了一个带有验证测试的完整代码示例。我不知道您的情况可能会发生什么,但是使用 jackson-databind 2.9.2 运行示例代码确实可以按预期工作。
        • 使用Spring Boot,只需添加属性spring.jackson.mapper.accept-case-insensitive-enums=true
        • @JordanSilva 也许您正尝试像我一样在获取参数中反序列化枚举?=) 我已经解决了我的问题并在这里回答了。希望对你有帮助
        【解决方案9】:

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

        我的请求对象中有一个枚举,需要不区分大小写

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

        【讨论】:

          【解决方案10】:

          如果你使用 Spring Boot 2.1.x 和 Jackson 2.9,你可以简单地使用这个应用程序属性:

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

          【讨论】:

          【解决方案11】:

          对于那些试图在 GET 参数 中忽略大小写反序列化 Enum 的人,启用 ACCEPT_CASE_INSENSITIVE_ENUMS 不会有任何好处。这无济于事,因为此选项仅适用于 body 反序列化。试试这个:

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

          答案和代码示例来自here

          【讨论】:

            【解决方案12】:

            向@Konstantin Zyubin 道歉,他的回答接近我所需要的——但我不明白,所以我认为应该这样:

            如果您想反序列化一种枚举类型以区分大小写 - 即您不想或不能修改整个应用程序的行为,您可以为一种类型创建自定义反序列化器 - 通过 sub-对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;
            }
            

            【讨论】:

              【解决方案13】:

              要允许在 jackson 中对枚举进行不区分大小写的反序列化,只需将以下属性添加到您的 Spring Boot 项目的 application.properties 文件中即可。

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

              如果您有 yaml 版本的属性文件,请将以下属性添加到您的 application.yml 文件中。

              spring:
                jackson:
                  mapper:
                    accept-case-insensitive-enums: true
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2018-08-16
                • 1970-01-01
                • 1970-01-01
                • 2012-01-06
                • 1970-01-01
                • 2023-01-18
                • 2015-10-27
                • 2015-05-23
                相关资源
                最近更新 更多