Autre réponse. Cela a pris un peu plus de temps.
Commentaire en marge : La solution ci-dessus fonctionnerait si vous utilisiez récursivement la réflexion pour travailler sur les champs de votre classe. Ensuite, vous sérialisez ces champs avec le sérialiseur spécial, tout en utilisant un sérialiseur séparé pour l'objet parent. Cela éviterait le débordement de la pile.
Cela dit, je suis un développeur paresseux, et j'aime donc faire les choses paresseusement. Je suis en train d'adapter une solution de Google pour vous.
NOTE : VEUILLEZ TESTER CECI ET L'ADAPTER À VOS BESOINS. IL S'AGIT D'UN PROTOTYPE ET JE N'AI PAS NETTOYÉ LE CODE INUTILE NI VÉRIFIÉ LES PROBLÈMES ÉVENTUELS.
La source originale du code :
https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
Il s'agit donc d'un projet basé sur la RuntimeTypeAdapterFactory
. Cette fabrique est fournie par Google et son but est de supporter la désérialisation hiérarchique. Pour ce faire, vous devez enregistrer une classe de base et TOUTES les sous-classes, avec une propriété que vous souhaitez ajouter comme identifiant. Si vous lisez les javadocs, ce sera beaucoup plus clair.
Cela nous offre évidemment ce que nous voulons : enregistrer récursivement différents adaptateurs pour les types de classes qui peuvent les gérer, tout en ne tournant PAS en rond et en ne provoquant pas de débordement de pile. Avec un problème important : vous devez enregistrer TOUS sous-classes. Ceci n'est évidemment pas approprié (bien que l'on puisse argumenter que vous pourriez résoudre le chemin de classe et simplement ajouter toutes vos classes au démarrage pour être capable d'utiliser ceci partout). J'ai donc regardé dans la source et j'ai modifié le code pour le faire dynamiquement. Notez que google vous déconseille de le faire - utilisez-le selon vos propres conditions :)
Voici mon usine :
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {@code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {@code
* shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapter.registerSubtype(Circle.class, "Circle");
* shapeAdapter.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*/
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
// && !String.class.isAssignableFrom(type.getRawType())
if(Object.class.isAssignableFrom(type.getRawType()) ) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
labelToDelegate.put("class", delegate);
subtypeToDelegate.put(type.getRawType(), delegate);
}
// for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
// TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
// labelToDelegate.put(entry.getKey(), delegate);
// subtypeToDelegate.put(entry.getValue(), delegate);
// }
return new TypeAdapter<R>() {
@Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
@Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = srcType.getName();
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonElement jsonTree = delegate.toJsonTree(value);
if(jsonTree.isJsonPrimitive()) {
Streams.write(jsonTree, out);
} else {
JsonObject jsonObject = jsonTree.getAsJsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}
}.nullSafe();
}
}
J'ai ajouté toutes les importations pour vous. Ceci n'est pas (vraiment) publié dans maven central, bien que vous puissiez le trouver ici : https://mvnrepository.com/artifact/org.danilopianini/gson-extras/0.1.0
Quoi qu'il en soit, vous devrez faire des adaptations pour que cela fonctionne pour vous, alors j'ai fait une copie. La copie compile complètement et vous pouvez simplement la coller dans votre code et vous épargner la dépendance supplémentaire.
Les éléments importants de ce code sont les suivants : (et je les ai volontairement laissés dans le code mais commentés pour que vous puissiez vous en rendre compte)
en create(Gson gson, TypeToken<R> type)
Vérifier si le type brut est assignable à partir de la classe String. Vous voulez que cette vérification soit appliquée à chaque objet de la classe, c'est donc ce qui est fait. Notez que le code précédent vérifie si le type est enregistré dans la classe - ce n'est plus nécessaire (en conséquence, les variables ne seraient pas nécessaires ; vous devriez nettoyer le code).
en @Override public void write(JsonWriter out, R value) throws IOException {
:
Tout d'abord, nous nous débarrassons de l'étiquette. Notre étiquette est et sera toujours le nom du type de source. C'est ce qui est fait dans :
String label = srcType.getName();
Deuxièmement, nous devons faire une distinction entre les types primitifs et les types objets. Les types primitifs sont les chaînes de caractères, les nombres entiers, etc. dans le monde Gson. Cela signifie que notre vérification ci-dessus (ajouter un adaptateur) ne tient pas compte du fait que ces types objets sont en fait des types primitifs. C'est ce que nous faisons :
if(jsonTree.isJsonPrimitive()) {
Streams.write(jsonTree, out);
Ceci permet d'y remédier. S'il est primitif, il suffit d'écrire l'arbre dans le flux. Si ce n'est pas le cas, nous écrivons alors tous les autres champs ET le champ de la classe.
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
Fewww - enfin, cela permet de régler ce problème. Et voici l'exemple qui prouve que mon code fait ce que (je crois) vous voulez qu'il fasse ;)
public class GsonClassNameTest {
static Gson create = new GsonBuilder().registerTypeAdapterFactory(RuntimeClassNameTypeAdapterFactory.of(Object.class)).create();
public static void main(String[] args) {
String json = create.toJson(new X());
System.out.println(json);
}
public static class X {
public String test = "asd";
public int xyz = 23;
public Y y_class = new Y();
}
public static class Y {
String yTest = "asd2";
Z zTest = new Z();
}
public static class Z {
long longVal = 25;
double doubleTest = 2.4;
}
}
Cela produit maintenant ce json pour vous :
{
"class":"google.GsonClassNameTest$X",
"test":"asd",
"xyz":23,
"y_class":{
"class":"google.GsonClassNameTest$Y",
"yTest":"asd2",
"zTest":{
"class":"google.GsonClassNameTest$Z",
"longVal":25,
"doubleTest":2.4
}
}
}
Comme vous pouvez le voir, les chaînes, les longueurs et les entiers sont correctement créés. Chaque objet de classe a reçu son nom de classe de manière récurrente.
Il s'agit d'une approche générique qui devrait fonctionner avec tout ce que vous créez. Cependant, si vous décidez d'adopter cette approche, faites-moi une faveur et écrivez quelques tests unitaires ;) Comme je l'ai mentionné précédemment, j'ai prototypé cette implémentation.
J'espère que cela me permettra d'obtenir une note :)
Voir aussi,
Artur