Usando Enums ao analisar JSON com GSON


119

Isso está relacionado a uma pergunta anterior que fiz aqui anteriormente

Análise JSON usando Gson

Estou tentando analisar o mesmo JSON, mas agora mudei um pouco minhas classes.

{
    "lower": 20,
    "upper": 40,
    "delimiter": " ",
    "scope": ["${title}"]
}

Minha classe agora se parece com:

public class TruncateElement {

   private int lower;
   private int upper;
   private String delimiter;
   private List<AttributeScope> scope;

   // getters and setters
}


public enum AttributeScope {

    TITLE("${title}"),
    DESCRIPTION("${description}"),

    private String scope;

    AttributeScope(String scope) {
        this.scope = scope;
    }

    public String getScope() {
        return this.scope;
    }
}

Este código lança uma exceção,

com.google.gson.JsonParseException: The JsonDeserializer EnumTypeAdapter failed to deserialized json object "${title}" given the type class com.amazon.seo.attribute.template.parse.data.AttributeScope
at 

A exceção é compreensível, porque de acordo com a solução da minha pergunta anterior, GSON espera que os objetos Enum sejam realmente criados como

${title}("${title}"),
${description}("${description}");

Mas, uma vez que isso é sintaticamente impossível, quais são as soluções recomendadas e soluções alternativas?

Respostas:


57

Da documentação do Gson :

Gson fornece serialização e desserialização padrão para Enums ... Se você preferir alterar a representação padrão, você pode fazer isso registrando um adaptador de tipo através de GsonBuilder.registerTypeAdapter (Type, Object).

A seguir está uma dessas abordagens.

import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;

public class GsonFoo
{
  public static void main(String[] args) throws Exception
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(AttributeScope.class, new AttributeScopeDeserializer());
    Gson gson = gsonBuilder.create();

    TruncateElement element = gson.fromJson(new FileReader("input.json"), TruncateElement.class);

    System.out.println(element.lower);
    System.out.println(element.upper);
    System.out.println(element.delimiter);
    System.out.println(element.scope.get(0));
  }
}

class AttributeScopeDeserializer implements JsonDeserializer<AttributeScope>
{
  @Override
  public AttributeScope deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    AttributeScope[] scopes = AttributeScope.values();
    for (AttributeScope scope : scopes)
    {
      if (scope.scope.equals(json.getAsString()))
        return scope;
    }
    return null;
  }
}

class TruncateElement
{
  int lower;
  int upper;
  String delimiter;
  List<AttributeScope> scope;
}

enum AttributeScope
{
  TITLE("${title}"), DESCRIPTION("${description}");

  String scope;

  AttributeScope(String scope)
  {
    this.scope = scope;
  }
}

310

Eu quero expandir um pouco a resposta NAZIK / user2724653 (para o meu caso). Aqui está um código Java:

public class Item {
    @SerializedName("status")
    private Status currentState = null;

    // other fields, getters, setters, constructor and other code...

    public enum Status {
        @SerializedName("0")
        BUY,
        @SerializedName("1")
        DOWNLOAD,
        @SerializedName("2")
        DOWNLOADING,
        @SerializedName("3")
        OPEN
     }
}

no arquivo json você tem apenas um campo "status": "N",, onde N = 0,1,2,3 - depende dos valores de Status. Então isso é tudo, GSONfunciona bem com os valores da enumclasse aninhada . No meu caso eu tenho analisado uma lista de Itemsde jsonmatriz:

List<Item> items = new Gson().<List<Item>>fromJson(json,
                                          new TypeToken<List<Item>>(){}.getType());

28
Esta resposta resolve tudo perfeitamente, sem necessidade de adaptadores de tipo!
Lena Bru

4
Quando faço isso, com Retrofit / Gson, o SerializedName dos valores de enum tem aspas extras adicionadas. O servidor realmente recebe "1", por exemplo, em vez de simplesmente 1...
Matthew Housser

17
O que acontecerá se json com status 5 chegar? Existe alguma maneira de definir o valor padrão?
DmitryBorodin de

