Как десериализовать строку 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();
}
И это сработало. Но мне было интересно, есть ли лучшее решение, потому что мне это кажется взломом.
Я также могу написать собственный десериализатор, но у меня есть много разных 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));
Ответы:
В версии 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]
источник
Джексон 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(); } } }
источник
jackson-databind
2.9.2 работает именно так, как ожидалось.spring.jackson.mapper.accept-case-insensitive-enums=true
Я столкнулся с этой же проблемой в своем проекте, мы решили построить наши перечисления со строковым ключом и использовать
@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()))
key
поле не нужно. ВgetKey
, можно простоreturn name().toLowerCase()
Начиная с версии Jackson 2.6, вы можете просто сделать это:
public enum DataType { @JsonProperty("json") JSON, @JsonProperty("html") HTML }
Для полного примера см. Эту суть .
источник
Я выбрал решение Сэма Б., но более простой вариант.
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, которого ваш код избегает, так что это хорошее преимущество, если вы предпочитаете нулевую проверку проверке исключений.Если вы используете Spring Boot
2.1.x
с Jackson,2.9
вы можете просто использовать это свойство приложения:spring.jackson.mapper.accept-case-insensitive-enums=true
источник
Для тех, кто пытается десериализовать Enum, игнорируя регистр в параметрах 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()); } }
Ответ и примеры кода отсюда
источник
Приношу свои извинения @Konstantin Zyubin, его ответ был близок к тому, что мне было нужно, но я этого не понимал, поэтому вот как, я думаю, он должен идти:
Если вы хотите десериализовать один тип перечисления как нечувствительный к регистру - т.е. вы не хотите или не можете изменять поведение всего приложения, вы можете создать собственный десериализатор только для одного типа - путем подкласса
StdConverter
и принудительного Джексон, чтобы использовать его только в соответствующих полях с помощью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; }
источник
Чтобы разрешить нечувствительную к регистру десериализацию перечислений в jackson, просто добавьте указанное ниже свойство в
application.properties
файл проекта весенней загрузки.spring.jackson.mapper.accept-case-insensitive-enums=true
Если у вас есть yaml-версия файла свойств, добавьте в
application.yml
файл свойство ниже .spring: jackson: mapper: accept-case-insensitive-enums: true
источник
Проблема отправлена на 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); } }
источник
Я использовал модификацию решения Яго Фернандеса и Пола.
У меня было перечисление в моем 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 } }
источник
Вот как я иногда обрабатываю перечисления, когда хочу выполнить десериализацию без учета регистра (на основе кода, опубликованного в вопросе):
@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)); }
Если перечисление нетривиально и, следовательно, в своем собственном классе я обычно добавляю статический метод синтаксического анализа для обработки строк в нижнем регистре.
источник
Десериализовать перечисление с помощью Джексона просто. Если вы хотите десериализовать перечисление на основе String, вам понадобится конструктор, геттер и сеттер для вашего перечисления. Также класс, который использует это перечисление, должен иметь сеттер, который получает 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, вы можете десериализовать его в класс Endpoint с помощью ObjectMapper Джексона:
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(); }
источник