Jackson Databind Enum Groß- und Kleinschreibung nicht berücksichtigen

97

Wie kann ich eine JSON-Zeichenfolge deserialisieren, die Aufzählungswerte enthält, bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird? (mit Jackson Databind)

Die JSON-Zeichenfolge:

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

und mein Java POJO:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

In diesem Fall würde das Deserialisieren des JSON mit "type":"json"fehlschlagen, wo "type":"JSON"dies funktionieren würde. Aber ich möchte "json"auch aus Gründen der Namenskonvention arbeiten.

Die Serialisierung des POJO führt auch zu Großbuchstaben "type":"JSON"

Ich dachte an @JsonCreatorund @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();
    }

Und es hat funktioniert. Aber ich habe mich gefragt, ob es eine bessere Lösung gibt, weil das für mich wie ein Hack aussieht.

Ich kann auch einen benutzerdefinierten Deserializer schreiben, aber ich habe viele verschiedene POJOs, die Aufzählungen verwenden, und es wäre schwierig, sie zu warten.

Kann jemand einen besseren Weg vorschlagen, um Enums mit der richtigen Namenskonvention zu serialisieren und zu deserialisieren?

Ich möchte nicht, dass meine Aufzählungen in Java in Kleinbuchstaben geschrieben werden!

Hier ist ein Testcode, den ich verwendet habe:

    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));
tom91136
quelle
Auf welcher Version von Jackson bist du? Werfen
Alexey Gavrilov
Ich benutze Jackson 2.2.3
tom91136
OK Ich habe gerade auf 2.4.0-RC3 aktualisiert
tom91136

Antworten:

38

In Version 2.4.0 können Sie einen benutzerdefinierten Serializer für alle Enum-Typen registrieren ( Link zum Github-Problem). Sie können auch den Standard-Enum-Deserializer selbst ersetzen, der den Enum-Typ kennt. Hier ist ein Beispiel:

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

Ausgabe:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
quelle
1
Danke, jetzt kann ich alle Boilerplate in meinem POJO entfernen :)
tom91136
Ich persönlich setze mich in meinen Projekten dafür ein. Wenn Sie sich mein Beispiel ansehen, erfordert es viel Boilerplate-Code. Ein Vorteil der Verwendung separater Attribute für die De- / Serialisierung besteht darin, dass die Namen der Java-wichtigen Werte (Aufzählungsnamen) von den Client-wichtigen Werten (hübscher Druck) entkoppelt werden. Wenn beispielsweise der HTML-Datentyp in HTML_DATA_TYPE geändert werden soll, können Sie dies tun, ohne die externe API zu beeinflussen, wenn ein Schlüssel angegeben ist.
Sam Berry
1
Dies ist ein guter Anfang, aber er schlägt fehl, wenn Ihre Aufzählung JsonProperty oder JsonCreator verwendet. Dropwizard verfügt über FuzzyEnumModule , eine robustere Implementierung.
Pixel Elephant
130

Jackson 2.9

Dies ist jetzt mit jackson-databind2.9.0 und höher sehr einfach

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);

// objectMapper now deserializes enums in a case-insensitive manner

