1
votes

Comment résoudre une référence circulaire lors de la sérialisation d'un objet qui a un membre de classe avec le même type de cet objet

Je suis confronté à ce problème lorsque j'utilise Gson pour sérialiser un objet qui a un membre de classe du même type:

https://github.com/google/gson/issues/1447

L'objet:

private Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {

        public boolean shouldSkipClass(Class<?> clazz) {
            return false; //(clazz == StructId.class);
        }

        /**
          * Custom field exclusion goes here
          */
        public boolean shouldSkipField(FieldAttributes f) {
            //Ignore inner StructIds to solve circular serialization
            return ( f.getName().equals("ParentId") || f.getName().equals("ChildId") ); 
        }

     })
    /**
      * Use serializeNulls method if you want To serialize null values 
      * By default, Gson does not serialize null values
      */
    .serializeNulls()
    .create();

Et comme StructId contient ParentId / ChildId avec le même type, j'obtenais une boucle infinie en essayant de le sérialiser, alors ce que j'ai fait est:

public class StructId implements Serializable {
private static final long serialVersionUID = 1L;

public String Name;
public StructType Type;
public StructId ParentId;
public StructId ChildId;

Mais ce n'est pas assez bon car j'ai besoin des données dans Parent / Child et les ignorer lors de la sérialisation n'est pas une solution. Comment est-il possible de le résoudre?

Lié à la réponse marquée comme Solution:

J'ai une telle structure: - Struct1 -- Table --- Variable1

L'objet avant la sérialisation est: entrez la description de l'image ici

Et Json qui est généré est: entrez la description de l'image ici

Comme vous pouvez le voir, le ParentId de Table est "Struct1" mais le ChildId de "Struct1" est vide et il devrait être "Table" p>

BR


1 commentaires

Veuillez modifier votre question pour qu'elle contienne votre véritable question.


3 Réponses :


1
votes

Je pense qu'utiliser ExclusionStrategy n'est pas la bonne approche pour résoudre ce problème.

Je suggérerais plutôt d'utiliser JsonSerializer et JsonDeserializer personnalisé pour votre classe StructId .
(Peut être une approche utilisant TypeAdapter serait encore mieux, mais je n'avais pas assez d'expérience Gson pour que cela fonctionne.)

Vous créeriez donc votre instance Gson par:

{
    "Name": "John",
    "Type": "A",
    "ChildId": {
        "Name": "Jane",
        "Type": "B",
        "ChildId": {
            "Name": "Joe",
            "Type": "A"
        }
    }
}

La classe StructIdSerializer ci-dessous est responsable de la conversion d'un StructId en JSON. Il convertit ses propriétés Name , Type et ChildId en JSON. Notez qu'il ne convertit pas la propriété ParentId en JSON, car cela produirait une récursion infinie.

public class StructIdDeserializer implements JsonDeserializer<StructId> {

    @Override
    public StructId deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
        throws JsonParseException {
        StructId id = new StructId();
        id.Name = json.getAsJsonObject().get("Name").getAsString();
        id.Type = context.deserialize(json.getAsJsonObject().get("Type"), StructType.class);
        JsonElement childJson = json.getAsJsonObject().get("ChildId");
        if (childJson != null) {
            id.ChildId = context.deserialize(childJson, StructId.class);  // recursion!
            id.ChildId.ParentId = id;
        }
        return id;
    }
}

La classe StructIdDeserializer ci-dessous est responsable de la conversion de JSON en un StructId . Il convertit les propriétés JSON Name , Type et ChildId aux champs Java correspondants dans StructId . Notez que le champ Java ParentId est reconstruit à partir de la structure d'imbrication JSON, car il n'est pas directement contenu en tant que propriété JSON.

public class StructIdSerializer implements JsonSerializer<StructId> {

    @Override
    public JsonElement serialize(StructId src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("Name", src.Name);
        jsonObject.add("Type", context.serialize(src.Type));
        jsonObject.add("ChildId", context.serialize(src.ChildId));  // recursion!
        return jsonObject;
    }
}

J'ai testé le code ci-dessus avec cet exemple d'entrée JSON

Gson gson = new GsonBuilder()
    .registerTypeAdapter(StructId.class, new StructIdSerializer())
    .registerTypeAdapter(StructId.class, new StructIdDeserializer())
    .setPrettyPrinting()
    .create();

en le désérialisant avec
StructId root = gson.fromJson (nouveau FileReader ("example.json"), StructId.class); ,
puis en sérialisant cela avec
System.out.println (gson.toJson (racine));
et a obtenu à nouveau le JSON d'origine.


6 commentaires

Ça a l'air bien. J'ai implémenté la solution Serializer / Deserializer et j'ai quelques questions: 1) pourquoi avez-vous utilisé addProperty pour Name et pas ajouté comme valeur? 2) le json arrivant au contrôleur de serveur est comme ceci: {"++ \" Name \ "": "+ \" struct \ ""} et lors de l'appel de id.Name = json.getAsJsonObject (). Get ("Name ") .getAsString (); obtenir null


@zbeedatm 1) J'ai utilisé addProperty ("Name", ...) car cela suffit pour ajouter une primitive JSON. 2) Ce JSON a l'air bizarre. Il serait préférable d'ajouter un exemple JSON formaté à votre question au lieu de le presser dans un commentaire. J'ai également ajouté mon exemple d'entrée JSON à la réponse.


Ces caractères apparaissent à cause de: URLEncoder.encode (jsonDesttinationIdString, ENCODING). Mais cela fonctionnait comme prévu avant d'utiliser le sérialiseur personnalisé ... vous trouverez un moyen de le gérer de l'autre côté par Encoder.decode peut-être