8
@DmitryBorodin Se o valor de JSON não corresponder a nenhum SerializedName, o enum será padronizado como null. O comportamento padrão de estado desconhecido pode ser tratado em uma classe de wrapper. No entanto, se você precisar de uma representação para "desconhecido" null, será necessário escrever um desserializador personalizado ou adaptador de tipo.
Peter F

32

Use anotação @SerializedName:

@SerializedName("${title}")
TITLE,
@SerializedName("${description}")
DESCRIPTION

9

Com GSON versão 2.2.2 enum será empacotado e descompactado facilmente.

import com.google.gson.annotations.SerializedName;

enum AttributeScope
{
  @SerializedName("${title}")
  TITLE("${title}"),

  @SerializedName("${description}")
  DESCRIPTION("${description}");

  private String scope;

  AttributeScope(String scope)
  {
    this.scope = scope;
  }

  public String getScope() {
    return scope;
  }
}

8

O trecho a seguir remove a necessidade de explícito Gson.registerTypeAdapter(...), usando a @JsonAdapter(class)anotação, disponível desde Gson 2.3 (consulte o comentário pm_labs ).

@JsonAdapter(Level.Serializer.class)
public enum Level {
    WTF(0),
    ERROR(1),
    WARNING(2),
    INFO(3),
    DEBUG(4),
    VERBOSE(5);

    int levelCode;

    Level(int levelCode) {
        this.levelCode = levelCode;
    }

    static Level getLevelByCode(int levelCode) {
        for (Level level : values())
            if (level.levelCode == levelCode) return level;
        return INFO;
    }

    static class Serializer implements JsonSerializer<Level>, JsonDeserializer<Level> {
        @Override
        public JsonElement serialize(Level src, Type typeOfSrc, JsonSerializationContext context) {
            return context.serialize(src.levelCode);
        }

        @Override
        public Level deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
            try {
                return getLevelByCode(json.getAsNumber().intValue());
            } catch (JsonParseException e) {
                return INFO;
            }
        }
    }
}

1
Observe que esta anotação está disponível apenas a partir da versão 2.3: google.github.io/gson/apidocs/index.html?com/google/gson/…
pm_labs

3
tenha cuidado ao adicionar suas classes de serializador / desserializador à configuração do programa, pois elas podem ser removidas (aconteceu comigo)
TormundThunderfist

2

Se você realmente deseja usar o valor ordinal do Enum, pode registrar um tipo de adaptador de fábrica para substituir o padrão de fábrica do Gson.

public class EnumTypeAdapter <T extends Enum<T>> extends TypeAdapter<T> {
    private final Map<Integer, T> nameToConstant = new HashMap<>();
    private final Map<T, Integer> constantToName = new HashMap<>();

    public EnumTypeAdapter(Class<T> classOfT) {
        for (T constant : classOfT.getEnumConstants()) {
            Integer name = constant.ordinal();
            nameToConstant.put(name, constant);
            constantToName.put(constant, name);
        }
    }
    @Override public T read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        return nameToConstant.get(in.nextInt());
    }

    @Override public void write(JsonWriter out, T value) throws IOException {
        out.value(value == null ? null : constantToName.get(value));
    }

    public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() {
        @SuppressWarnings({"rawtypes", "unchecked"})
        @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
            Class<? super T> rawType = typeToken.getRawType();
            if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) {
                return null;
            }
            if (!rawType.isEnum()) {
                rawType = rawType.getSuperclass(); // handle anonymous subclasses
            }
            return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
        }
    };
}

Depois é só registrar a fábrica.

Gson gson = new GsonBuilder()
               .registerTypeAdapterFactory(EnumTypeAdapter.ENUM_FACTORY)
               .create();

0

use este método

GsonBuilder.enableComplexMapKeySerialization();

3
Embora este código possa responder à pergunta, fornecer contexto adicional sobre como e / ou por que ele resolve o problema melhoraria o valor da resposta a longo prazo.
Nic3500 de

a partir do gson 2.8.5, isso é necessário para usar anotações SerializedName em enums que você deseja usar como chaves
vazor 01 de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.