Jackson databind enum case insensitive

106

Bagaimana cara menghentikan derialisasi string JSON yang berisi nilai enum yang tidak peka huruf besar / kecil? (menggunakan Jackson Databind)

String JSON:

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

dan POJO Java saya:

public static class Endpoint {

    public enum DataType {
        JSON, HTML
    }

    public String url;
    public DataType type;

    public Endpoint() {

    }

}

dalam kasus ini, deserialisasi JSON dengan "type":"json"akan gagal di tempat yang "type":"JSON"akan berhasil. Tapi saya ingin "json"bekerja juga karena alasan konvensi penamaan.

Serialisasi POJO juga menghasilkan huruf besar "type":"JSON"

Saya berpikir untuk menggunakan @JsonCreatordan @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();
    }

Dan itu berhasil. Tapi saya bertanya-tanya apakah ada solusi yang lebih baik karena ini terlihat seperti hack bagi saya.

Saya juga dapat menulis deserializer khusus tetapi saya mendapatkan banyak POJO berbeda yang menggunakan enum dan akan sulit untuk dipelihara.

Adakah yang bisa menyarankan cara yang lebih baik untuk membuat serial dan deserialisasi enum dengan konvensi penamaan yang tepat?

Saya tidak ingin enum saya di java menjadi huruf kecil!

Berikut beberapa kode tes yang saya gunakan:

    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
sumber
Anda menggunakan versi Jackson yang mana? Lihatlah JIRA
Alexey Gavrilov
Saya menggunakan Jackson 2.2.3
tom91136
OK saya baru saja memperbarui ke 2.4.0-RC3
tom91136

Jawaban:

38

Di versi 2.4.0 Anda dapat mendaftarkan serializer khusus untuk semua jenis Enum ( tautan ke masalah github). Anda juga dapat mengganti deserializer Enum standar Anda sendiri yang akan mengetahui tentang jenis Enum. Berikut ini contohnya:

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

Keluaran:

["json","html"]
[JSON, HTML]
Alexey Gavrilov
sumber
1
Terima kasih, sekarang saya dapat menghapus semua boilerplate di POJO saya :)
tom91136
Saya pribadi menganjurkan hal ini dalam proyek saya. Jika Anda melihat contoh saya, ini membutuhkan banyak kode boilerplate. Salah satu keuntungan menggunakan atribut terpisah untuk de / serialization adalah ia memisahkan nama dari nilai penting Java (nama enum) menjadi nilai penting klien (cukup cetak). Misalnya, jika diinginkan untuk mengubah Jenis Data HTML menjadi HTML_DATA_TYPE, Anda dapat melakukannya tanpa memengaruhi API eksternal, jika ada kunci yang ditentukan.
Sam Berry
1
Ini adalah awal yang baik tetapi akan gagal jika enum Anda menggunakan JsonProperty atau JsonCreator. Dropwizard memiliki FuzzyEnumModule yang merupakan implementasi yang lebih kuat.
Pixel Elephant
143

Jackson 2.9

Ini sekarang sangat sederhana, menggunakan jackson-databind2.9.0 ke atas

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

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

