Неверный регистр
Как я могу десериализовать строку JSON, содержащую значения enum, которые нечувствительны к регистру? (используя 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();
}
И это сработало. Но мне было интересно, есть ли лучший solutuon, потому что это выглядит как хак для меня.
Я также могу написать собственный десериализатор, но у меня появилось много разных 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));
Ответы
Ответ 1
В версии 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
Джексон 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();
}
}
}
Ответ 3
Я столкнулся с этой же проблемой в своем проекте, мы решили создать наши перечисления с помощью строкового ключа и использовать @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;
}
}
Ответ 4
Начиная с Jackson 2.6, вы можете просто сделать это:
public enum DataType {
@JsonProperty("json")
JSON,
@JsonProperty("html")
HTML
}
В качестве полного примера см. этот метод.
Ответ 5
Я пошел на решение Сэма Б., но более простой вариант.
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;
}
}
Ответ 6
Если вы используете Spring Boot 2.1.x
с Джексоном 2.9
, вы можете просто использовать это свойство приложения:
spring.jackson.mapper.accept-case-insensitive-enums=true
Ответ 7
Проблема выдается в 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);
}
}
Ответ 8
Я использовал модификацию решения Яго Фернандеса и Пола.
У меня был enum в моем объекте запроса, который должен был быть нечувствительным к регистру
@POST
public Response doSomePostAction(RequestObject object){
//resource implementation
}
class RequestObject{
//other params
MyEnumType myType;
@JsonSetter
public void setMyType(String type){
MyEnumType.valueOf(type.toUpperCase());
}
@JsonGetter
public String getType(){
return myType.toString();//this can change
}
}
Ответ 9
Для тех, кто пытается десериализовать 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());
}
}
Ответ и примеры кода можно найти здесь here
Ответ 10
Вот как я иногда обрабатываю перечисления, когда хочу десериализовать без учета регистра (построив код, размещенный в вопросе):
@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));
}
Если перечисление нетривиально и, следовательно, в его собственном классе, я обычно добавляю статический метод синтаксического анализа для обработки строчных строк.
Ответ 11
Дезаминировать перечисление с помощью джексона просто. Если вы хотите, чтобы десериализовать enum, основанный на String, нужен конструктор, getter и setter для вашего enum. Также класс, который использует это перечисление, должен иметь установщик, который получает DataType как param, а не 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 из 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();
}