Serializando e desserializando uma lista com o Gson
1. Introdução
Neste tutorial, vamos explorar alguns casos avançados deserializationedeserialization paraList usandoGoogle’s Gson library.
2. Lista de Objetos
Um caso de uso comum é serializar e desserializar uma lista de POJOs.
Considere a classe:
public class MyClass {
private int id;
private String name;
public MyClass(int id, String name) {
this.id = id;
this.name = name;
}
// getters and setters
}
Aqui está como serializaríamosList<MyClass>:
@Test
public void givenListOfMyClass_whenSerializing_thenCorrect() {
List list = Arrays.asList(new MyClass(1, "name1"), new MyClass(2, "name2"));
Gson gson = new Gson();
String jsonString = gson.toJson(list);
String expectedString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";
assertEquals(expectedString, jsonString);
}
Como podemos ver, a serialização é bastante direta.
No entanto, a desserialização é complicada. Esta é uma maneira incorreta de fazer isso:
@Test(expected = ClassCastException.class)
public void givenJsonString_whenIncorrectDeserializing_thenThrowClassCastException() {
String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";
Gson gson = new Gson();
List outputList = gson.fromJson(inputString, ArrayList.class);
assertEquals(1, outputList.get(0).getId());
}
Aqui,although we would get a List of size two, post-deserialization, it wouldn’t be a List of MyClass. Portanto, a linha # 6 lançaClassCastException.
Gson can serialize a collection of arbitrary objects but can’t deserialize the data without additional information. That’s because there’s no way for the user to indicate the type of the resulting object. Em vez disso, durante a desserialização, oCollection deve ser de um tipo específico e genérico.
A maneira correta de desserializar oList seria:
@Test
public void givenJsonString_whenDeserializing_thenReturnListOfMyClass() {
String inputString = "[{\"id\":1,\"name\":\"name1\"},{\"id\":2,\"name\":\"name2\"}]";
List inputList = Arrays.asList(new MyClass(1, "name1"), new MyClass(2, "name2"));
Type listOfMyClassObject = new TypeToken>() {}.getType();
Gson gson = new Gson();
List outputList = gson.fromJson(inputString, listOfMyClassObject);
assertEquals(inputList, outputList);
}
Aqui,we use Gson’s TypeToken to determine the correct type to be deserialized – ArrayList<MyClass>. O idioma usado para obterlistOfMyClassObject realmente define uma classe interna local anônima contendo um métodogetType() que retorna o tipo totalmente parametrizado.
3. Lista de objetos polimórficos
3.1. O problema
Considere um exemplo de hierarquia de classes de animais:
public abstract class Animal {
// ...
}
public class Dog extends Animal {
// ...
}
public class Cow extends Animal {
// ...
}
Como serializamos e desserializamosList<Animal>? Poderíamos usarTypeToken<ArrayList<Animal>> como usamos na seção anterior. No entanto, Gson ainda não será capaz de descobrir o tipo de dados concreto dos objetos armazenados na lista.
3.2. Usando o desserializador personalizado
Uma maneira de resolver isso é adicionar informações de tipo ao JSON serializado. Honramos essas informações de tipo durante a desserialização de JSON. Para isso, precisamos escrever nosso próprio serializador e desserializador personalizado.
Em primeiro lugar, vamos introduzir um novo campoString chamadotype na classe baseAnimal. Ele armazena o nome simples da classe à qual pertence.
Vamos dar uma olhada em nossos exemplos de classes:
public abstract class Animal {
public String type = "Animal";
}
public class Dog extends Animal {
private String petName;
public Dog() {
petName = "Milo";
type = "Dog";
}
// getters and setters
}
public class Cow extends Animal {
private String breed;
public Cow() {
breed = "Jersey";
type = "Cow";
}
// getters and setters
}
A serialização continuará funcionando como antes, sem problemas:
@Test
public void givenPolymorphicList_whenSerializeWithTypeAdapter_thenCorrect() {
String expectedString
= "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";
List inList = new ArrayList<>();
inList.add(new Dog());
inList.add(new Cow());
String jsonString = new Gson().toJson(inList);
assertEquals(expectedString, jsonString);
}
Para desserializar a lista, teremos que fornecer um desserializador personalizado:
public class AnimalDeserializer implements JsonDeserializer {
private String animalTypeElementName;
private Gson gson;
private Map> animalTypeRegistry;
public AnimalDeserializer(String animalTypeElementName) {
this.animalTypeElementName = animalTypeElementName;
this.gson = new Gson();
this.animalTypeRegistry = new HashMap<>();
}
public void registerBarnType(String animalTypeName, Class extends Animal> animalType) {
animalTypeRegistry.put(animalTypeName, animalType);
}
public Animal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
JsonObject animalObject = json.getAsJsonObject();
JsonElement animalTypeElement = animalObject.get(animalTypeElementName);
Class extends Animal> animalType = animalTypeRegistry.get(animalTypeElement.getAsString());
return gson.fromJson(animalObject, animalType);
}
}
Aqui, o mapaanimalTypeRegistry mantém o mapeamento entre o nome da classe e o tipo de classe.
Durante a desserialização, primeiro extraímos o campotype recém-adicionado. Usando esse valor, fazemos uma pesquisa no mapaanimalTypeRegistry para obter o tipo de dados concreto. Este tipo de dados é então passado parafromJson().
Vamos ver como usar nosso desserializador personalizado:
@Test
public void givenPolymorphicList_whenDeserializeWithTypeAdapter_thenCorrect() {
String inputString
= "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";
AnimalDeserializer deserializer = new AnimalDeserializer("type");
deserializer.registerBarnType("Dog", Dog.class);
deserializer.registerBarnType("Cow", Cow.class);
Gson gson = new GsonBuilder()
.registerTypeAdapter(Animal.class, deserializer)
.create();
List outList = gson.fromJson(inputString, new TypeToken>(){}.getType());
assertEquals(2, outList.size());
assertTrue(outList.get(0) instanceof Dog);
assertTrue(outList.get(1) instanceof Cow);
}
3.3. UsandoRuntimeTypeAdapterFactory
Uma alternativa para escrever um desserializador personalizado é usar a classeRuntimeTypeAdapterFactory presente emGson source code. No entanto,it’s not exposed by the library for the user to use. Portanto, teremos que criar uma cópia da classe em nosso projeto Java.
Feito isso, podemos usá-lo para desserializar nossa lista:
@Test
public void givenPolymorphicList_whenDeserializeWithRuntimeTypeAdapter_thenCorrect() {
String inputString
= "[{\"petName\":\"Milo\",\"type\":\"Dog\"},{\"breed\":\"Jersey\",\"type\":\"Cow\"}]";
Type listOfAnimals = new TypeToken>(){}.getType();
RuntimeTypeAdapterFactory adapter = RuntimeTypeAdapterFactory.of(Animal.class, "type")
.registerSubtype(Dog.class)
.registerSubtype(Cow.class);
Gson gson = new GsonBuilder().registerTypeAdapterFactory(adapter).create();
List outList = gson.fromJson(inputString, listOfAnimals);
assertEquals(2, outList.size());
assertTrue(outList.get(0) instanceof Dog);
assertTrue(outList.get(1) instanceof Cow);
}
Observe que o mecanismo subjacente ainda é o mesmo.
Ainda precisamos introduzir as informações de tipo durante a serialização. As informações de tipo podem ser usadas posteriormente durante a desserialização. Hence, the field type is still required in every class for this solution to work. Nós simplesmente não temos que escrever nosso próprio desserializador.
RuntimeTypeAdapterFactory fornece o tipo de adaptador correto com base no nome do campo transmitido a ele e nos subtipos registrados.
4. Conclusão
Neste artigo, vimos como serializar e desserializar uma lista de objetos usando o Gson.
Como de costume, o código está disponívelover on GitHub.