1
votes

Comment contourner la sérialisation Gson donnant des résultats différents lors de l'utilisation de caractères génériques génériques?

Prenons cet exemple:

static class BaseBean { String baseField = "base"; }
static class ChildBean extends BaseBean { String childField = "child"; }

static class BaseBeanHolder {
    List <? extends BaseBean> beans;

    public BaseBeanHolder(List<? extends BaseBean> beans) { this.beans = beans; }
}

static class ChildBeanHolder {
    List <ChildBean> beans;

    public ChildBeanHolder(List<ChildBean> beans) { this.beans = beans; }
}

@Test
public void mcve() {
    BaseBeanHolder baseHolder = new BaseBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(baseHolder));

    ChildBeanHolder childHolder = new ChildBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(childHolder));
}

Il imprime:

{"beans": [{"baseField": "base"}]}

{"beans": [{"childField": "child", "baseField": "base"}]}

Ainsi, bien que les deux listes contiennent des objets enfants, seul le deuxième détenteur entraîne la sérialisation des champs enfants en JSON.

J'ai vu d'autres questions, comme ici mais je me demande s'il existe des solutions de contournement raisonnables pour réaliser ce que je veux.

En d'autres termes: y a-t-il un moyen d'avoir une classe one "holder" qui accepte soit BaseBeans, soit ChildBeans (le extend BaseBean> fait cela), et que aussi strong > me donne les bons résultats lors de la sérialisation d'instances avec Gson dans des chaînes JSON?

(note: je ne peux pas utiliser d'adaptateurs de type spécifiques, car je n'ai aucun contrôle d'où provient cette instance réelle de Gson et comment elle est configuré dans notre environnement)


0 commentaires

3 Réponses :


1
votes

Gson est construit en tenant compte de "Je vais être utilisé pour sérialiser" et "Je vais être utilisé pour désérialiser".

Il n'y a aucun moyen de déterminer à partir du JSON brut quel est le type d'exécution exact d'un descendant de BaseBean .

Vous pouvez utiliser RuntimeTypeAdapterFactory comme décrit ici - malheureusement, il n'est pas publié avec le module Gson de base ni dans Maven Central comme décrit ici . Cela publiera suffisamment d'informations avec le JSON pour permettre à Gson de le désérialiser.


2 commentaires

Bon point, mais malheureusement, dans notre base de code, l'instance Gson arrive sur un chemin "générique", et je ne peux pas ajouter d'adaptateurs de type personnalisés pour de tels cas spécifiques.


Je ne pense pas que ce sera possible de le faire alors. Gson n'a aucun moyen de savoir sur le chemin de désérialisation quel est le type en question, donc il utilisera par défaut l'itinéraire le plus sûr possible: (Au moment de l'exécution, pouvez-vous être sûr à 100% que le seul type que vous sérialisez est ChildBean (et non d'autres descendants tels que AnotherChildBeanType, OneMoreChildBeanType, etc.)? Si tel est le cas, il existe une solution de contournement hacky que j'ai déjà déployée.



1
votes

Généralement, les implémentations de collection "prennent" le type de la déclaration du champ de collection - pas d'un élément donné dans la Liste / Set / etc. Nous devons écrire un sérialiseur personnalisé qui, pour chaque élément, trouve le sérialiseur et l'utilise. Implémentation simple:

{
  "beans": [
    {
      "baseField": "base",
      "@type": "BaseBean"
    },
    {
      "childField": "child",
      "baseField": "base",
      "@type": "ChildBean"
    },
    {
      "bean2Int": 356,
      "baseField": "base",
      "@type": "ChildBean2"
    }
  ]
}
BaseBean{baseField='base'}
ChildBean{baseField='base', childField='child'}
ChildBean2{baseField='base', bean2Int=356}

Et voici comment nous pouvons l'utiliser:

class TypeAwareListJsonAdapter implements JsonSerializer<List<?>>, JsonDeserializer<List<?>> {

    private final String typeProperty = "@type";

    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonObject jsonElement = (JsonObject) context.serialize(item, item.getClass());
            jsonElement.addProperty(typeProperty, item.getClass().getSimpleName());

            array.add(jsonElement);
        }
        return array;
    }

    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        final Type elementType = $Gson$Types.getCollectionElementType(typeOfT, List.class);

        if (json instanceof JsonArray) {
            final JsonArray array = (JsonArray) json;
            final int size = array.size();
            if (size == 0) {
                return Collections.emptyList();
            }

            final List<?> suites = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                JsonObject jsonElement = (JsonObject) array.get(i);
                String simpleName = jsonElement.get(typeProperty).getAsString();
                suites.add(context.deserialize(jsonElement, getClass(simpleName, elementType)));
            }

            return suites;
        }

        return Collections.emptyList();
    }

    private Type getClass(String simpleName, Type defaultType) {
        try {
            // you can use mapping or something else...
            return Class.forName("com.model." + simpleName);
        } catch (ClassNotFoundException e) {
            return defaultType;
        }
    }
}

