Usando Enums mientras se analiza JSON con GSON

119

Esto está relacionado con una pregunta anterior que hice aquí anteriormente.

Análisis JSON usando Gson

Estoy tratando de analizar el mismo JSON, pero ahora he cambiado un poco mis clases.

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

Mi clase ahora se ve así:

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 lanza una excepción,

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 

La excepción es comprensible, porque según la solución a mi pregunta anterior, GSON espera que los objetos Enum se creen realmente como

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

Pero dado que esto es sintácticamente imposible, ¿cuáles son las soluciones recomendadas?

Sachin Kulkarni
fuente

Respuestas:

57

De la documentación de Gson :

Gson proporciona serialización y deserialización predeterminadas para Enums ... Si prefiere cambiar la representación predeterminada, puede hacerlo registrando un adaptador de tipo a través de GsonBuilder.registerTypeAdapter (Type, Object).

A continuación se muestra uno de esos enfoques.

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
fuente
310

Quiero expandir un poco la respuesta NAZIK / user2724653 (para mi caso). Aquí hay un 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
     }
}

en el archivo json tienes solo un campo "status": "N", , donde N = 0,1,2,3 - depende de los valores de Estado. Eso es todo, GSONfunciona bien con los valores de la enumclase anidada . En mi caso he analizado sintácticamente una lista de Itemsde jsonmatriz:

List<Item> items = new Gson().<List<Item>>fromJson(json,
                                          new TypeToken<List<Item>>(){}.getType());
validcat
fuente
28
Esta respuesta lo resuelve todo a la perfección, ¡sin necesidad de adaptadores de tipo!
Lena Bru
4
Cuando hago esto, con Retrofit / Gson, el SerializedName de los valores de enumeración tiene comillas adicionales agregadas. El servidor realmente recibe "1", por ejemplo, en lugar de simplemente 1...
Matthew Housser
17
¿Qué pasará si llega json con estado 5? ¿Hay alguna forma de definir el valor predeterminado?
DmitryBorodin
8
@DmitryBorodin Si el valor de JSON no coincide con ninguno SerializedName, la enumeración será predeterminada null. El comportamiento predeterminado de estado desconocido podría manejarse en una clase contenedora. Sin embargo, si necesita una representación para "desconocido" que no sea nullentonces, deberá escribir un deserializador personalizado o un adaptador de tipo.
Peter F
32

Utilice la anotación @SerializedName:

@SerializedName("${title}")
TITLE,
@SerializedName("${description}")
DESCRIPTION
C ª
fuente
9

Con GSON versión 2.2.2, la enumeración se calculará y eliminará fácilmente.

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
fuente
8

El siguiente fragmento elimina la necesidad de explícito Gson.registerTypeAdapter(...), usando la @JsonAdapter(class)anotación, disponible desde Gson 2.3 (ver comentario 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
fuente
1
Tenga en cuenta que esta anotación solo está disponible a partir de la versión 2.3: google.github.io/gson/apidocs/index.html?com/google/gson/…
pm_labs
3
tenga cuidado de agregar sus clases de serializador / deserializador a su configuración de proguard, ya que podrían eliminarse (me sucedió a mí)
TormundThunderfist
2

Si realmente desea utilizar el valor ordinal de Enum, puede registrar una fábrica de adaptadores de tipo para anular la fábrica predeterminada de 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);
        }
    };
}

Entonces simplemente registre la fábrica.

Gson gson = new GsonBuilder()
               .registerTypeAdapterFactory(EnumTypeAdapter.ENUM_FACTORY)
               .create();
Tom Bollwitt
fuente
0

usa este método

GsonBuilder.enableComplexMapKeySerialization();
Ahamadullah Saikat
fuente
3
Si bien este código puede responder a la pregunta, proporcionar un contexto adicional sobre cómo y / o por qué resuelve el problema mejoraría el valor de la respuesta a largo plazo.
Nic3500
a partir de gson 2.8.5, esto es necesario para usar las anotaciones de SerializedName en las enumeraciones que desea usar como claves
vazor