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?

Sachin Kulkarni
fonte

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;
  }
}
Programador Bruce
fonte
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());
validcat
fonte
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
Inc
fonte
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;
  }
}
user2601995
fonte
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;
            }
        }
    }
}
Wout
fonte
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();
Tom Bollwitt
fonte
0

use este método

GsonBuilder.enableComplexMapKeySerialization();
Ahamadullah Saikat
fonte
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