web-dev-qa-db-fra.com

Utilisation de Gson avec des types d'interface

Je travaille sur un code de serveur, où le client envoie des requêtes sous forme de JSON. Mon problème est qu’il existe un certain nombre de requêtes possibles, toutes variant dans les détails de la mise en œuvre. J'ai donc pensé à utiliser une interface de requête, définie comme suit:

public interface Request {
    Response process ( );
}

A partir de là, j'ai implémenté l'interface dans une classe nommée LoginRequest comme indiqué:

public class LoginRequest implements Request {
    private String type = "LOGIN";
    private String username;
    private String password;

    public LoginRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * This method is what actually runs the login process, returning an
     * appropriate response depending on the outcome of the process.
     */
    @Override
    public Response process() {
        // TODO: Authenticate the user - Does username/password combo exist
        // TODO: If the user details are ok, create the Player and add to list of available players
        // TODO: Return a response indicating success or failure of the authentication
        return null;
    }

    @Override
    public String toString() {
        return "LoginRequest [type=" + type + ", username=" + username
            + ", password=" + password + "]";
    }
}

Pour travailler avec JSON, j'ai créé une instance GsonBuilder et enregistré une InstanceCreator comme indiqué:

public class LoginRequestCreator implements InstanceCreator<LoginRequest> {
    @Override
    public LoginRequest createInstance(Type arg0) {
        return new LoginRequest("username", "password");
    }
}

que j'ai ensuite utilisé comme indiqué dans l'extrait ci-dessous:

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(LoginRequest.class, new LoginRequestCreator());
Gson parser = builder.create();
Request request = parser.fromJson(completeInput, LoginRequest.class);
System.out.println(request);

et je reçois la sortie attendue.

Ce que je souhaite faire est de remplacer la ligne Request request = parser.fromJson(completeInput, LoginRequest.class); par quelque chose de similaire à Request request = parser.fromJson(completeInput, Request.class);, mais cela ne fonctionnera pas, car Request est une interface.

Je souhaite que ma Gson renvoie le type de demande approprié en fonction du JSON reçu.

Un exemple de JSON que j'ai transmis au serveur est présenté ci-dessous:

{
    "type":"LOGIN",
    "username":"someuser",
    "password":"somepass"
}

Je le répète, je cherche un moyen d’analyser les demandes (en JSON) des clients et de renvoyer des objets de classes implémentant l’interface Request.

23
fredmanglis

En supposant que les différentes requêtes JSON possibles ne soient pas très différentes les unes des autres, je propose une approche différente, plus simple à mon sens.

Disons que vous avez ces 3 requêtes JSON différentes:

{
    "type":"LOGIN",
    "username":"someuser",
    "password":"somepass"
}
////////////////////////////////
{
    "type":"SOMEREQUEST",
    "param1":"someValue",
    "param2":"someValue"
}
////////////////////////////////
{
    "type":"OTHERREQUEST",
    "param3":"someValue"
}

Gson vous permet d’avoir une seule classe pour encapsuler toutes les réponses possibles, comme ceci:

public class Request { 
  @SerializedName("type")   
  private String type;
  @SerializedName("username")
  private String username;
  @SerializedName("password")
  private String password;
  @SerializedName("param1")
  private String param1;
  @SerializedName("param2")
  private String param2;
  @SerializedName("param3")
  private String param3;
  //getters & setters
}

En utilisant l'annotation @SerializedName, lorsque Gson tente d'analyser la demande JSON, il recherche simplement, pour chaque attribut nommé de la classe, s'il existe un champ dans la demande JSON portant le même nom. Si ce type de champ n'existe pas, l'attribut dans la classe est simplement défini sur null

De cette façon, vous pouvez analyser de nombreuses réponses JSON en utilisant uniquement votre classe Request, comme ceci:

Gson gson = new Gson();
Request request = gson.fromJson(jsonString, Request.class);