Vollständiges Beispiel mit Tests

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();
    }
  }
}
davnicwil
quelle
3
Dieser ist Gold!
Vikas Prasad
8
Ich benutze 2.9.2 und es funktioniert nicht. Auslöser: com.fasterxml.jackson.databind.exc.InvalidFormatException: Wert des Typs kann nicht deserialisiert werden .... Geschlecht` aus Zeichenfolge "männlich": Wert nicht einer der deklarierten Enum-Instanznamen: [FAMALE, MALE]
Jordan Silva
@ JordanSilva es funktioniert sicherlich mit v2.9.2. Ich habe ein vollständiges Codebeispiel mit Tests zur Überprüfung hinzugefügt. Ich weiß nicht, was in Ihrem Fall passiert sein könnte, aber das Ausführen des Beispielcodes mit jackson-databind2.9.2 funktioniert speziell wie erwartet.
Davnicwil
4
Mit Spring Boot können Sie einfach die Eigenschaft hinzufügenspring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva Vielleicht versuchen Sie, enum in get-Parametern so zu deserialisieren, wie ich es getan habe? =) Ich habe mein Problem gelöst und hier geantwortet. Hoffe es kann helfen
Konstantin Zyubin
82

Ich bin in meinem Projekt auf dasselbe Problem gestoßen. Wir haben beschlossen, unsere Aufzählungen mit einem String-Schlüssel und einer Verwendung @JsonValuesowie einem statischen Konstruktor für die Serialisierung bzw. Deserialisierung zu erstellen .

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;
    }
}
Sam Berry
quelle
1
Dies sollte sein DataType.valueOf(key.toUpperCase())- ansonsten haben Sie nichts wirklich geändert. Defensives return (null == key ? null : DataType.valueOf(key.toUpperCase()))
Codieren
2
Guter Fang @sarumont. Ich habe die Bearbeitung vorgenommen. Außerdem wurde die Methode in "fromString" umbenannt, um mit JAX-RS gut zu spielen .
Sam Berry
1
Ich mochte diesen Ansatz, entschied mich aber für eine weniger ausführliche Variante, siehe unten.
Linqu
2
anscheinend ist das keyFeld unnötig. In getKey, Sie könnten geradereturn name().toLowerCase()
yair
1
Ich mag das Schlüsselfeld für den Fall, dass Sie die Aufzählung anders benennen möchten als das, was der JSON haben wird. In meinem Fall sendet ein Legacy-System einen wirklich abgekürzten und schwer zu merkenden Namen für den Wert, den es sendet, und ich kann dieses Feld verwenden, um einen besseren Namen für meine Java-Enumeration zu übersetzen.
grinch
45

Seit Jackson 2.6 können Sie dies einfach tun:

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

Ein vollständiges Beispiel finden Sie in diesem Kern .

Andrew Bickerton
quelle
25
Beachten Sie, dass dies das Problem umkehrt. Jetzt akzeptiert Jackson nur Kleinbuchstaben und lehnt Groß- oder Großbuchstaben ab.
Pixel Elephant
30

Ich habe mich für die Lösung von Sam B. entschieden, aber für eine einfachere Variante.

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;
    }
}
linqu
quelle
Ich denke nicht, dass das einfacher ist. DataType.valueOf(key.toUpperCase())ist eine direkte Instanziierung, bei der Sie eine Schleife haben. Dies könnte ein Problem für eine sehr große Anzahl sein. Sie können natürlich valueOfeine IllegalArgumentException auslösen, die Ihr Code vermeidet. Dies ist also ein guter Vorteil, wenn Sie die Nullprüfung der Ausnahmeprüfung vorziehen.
Patrick M
20

Wenn Sie Spring Boot 2.1.xmit Jackson verwenden 2.9, können Sie einfach diese Anwendungseigenschaft verwenden:

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

etSingh
quelle
3

Für diejenigen, die versuchen, Enum zu deserialisieren, wobei Groß- und Kleinschreibung in GET-Parametern ignoriert wird , hilft das Aktivieren von ACCEPT_CASE_INSENSITIVE_ENUMS nichts. Es wird nicht helfen, da diese Option nur für die Deserialisierung des Körpers funktioniert . Versuchen Sie stattdessen Folgendes:

public class StringToEnumConverter implements Converter<String, Modes> {
    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from.toUpperCase());
    }
}

und dann

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

Die Antwort- und Codebeispiele finden Sie hier

Konstantin Zyubin
quelle
1

Mit Entschuldigungen an @Konstantin Zyubin war seine Antwort nah an dem, was ich brauchte - aber ich habe es nicht verstanden, also denke ich, dass es so gehen sollte:

Wenn Sie einen Aufzählungstyp als Groß- und Kleinschreibung deserialisieren mögen - das heißt , Sie wollen nicht, oder nicht, ändern Sie das Verhalten der gesamten Anwendung, können Sie einen benutzerdefinierten Deserializer erstellen nur für eine Art - von Unter classing StdConverterund Kraft Jackson verwendet es nur auf den relevanten Feldern mit demJsonDeserialize Anmerkung.

Beispiel:

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;
}
Guss
quelle
0

Das Problem wird an com.fasterxml.jackson.databind.util.EnumResolver weitergeleitet . Es verwendet HashMap, um Aufzählungswerte zu speichern, und HashMap unterstützt keine Schlüssel, bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird.

In den obigen Antworten sollten alle Zeichen in Groß- oder Kleinbuchstaben geschrieben sein. aber ich habe alle (un) sensiblen Probleme für Enums damit behoben:

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

Verwendung:

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

}
bhdrk
quelle
0

Ich habe eine Modifikation der Iago Fernández- und Paul-Lösung verwendet.

Ich hatte eine Aufzählung in meinem Anfrageobjekt, bei der die Groß- und Kleinschreibung nicht berücksichtigt werden musste

@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 
 }
}
trooper31
quelle
0

Fügen Sie einfach die folgende Eigenschaft zur application.propertiesDatei Ihres Spring-Boot-Projekts hinzu, um die Deserialisierung von Aufzählungen in Jackson ohne Berücksichtigung der Groß- und Kleinschreibung zu ermöglichen .

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

Wenn Sie die yaml-Version der Eigenschaftendatei haben, fügen Sie Ihrer application.ymlDatei die folgende Eigenschaft hinzu .

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Vivek
quelle
-1

So gehe ich manchmal mit Aufzählungen um, wenn ich ohne Berücksichtigung der Groß- und Kleinschreibung deserialisieren möchte (basierend auf dem in der Frage angegebenen Code):

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

Wenn die Aufzählung nicht trivial ist und sich daher in einer eigenen Klasse befindet, füge ich normalerweise eine statische Analysemethode hinzu, um Zeichenfolgen in Kleinbuchstaben zu verarbeiten.

Paul
quelle
-1

Enumeration mit Jackson zu deserialisieren ist einfach. Wenn Sie eine auf String basierende Enumeration deserialisieren möchten, benötigen Sie einen Konstruktor, einen Getter und einen Setter für Ihre Enumeration. Auch Klassen, die diese Enumeration verwenden, müssen einen Setter haben, der DataType als Parameter empfängt, nicht 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;
     }

}

Wenn Sie Ihren JSON haben, können Sie mit ObjectMapper of Jackson in die Endpoint-Klasse deserialisieren:

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();
}
Iago Fernández
quelle