Jak mogę deserializować ciąg JSON, który zawiera wartości wyliczenia, w których wielkość liter nie jest rozróżniana? (używając Jackson Databind)
Ciąg JSON:
[{"url": "foo", "type": "json"}]
i mój Java POJO:
public static class Endpoint {
public enum DataType {
JSON, HTML
}
public String url;
public DataType type;
public Endpoint() {
}
}
w takim przypadku deserializacja JSON z "type":"json"
nie powiedzie się w miejscu, w którym "type":"JSON"
działałoby. Ale chcę "json"
pracować również ze względu na konwencję nazewnictwa.
Serializacja POJO skutkuje również pisaniem wielkich liter "type":"JSON"
Myślałem o użyciu @JsonCreator
i @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();
}
I zadziałało. Ale zastanawiałem się, czy istnieje lepsze rozwiązanie, ponieważ dla mnie wygląda to na włamanie.
Mogę również napisać niestandardowy deserializator, ale mam wiele różnych POJO, które używają wyliczeń i byłoby to trudne do utrzymania.
Czy ktoś może zaproponować lepszy sposób serializacji i deserializacji wyliczeń z odpowiednią konwencją nazewnictwa?
Nie chcę, aby moje wyliczenia w Javie były pisane małymi literami!
Oto kod testowy, którego użyłem:
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));
Odpowiedzi:
W wersji 2.4.0 możesz zarejestrować własny serializator dla wszystkich typów Enum ( link do sprawy na githubie). Możesz również samodzielnie zastąpić standardowy deserializator wyliczenia, który będzie świadomy typu wyliczenia. Oto przykład:
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); } }
Wynik:
["json","html"] [JSON, HTML]
źródło
Jackson 2.9
Jest to teraz bardzo proste przy użyciu wersji
jackson-databind
2.9.0 i nowszychObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); // objectMapper now deserializes enums in a case-insensitive manner
Pełny przykład z testami
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(); } } }
źródło
jackson-databind
2.9.2 działa zgodnie z oczekiwaniami.spring.jackson.mapper.accept-case-insensitive-enums=true
Natknąłem się na ten sam problem w moim projekcie, zdecydowaliśmy się zbudować nasze wyliczenia z kluczem ciągu i użyć
@JsonValue
oraz konstruktora statycznego odpowiednio do serializacji i deserializacji.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; } }
źródło
DataType.valueOf(key.toUpperCase())
- w przeciwnym razie tak naprawdę nic nie zmieniłeś. Kodowanie defensywne, aby uniknąć NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
key
pole jest niepotrzebne. WgetKey
, możesz po prostureturn name().toLowerCase()
Od wersji Jacksona 2.6 możesz po prostu zrobić to:
public enum DataType { @JsonProperty("json") JSON, @JsonProperty("html") HTML }
Pełny przykład można znaleźć w tym skrócie .
źródło
Poszedłem na rozwiązanie Sama B., ale prostszy wariant.
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; } }
źródło
DataType.valueOf(key.toUpperCase())
to bezpośrednia instancja, w której masz pętlę. Może to stanowić problem w przypadku bardzo wielu wyliczeń. OczywiścievalueOf
może zgłosić wyjątek IllegalArgumentException, którego kod unika, więc jest to dobra zaleta, jeśli wolisz sprawdzanie wartości null od sprawdzania wyjątków.Jeśli używasz Spring Boot
2.1.x
z Jacksonem2.9
, możesz po prostu użyć tej właściwości aplikacji:spring.jackson.mapper.accept-case-insensitive-enums=true
źródło
Dla tych, którzy próbują deserializować Enum, ignorując wielkość liter w parametrach GET , włączenie ACCEPT_CASE_INSENSITIVE_ENUMS nie przyniesie nic dobrego. To nie pomoże, ponieważ ta opcja działa tylko w przypadku deserializacji treści . Zamiast tego spróbuj tego:
public class StringToEnumConverter implements Converter<String, Modes> { @Override public Modes convert(String from) { return Modes.valueOf(from.toUpperCase()); } }
i wtedy
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); } }
Odpowiedzi i próbki kodu pochodzą stąd
źródło
Z przeprosinami dla @Konstantin Zyubin, jego odpowiedź była zbliżona do tego, czego potrzebowałem - ale tego nie rozumiałem, więc myślę, że powinno to wyglądać następująco:
Jeśli chcesz deserializować jeden typ wyliczenia jako niewrażliwy na wielkość liter - tj. Nie chcesz lub nie możesz modyfikować zachowania całej aplikacji, możesz utworzyć niestandardowy deserializator tylko dla jednego typu - przez podklasy
StdConverter
i wymuszenie Jackson, aby używać go tylko na odpowiednich polach przy użyciu rozszerzeniaJsonDeserialize
adnotacji.Przykład:
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; }
źródło
Aby umożliwić deserializację wyliczeń bez uwzględniania wielkości liter w języku jackson, po prostu dodaj poniższą właściwość do
application.properties
pliku projektu Spring Boot.spring.jackson.mapper.accept-case-insensitive-enums=true
Jeśli masz wersję YAML pliku properties, dodaj poniższą właściwość do swojego
application.yml
pliku.spring: jackson: mapper: accept-case-insensitive-enums: true
źródło
Problem został zgłoszony do com.fasterxml.jackson.databind.util.EnumResolver . używa HashMap do przechowywania wartości wyliczeniowych, a HashMap nie obsługuje kluczy bez rozróżniania wielkości liter.
w powyższych odpowiedziach wszystkie znaki powinny być pisane dużymi lub małymi literami. ale naprawiłem wszystkie (nie) wrażliwe problemy dla wyliczeń z tym:
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; } } }
Stosowanie:
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); } }
źródło
Użyłem modyfikacji rozwiązania Iago Fernándeza i Paula.
Miałem wyliczenie w moim obiekcie żądania, które musiało być niewrażliwe na wielkość liter
@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 } }
źródło
Oto, jak czasami radzę sobie z wyliczeniami, gdy chcę deserializować bez uwzględniania wielkości liter (na podstawie kodu zamieszczonego w pytaniu):
@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)); }
Jeśli wyliczenie jest nietrywialne, a zatem w jego własnej klasie zwykle dodaję statyczną metodę analizy do obsługi ciągów z małymi literami.
źródło
Deserializacja wyliczenia z Jacksonem jest prosta. Jeśli chcesz deserializować wyliczenie oparte na String, potrzebujesz konstruktora, metody pobierającej i ustawiającej do twojego wyliczenia.Również klasa używająca tego wyliczenia musi mieć metodę ustawiającą, która odbiera DataType jako parametr, a nie 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; } }
Gdy masz już swój json, możesz deserializować do klasy Endpoint za pomocą ObjectMapper of 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(); }
źródło