Contoh lengkap dengan tes

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.dll
sumber
3
Yang ini emas!
Vikas Prasad
8
Saya menggunakan 2.9.2 dan tidak berhasil. Disebabkan oleh: com.fasterxml.jackson.databind.exc.InvalidFormatException: Tidak dapat menghilangkan nama jenis .... Gender` dari String "male": nilai bukan salah satu dari nama instance Enum yang dideklarasikan: [FAMALE, MALE]
Jordan Silva
@JordanSilva pasti berfungsi dengan v2.9.2. Saya telah menambahkan contoh kode lengkap dengan tes untuk verifikasi. Saya tidak tahu apa yang mungkin terjadi dalam kasus Anda, tetapi menjalankan kode contoh dengan jackson-databind2.9.2 secara khusus berfungsi seperti yang diharapkan.
davnicwil
8
menggunakan Spring Boot, Anda cukup menambahkan propertispring.jackson.mapper.accept-case-insensitive-enums=true
Arne Burmeister
1
@JordanSilva mungkin Anda mencoba deserialize enum dalam mendapatkan parameter seperti yang saya lakukan? =) Saya telah memecahkan masalah saya dan menjawabnya di sini. Semoga dapat membantu
Konstantin Zyubin
86

Saya mengalami masalah yang sama ini dalam proyek saya, kami memutuskan untuk membangun enum kami dengan kunci string dan menggunakan @JsonValuedan konstruktor statis untuk serialisasi dan deserialisasi masing-masing.

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
sumber
1
Ini seharusnya DataType.valueOf(key.toUpperCase())- jika tidak, Anda tidak benar-benar mengubah apa pun. Pengkodean pertahanan untuk menghindari NPE:return (null == key ? null : DataType.valueOf(key.toUpperCase()))
sarumont
2
Tangkapan bagus @sarumont. Saya sudah mengeditnya. Juga, mengganti nama metode menjadi "fromString" untuk bermain bagus dengan JAX-RS .
Sam Berry
1
Saya menyukai pendekatan ini, tetapi memilih varian yang tidak terlalu bertele-tele, lihat di bawah.
linqu
2
ternyata keylapangan itu tidak perlu. Masuk getKey, Anda bisa sajareturn name().toLowerCase()
yair
1
Saya suka bidang kunci dalam kasus di mana Anda ingin memberi nama enum sesuatu yang berbeda dari apa yang akan dimiliki json. Dalam kasus saya, sistem warisan mengirimkan nama yang sangat disingkat dan sulit diingat untuk nilai yang dikirimnya, dan saya dapat menggunakan bidang ini untuk menerjemahkan ke nama yang lebih baik untuk enum java saya.
grinch
50

Sejak Jackson 2.6, Anda cukup melakukan ini:

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

Untuk contoh lengkap, lihat inti ini .

Andrew Bickerton
sumber
27
Perhatikan bahwa melakukan ini akan membalikkan masalah. Sekarang Jackson hanya akan menerima huruf kecil, dan menolak nilai huruf besar atau campuran apa pun.
Pixel Elephant
30

Saya memilih solusi Sam B. tetapi varian yang lebih sederhana.

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
sumber
Saya tidak berpikir ini lebih sederhana. DataType.valueOf(key.toUpperCase())adalah instantiation langsung di mana Anda memiliki loop. Ini bisa menjadi masalah untuk banyak enum. Tentu saja, valueOfdapat memunculkan IllegalArgumentException, yang dihindari kode Anda, jadi itu adalah keuntungan yang baik jika Anda lebih memilih pemeriksaan null daripada pemeriksaan pengecualian.
Patrick M
24

Jika Anda menggunakan Spring Boot 2.1.xwith Jackson, 2.9Anda cukup menggunakan properti aplikasi ini:

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

etSingh
sumber
4

Bagi mereka yang mencoba untuk menghilangkan kasus Enum mengabaikan kasus di parameter GET , mengaktifkan ACCEPT_CASE_INSENSITIVE_ENUMS tidak akan ada gunanya. Ini tidak akan membantu karena opsi ini hanya berfungsi untuk deserialisasi tubuh . Sebagai gantinya coba ini:

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

lalu

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Jawaban dan contoh kode berasal dari sini

Konstantin Zyubin
sumber
1

Dengan permintaan maaf kepada @Konstantin Zyubin, jawabannya mendekati apa yang saya butuhkan - tetapi saya tidak memahaminya, jadi begini menurut saya jawabannya:

Jika Anda ingin deserialize satu jenis enum sebagai case insensitive - yaitu Anda tidak ingin, atau tidak bisa, mengubah perilaku seluruh aplikasi, Anda dapat membuat deserializer kustom hanya untuk satu jenis - dengan sub-classing StdConverterdan memaksa Jackson menggunakannya hanya pada bidang yang relevan dengan menggunakan JsonDeserializepenjelasan.

Contoh:

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
sumber
1

Untuk mengizinkan deserialisasi enum yang tidak peka huruf besar / kecil di jackson, cukup tambahkan properti di bawah ini ke application.propertiesfile proyek boot musim semi Anda.

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

Jika Anda memiliki versi yaml dari file properti, tambahkan properti di bawah ini ke application.ymlfile Anda .

spring:
  jackson:
    mapper:
      accept-case-insensitive-enums: true
Vivek
sumber
Terima kasih, dapat mengonfirmasi ini berfungsi di Spring Boot.
Joonas Vali
0

Masalah dilepaskan ke com.fasterxml.jackson.databind.util.EnumResolver . ia menggunakan HashMap untuk menyimpan nilai enum dan HashMap tidak mendukung kunci case insensitive.

dalam jawaban di atas, semua karakter harus dalam huruf besar atau kecil. tapi saya memperbaiki semua (dalam) masalah sensitif untuk enum dengan itu:

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

Pemakaian:

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
sumber
0

Saya menggunakan modifikasi solusi Iago Fernández dan Paul.

Saya memiliki enum di objek permintaan saya yang harus case insensitive

@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 
 }
}
polisi 31
sumber
-1

Inilah cara saya terkadang menangani enum ketika saya ingin melakukan deserialisasi dengan cara yang tidak peka huruf besar / kecil (membangun kode yang diposting dalam pertanyaan):

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

Jika enumnya tidak sepele dan dengan demikian di kelasnya sendiri, saya biasanya menambahkan metode parse statis untuk menangani String huruf kecil.

Paul
sumber
-1

Deserialisasi enum dengan jackson itu sederhana. Ketika Anda ingin deserialisasi enum yang berbasis di String membutuhkan konstruktor, pengambil dan penyetel ke enum Anda. Juga kelas yang menggunakan enum itu harus memiliki penyetel yang menerima DataType sebagai param, bukan 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;
     }

}

Saat Anda memiliki json, Anda dapat melakukan deserialisasi ke kelas Endpoint menggunakan 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();
}
Iago Fernández
sumber