Le code ci-dessus imprime:

{
  "beans": [
    {
      "baseField": "base"
    },
    {
      "childField": "child",
      "baseField": "base"
    },
    {
      "bean2Int": 356,
      "baseField": "base"
    }
  ]
}

EDIT

Lors de la sérialisation, nous perdons des informations sur le type qui seront nécessaires pendant le processus de désérialisation. J'ai développé des informations de type simples qui seront stockées lors de la sérialisation et utilisées dans la désérialisation. Cela pourrait ressembler à ceci:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        List<BaseBean> children = Arrays.asList(new BaseBean(), new ChildBean(), new ChildBean2());
        BaseBeanHolder baseHolder = new BaseBeanHolder(children);
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .create();
        System.out.println(gson.toJson(baseHolder));
    }
}

class BaseBean {
    String baseField = "base";
}

class ChildBean extends BaseBean {
    String childField = "child";
}

class ChildBean2 extends BaseBean {
    int bean2Int = 356;
}

class BaseBeanHolder {

    @JsonAdapter(TypeAwareListJsonSeserializer.class)
    private List<? extends BaseBean> beans;

    // getters, setters, toString
}

Le plus gros problème est de savoir comment mapper les classes aux valeurs JSON . Nous pouvons utiliser le nom simple de la classe ou fournir Map et l'utiliser. Maintenant, nous pouvons l'utiliser comme ci-dessus. Exemple d'application s'imprime maintenant:

class TypeAwareListJsonSeserializer implements JsonSerializer<List<?>> {
    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonElement jsonElement = context.serialize(item, item.getClass());
            array.add(jsonElement);
        }
        return array;
    }
}


5 commentaires

Très belle réponse. Maintenant, je me demande: y a-t-il une manière élégante similaire d'obtenir la désérialisation d'une manière "symétrique"? Ou cela nécessiterait-il un adaptateur de type distinct sur l'instance gson?


@GhostCat, je pense que ma réponse pour Mapping Json Array to Java Models pourrait être utile. Il a besoin d'un peu de refactorisation mais il contient principalement tout ce qu'il faut pour désérialiser l'objet JSON donné à saisir. J'essaierai d'inverser la mise en œuvre et de mettre à jour la réponse.


@GhostCat, lors de la sérialisation, nous avons perdu les informations relatives au type. Nous pouvons dans le désérialiseur essayer de désérialiser vers de nombreux sous-types et vérifier le fit rate pour un objet donné, mais ce sera problématique avec de nombreux sous-types. Le moyen le plus simple est d'inclure un champ supplémentaire pendant la sérialisation - comme le nom simple de la classe et d'utiliser cette information pendant la désérialisation. Bien sûr, si nous voulons avoir une classe d'adaptateur de type pour la sérialisation et la désérialisation, il faut étendre deux interfaces: JsonSerializer> et JsonDeserializer > .


Ouais, j'avais des idées similaires. Mais tout va bien maintenant. Mon plus gros problème pour le moment est de décider si je veux utiliser votre solution, ou ma solution de contournement (voir ci-dessous, en utilisant des tableaux) ou de prendre du recul et d'utiliser simplement les deux classes de détenteurs différentes (ce qui signifie plus de code, mais des types plus expressifs à être utilisé pour les différents cas d'utilisation dont j'ai besoin).


@GhostCat, le truc avec tableau est génial! Je ne savais pas que le processus de sérialisation des tableaux est différent de celui des collections . J'ai ajouté un exemple simple de sérialisation / désérialisation type-aware . J'espère que cela vous aidera d'une manière ou d'une autre.



1
votes

Plus d'un addendum: je viens de penser qu'au moins la sérialisation fonctionne bien avec les tableaux , donc une solution de contournement simple était de retravailler le support:

static class BaseBeanHolder {
    BaseBean[] beans;
    public BaseBeanHolder(BaseBean... beans) { this.beans = beans; }
}


0 commentaires