Une fois que votre demande JSON a été analysée dans votre classe, vous pouvez transférer les données de la classe wrap vers un objet XxxxRequest concret, comme:

switch (request.getType()) {
  case "LOGIN":
    LoginRequest req = new LoginRequest(request.getUsername(), request.getPassword());
    break;
  case "SOMEREQUEST":
    SomeRequest req = new SomeRequest(request.getParam1(), request.getParam2());
    break;
  case "OTHERREQUEST":
    OtherRequest req = new OtherRequest(request.getParam3());
    break;
}

Notez que cette approche devient un peu plus fastidieuse si vous avez beaucoup de demandes JSON différentes et si ces demandes sont très différentes les unes des autres, mais même dans ce cas, je pense que c'est une approche bonne et très simple ...

9
MikO

Le mappage polymorphe du type décrit n'est pas disponible dans Gson sans un certain niveau de codage personnalisé. Il existe un adaptateur de type d’extension disponible en supplément qui fournit la majeure partie des fonctionnalités recherchées, avec l’avertissement que les sous-types polymorphes doivent être déclarés à l’adaptateur à l’avance. Voici un exemple d'utilisation:

public interface Response {}

public interface Request {
    public Response process();
}

public class LoginRequest implements Request {
    private String userName;
    private String password;

    // Constructors, getters/setters, overrides
}

public class PingRequest implements Request {
    private String Host;
    private Integer attempts;

    // Constructors, getters/setters, overrides
}

public class RequestTest {

    @Test
    public void testPolymorphicSerializeDeserializeWithGSON() throws Exception {
        final TypeToken<List<Request>> requestListTypeToken = new TypeToken<List<Request>>() {
        };

        final RuntimeTypeAdapterFactory<Request> typeFactory = RuntimeTypeAdapterFactory
                .of(Request.class, "type")
                .registerSubtype(LoginRequest.class)
                .registerSubtype(PingRequest.class);

        final Gson gson = new GsonBuilder().registerTypeAdapterFactory(
                typeFactory).create();

        final List<Request> requestList = Arrays.asList(new LoginRequest(
                "bob.villa", "passw0rd"), new LoginRequest("nantucket.jones",
                "crabdip"), new PingRequest("example.com", 5));

        final String serialized = gson.toJson(requestList,
                requestListTypeToken.getType());
        System.out.println("Original List: " + requestList);
        System.out.println("Serialized JSON: " + serialized);

        final List<Request> deserializedRequestList = gson.fromJson(serialized,
                requestListTypeToken.getType());

        System.out.println("Deserialized list: " + deserializedRequestList);
    }
}

Notez qu'il n'est pas nécessaire de définir la propriété type sur les objets Java individuels. Elle n'existe que dans le JSON.

26
Perception

Genson library prend en charge les types polymorphes par défaut. Voici comment cela fonctionnerait:

// tell genson to enable polymorphic types support
Genson genson = new Genson.Builder().setWithClassMetadata(true).create();

// json value will be {"@class":"mypackage.LoginRequest", ... other properties ...}
String json = genson.serialize(someRequest);
// the value of @class property will be used to detect that the concrete type is LoginRequest
Request request = genson.deserialize(json, Request.class);

Vous pouvez également utiliser des alias pour vos types.

// a better way to achieve the same thing would be to use an alias
// no need to use setWithClassMetadata(true) as when you add an alias Genson 
// will automatically enable the class metadata mechanism
genson = new Genson.Builder().addAlias("loginRequest", LoginRequest.class).create();

// output is {"@class":"loginRequest", ... other properties ...}
genson.serialize(someRequest);
4
eugen

Par défaut, GSON ne peut pas différencier les classes sérialisées en JSON. En d'autres termes, vous devrez indiquer explicitement à l'analyseur quelle classe vous attendez.

Une solution pourrait consister à désérialiser custom ou à utiliser un adaptateur type, comme décrit ici .

0
Tony the Pony