2016-12-03 2 views
2

Supposons que nous ayons ce JSON:Comment désérialiser des types génériques avec Moshi?

[ 
    { 
    "__typename": "Car", 
    "id": "123", 
    "name": "Toyota Prius", 
    "numDoors": 4 
    }, 
    { 
    "__typename": "Boat", 
    "id": "4567", 
    "name": "U.S.S. Constitution", 
    "propulsion": "SAIL" 
    } 
] 

(il pourrait y avoir beaucoup plus d'éléments à la liste, ce qui montre que deux)

Je Car et Boat POJO qui utilisent une classe de base Vehicle pour la commune champs:

public abstract class Vehicle { 
    public final String id; 
    public final String name; 
} 

public class Car extends Vehicle { 
    public final Integer numDoors; 
} 

public class Boat extends Vehicle { 
    public final String propulsion; 
} 

Le résultat de l'analyse de cette JSON devrait être un List<Vehicle>. Le problème est qu'aucun parseur JSON va savoir, hors de la boîte, que __typename est de savoir comment distinguer un Boat d'un Car.

Avec Gson, je peux créer un JsonDeserializer<Vehicle> qui peut examiner le champ __typename, identifier si c'est un Car ou Boat, utilisez deserialize() fourni sur le JsonDeserializationContext pour analyser l'objet JSON particulier dans le type approprié. Cela fonctionne bien.

Cependant, la chose particulière que je construis devrait supporter les parseurs JSON enfichables, et j'ai pensé que j'essaierais Moshi comme un analyseur alternative. Cependant, ce problème particulier n'est pas bien couvert dans la documentation de Moshi à l'heure actuelle, et j'ai de la difficulté à trouver la meilleure façon d'y remédier. L'équivalent le plus proche de JsonDeserializer<T>is JsonAdapter<T>. Cependant, fromJson() obtient passé un JsonReader, qui a une API destructrice. Pour savoir ce que le __typename est, je serais capable d'analyser tout à la main à partir des événements JsonReader. Alors que je pourrais appeler adapter() on the Moshi instance pour essayer d'invoquer la logique d'analyse Moshi existante une fois que je connais le type de béton approprié, j'aurai consommé des données hors du JsonReader et cassé sa capacité à fournir la description complète de l'objet plus. Un autre analogue de JsonDeserializer<Vehicle> serait un @FromJson-annotated method qui renvoie un Vehicle. Cependant, je ne peux pas identifier une chose simple à passer dans la méthode. La seule chose que je peux penser est de créer encore une autre POJO représentant l'union de tous les domaines possibles:

public class SemiParsedKindOfVehicle { 
    public final String id; 
    public final String name; 
    public final Integer numDoors; 
    public final String propulsion; 
    public final String __typename; 
} 

Puis, en théorie, si je @FromJson Vehicle rideLikeTheWind(SemiParsedKindOfVehicle rawVehicle) sur une classe que je me inscrire comme un adaptateur de type avec Moshi , Moshi pourrait être en mesure d'analyser mes objets JSON en SemiParsedKindOfVehicle instances et d'appeler rideLikeTheWind(). Dans là, je rechercherais le __typename, identifie le type, et construis complètement le Car ou le Boat moi-même, retournant cet objet. Bien que réalisable, c'est un peu plus complexe que l'approche Gson, et mon scénario Car/Boat est sur la fin simple des structures de données possibles que je vais devoir traiter.

Existe-t-il une autre approche pour gérer cela avec Moshi qui me manque?

Répondre

1

Une chose que je n'avais pas prise en compte est que vous pouvez créer un adaptateur de type en utilisant un type générique, comme Map<String, Object>. Par conséquent, vous pouvez créer un VehicleAdapter qui recherche __typename.Il sera chargé de remplir complètement les instances Car et Boat (ou, facultativement, de déléguer cela aux constructeurs sur Car et Boat qui prennent le Map<String, Object> comme entrée). Par conséquent, ce n'est pas encore aussi pratique que l'approche de Gson. De plus, vous devez avoir une méthode de ne rien faire @ToJson, sinon Moshi rejette votre adaptateur de type. Mais, sinon, cela fonctionne, comme le montre cette classe de test JUnit4:

import com.squareup.moshi.FromJson; 
import com.squareup.moshi.JsonAdapter; 
import com.squareup.moshi.Moshi; 
import com.squareup.moshi.ToJson; 
import com.squareup.moshi.Types; 
import org.junit.Assert; 
import org.junit.Test; 
import java.io.IOException; 
import java.lang.reflect.Type; 
import java.util.List; 
import java.util.Map; 
import static org.junit.Assert.assertEquals; 

public class Foo { 
    static abstract class Vehicle { 
    public String id; 
    public String name; 
    } 

    static class Car extends Vehicle { 
    public Integer numDoors; 
    } 

    static class Boat extends Vehicle { 
    public String propulsion; 
    } 

    static class VehicleAdapter { 
    @FromJson 
    Vehicle fromJson(Map<String, Object> raw) { 
     String typename=raw.get("__typename").toString(); 
     Vehicle result; 

     if (typename.equals("Car")) { 
     Car car=new Car(); 

     car.numDoors=((Double)raw.get("numDoors")).intValue(); 
     result=car; 
     } 
     else if (typename.equals("Boat")) { 
     Boat boat=new Boat(); 

     boat.propulsion=raw.get("propulsion").toString(); 
     result=boat; 
     } 
     else { 
     throw new IllegalStateException("Could not identify __typename: "+typename); 
     } 

     result.id=raw.get("id").toString(); 
     result.name=raw.get("name").toString(); 

     return(result); 
    } 

    @ToJson 
    String toJson(Vehicle vehicle) { 
     throw new UnsupportedOperationException("Um, why is this required?"); 
    } 
    } 

    static final String JSON="[\n"+ 
    " {\n"+ 
    " \"__typename\": \"Car\",\n"+ 
    " \"id\": \"123\",\n"+ 
    " \"name\": \"Toyota Prius\",\n"+ 
    " \"numDoors\": 4\n"+ 
    " },\n"+ 
    " {\n"+ 
    " \"__typename\": \"Boat\",\n"+ 
    " \"id\": \"4567\",\n"+ 
    " \"name\": \"U.S.S. Constitution\",\n"+ 
    " \"propulsion\": \"SAIL\"\n"+ 
    " }\n"+ 
    "]"; 

    @Test 
    public void deserializeGeneric() throws IOException { 
    Moshi moshi=new Moshi.Builder().add(new VehicleAdapter()).build(); 
    Type payloadType=Types.newParameterizedType(List.class, Vehicle.class); 
    JsonAdapter<List<Vehicle>> jsonAdapter=moshi.adapter(payloadType); 
    List<Vehicle> result=jsonAdapter.fromJson(JSON); 

    assertEquals(2, result.size()); 

    assertEquals(Car.class, result.get(0).getClass()); 

    Car car=(Car)result.get(0); 

    assertEquals("123", car.id); 
    assertEquals("Toyota Prius", car.name); 
    assertEquals((long)4, (long)car.numDoors); 

    assertEquals(Boat.class, result.get(1).getClass()); 

    Boat boat=(Boat)result.get(1); 

    assertEquals("4567", boat.id); 
    assertEquals("U.S.S. Constitution", boat.name); 
    assertEquals("SAIL", boat.propulsion); 
    } 
} 
+1

La désérialisation polymorphique est maintenant beaucoup mieux supportée dans la version 1.4 de Moshi. (https://github.com/square/moshi/issues/89 pour les liens vers tous les détails.) Merci pour le post. Je vais suivre la balise Moshi sur SO maintenant, alors dépassez toutes les questions! –

+0

@EricCochran: Merci pour l'info! Mais, à moins de mal interpréter les choses, cette question ne semble pas se rapporter à la désérialisation polymorphe. – CommonsWare

+0

peut-être le plus important, 'JsonAdapter.fromJsonValue (Object)' (https://github.com/square/moshi/pull/234/files) permet de créer ces valeurs à partir de Maps et autres. –