Ok semble fonctionner fondamentalement. Mais sans ParentId et ces données sont également nécessaires ... alors que suggérez-vous? essayer l'autre manière de TypeAdapter? c'est peut-être réalisable là-bas


@zbeedatm Pour moi, le ParentId a été correctement défini dans les objets Java, et en JSON, il n'est pas nécessaire. Je vous suggère de vous en tenir à l'approche sérialiseur / désérialiseur et de le faire fonctionner. Avec l'approche TypeAdapter, vous obtiendrez les mêmes fonctionnalités, uniquement avec de meilleures performances en cas d'énormes contenus JSON.


Merci beaucoup @ thomas-fritsch, j'ai utilisé l'approche Adapter à la fin.



0
votes

Juste pour montrer une façon de faire la sérialisation (donc je ne gère pas la désérialisation) avec TypeAdapter et ExclusionStrategy . Ce n'est peut-être pas la plus belle implémentation, mais elle est quand même assez générique.

Cette solution utilise le fait que votre structure est une sorte de liste chaînée bidirectionnelle et compte tenu de tout nœud de cette liste, nous devons simplement séparer la sérialisation des parents et des enfants afin que ceux-ci soient sérialisés dans une seule direction pour éviter les références circulaires.

Nous avons d'abord besoin de ExclusionStrategy configurable comme:

{
  "Name": "name1237598030",
  "Type": {
       "name": "name688766789"
   },
  "ParentId": {
  "Name": "name1169146729",
  "Type": {
     "name": "name2040352617"
  }
 },
"ChildId": {
  "Name": "name302155142",
  "Type": {
     "name": "name24606376"
   }
 }
}


8 commentaires

Salut encore, j'ai réalisé que 2 niveaux ne suffisent pas ... Je dois remplir tout l'arborescence de la structure jusqu'au dernier nœud


L'avez-vous essayé? Je pense que cela devrait fonctionner. La seule chose est que le nœud racine est toujours le nœud que vous donnez et il aura alors un arbre parent et enfant


J'ai édité mon message avec plus d'informations relatives à votre solution, veuillez jeter un coup d'œil.


Est-il possible de le limiter à 2-3 niveaux seulement et d'éviter la boucle infinie? Je pense que ce sera suffisant si chaque objet peut contenir ses objets Parent & Child remplis


Si j'ai bien compris vos modifications, vous souhaitez répliquer à nouveau StructId = 59 dans son ParentId, StructId = 68 (mais sans son ParentId, structId = 68 à nouveau, pour empêcher la rtecursion). Mais pourquoi? Vous avez déjà les données comme élément racine. Peut-être devriez-vous repenser votre problème réel et peut-être reformuler votre question ou poser une autre question?


Oui, j'ai besoin de ces données (59 en tant qu'enfant de 68). J'ai un test Junit qui passe avec un flux normal et lors de l'exécution du même test via l'API restante et traitant des objets de sérialisation, il échoue. J'ai à nouveau édité mon message et donné la logique où il échoue si cela aide à comprendre. Il échoue lorsqu'il entre dans le troisième if et appelle à nouveau la méthode elle-même -recursion- avec structureIterator qui n'a pas d'enfants.


J'ai édité votre LinkedListAdapter pour ajouter plus de niveau imbriqué, et j'ai progressé. Je le marquerai comme solution car je n'ai pas d'autre moyen de le gérer en attendant, j'espère ne pas rencontrer de nouveaux problèmes au fur et à mesure que je poursuis mes tests.


@zbeedatm Comme vous le souhaitez mais je pense toujours qu'il aurait pu y avoir une solution encore meilleure si le vrai problème était mieux connu. Je vois votre cas de test, mais je ne sais pas pourquoi vous devez le faire de cette façon. Je vais peut-être enquêter plus tard.



0
votes

Désolé de vous avoir induit en erreur, sur la base de ce message:

Existe-t-il une solution à propos de la "référence circulaire" de Gson?

"Il n'y a pas de solution automatisée pour les références circulaires dans Gson. La seule bibliothèque de production JSON que je connaisse qui gère automatiquement les références circulaires est XStream (avec le backend Jettison). "

Mais c'est le cas où vous ne le faites pas n'utilise pas Jackson! Si vous utilisez déjà Jackson pour créer vos contrôleurs d'API REST, pourquoi ne pas l'utiliser pour effectuer la sérialisation. Pas besoin de composants externes comme: Gson ou XStream.

La solution avec Jackson:

Sérialisation:

//@JsonIdentityInfo(
//        generator = ObjectIdGenerators.PropertyGenerator.class, 
//        property = "Name")
@JsonIdentityInfo(
      generator = ObjectIdGenerators.UUIDGenerator.class, 
      property = "id")
public class StructId implements Serializable {
    private static final long serialVersionUID = 1L;

    @JsonProperty("id") // I added this field to have a unique identfier
    private UUID id = UUID.randomUUID();

Dé-sérialisation :

ObjectMapper mapper = new ObjectMapper();
    try {
        destinationStructId = destinationId.isEmpty() ? null : mapper.readValue(URLDecoder.decode(destinationId, ENCODING), StructId.class);
    } catch (IOException e) {
        throw new SpecificationException(e.getMessage());
    }

Et le plus important, vous devez utiliser l'annotation @JsonIdentityInfo:

ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
    try {
        jsonDesttinationIdString = ow.writeValueAsString(destinationId);
    } catch (JsonProcessingException ex) {
        throw new SpecificationException(ex.getMessage());
    }


0 commentaires