28

I am working on some server code, where the client sends requests in form of JSON. My problem is, there are a number of possible requests, all varying in small implementation details. I therefore thought to use a Request interface, defined as:

public interface Request {
    Response process ( );
}

From there, I implemented the interface in a class named LoginRequest as shown:

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 + "]";
    }
}

To work with JSON, I created a GsonBuilder instance and registered an InstanceCreator as shown:

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

which I then used as shown in the snippet below:

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);

and I get the expected output.

The thing I wish to do is replace the line Request request = parser.fromJson(completeInput, LoginRequest.class); with something similar to Request request = parser.fromJson(completeInput, Request.class); but doing that will not work, since Request is an interface.

I want my Gson to return the appropriate type of request depending on the received JSON.

An example of the JSON I passed to the server is shown below:

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

To reiterate, I am looking for a way to parse requests (In JSON) from clients and return objects of classes implementing the Request interface

MikO
  • 18,243
  • 12
  • 77
  • 109
fredmanglis
  • 487
  • 1
  • 5
  • 10
  • Can you please provide other examples of the different JSON responses you can get from the server? Because if you don't have many and very different possibilities, there's something you can do easy... – MikO May 06 '13 at 11:14
  • Thanks @MiKO for your input. Other likely requests are `PlayRequest`, `LogoutRequest`, `GetPlayersRequest`, `JoinGameRequest`, `StartGameRequest` etc... – fredmanglis May 06 '13 at 12:26
  • I mean if you can provide an example of the JSON request for at least one of those other types of requests. I mean, for your `LoginRequest` you have fiels: `type`, `username` and `password`, what about other requests? How do they look? – MikO May 06 '13 at 12:49

6 Answers6

28

Polymorphic mapping of the type described is not available in Gson without some level of custom coding. There is an extension type adapter available as an extra that provides a bulk of the functionality you are looking for, with the caveat that the polymorphic sub-types need to be declared to the adapter ahead of time. Here is an example of its use:

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);
    }
}

Note that you don't actually need to define the type property on the individual Java objects - it exists only in the JSON.

Eduardo
  • 4,282
  • 2
  • 49
  • 63
Perception
  • 79,279
  • 19
  • 185
  • 195
  • 5
    For those who are missing `RuntimeTypeAdapterFactory`, you can use this [gson-extras](https://github.com/DanySK/gson-extras) that is available on maven-central (the project's purpose is only to make it available on maven-central). – Tomask May 30 '17 at 09:17
  • According to this issue - https://github.com/google/gson/issues/1104 google doesn't upload gson-extras to maven central -> it must be built manually. – Pavel_K Apr 12 '20 at 21:00
9

Assuming that the different possible JSON requests you may have are not extremely different to each other, I suggest a different approach, simpler in my opinion.

Let's say that you have these 3 different JSON requests:

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

Gson allows you to have a single class to wrap all the possible responses, like this:

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
}

By using the annotation @SerializedName, when Gson try to parse the JSON request, it just look, for each named attribute in the class, if there's a field in the JSON request with the same name. If there's no such field, the attribute in the class is just set to null.

This way you can parse many different JSON responses using only your Request class, like this:

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

Once you have your JSON request parsed into your class, you can transfer the data from the wrap class to a concrete XxxxRequest object, something like:

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;
}

Note that this approach gets a bit more tedious if you have many different JSON requests and those requests are very different to each other, but even so I think is a good and very simple approach...

MikO
  • 18,243
  • 12
  • 77
  • 109
  • Thanks @MikO. I guess then the `switch-case` structure could go into some Request factory of sorts. Thanks. That was helpful. Let me look into that. – fredmanglis May 07 '13 at 08:57
  • Yes, putting the switch into a `RequestFactory` class definitely makes sense. – MikO May 07 '13 at 09:12
4

Genson library provides support for polymorphic types by default. Here is how it would work:

// 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);

You can also use aliases for your 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);
eugen
  • 5,856
  • 2
  • 29
  • 26
0

By default, GSON cannot differentiate classes serialized as JSON; in other words, you will need to explicitly tell the parser what class you are expecting.

A solution could be custom deserializing or using a type adapter, as described here.

Tony the Pony
  • 40,327
  • 71
  • 187
  • 281
0

I found this answer: https://stackoverflow.com/a/28830173 which solved my issue when using Calendar as the interface as the RunTimeType would be GregorianCalendar.

0

Have a utility method to create GSON for an interface of generic type.

// Utility method to register interface and its implementation to work with GSON

public static <T> Gson buildInterface(Class<T> interfaceType, List<Class<? extends T>> interfaceImplmentations) {
    final RuntimeTypeAdapterFactory<T> typeFactory = RuntimeTypeAdapterFactory.of(interfaceType, "type");
    for (Class<? extends T> implementation : interfaceImplmentations) {
        typeFactory.registerSubtype(implementation);
    }
    final Gson gson = new GsonBuilder().registerTypeAdapterFactory(typeFactory).create();
    return gson;
}

// Build Gson

List<Class<? extends Request>> customConfigs = new ArrayList<>();
customConfigs.add(LoginRequest.getClass());
customConfigs.add(SomeOtherRequest.getClass());
Gson gson = buildInterface(Request.class, customConfigs);

Use this gson to serialize or deserialize and this works.

2sb
  • 629
  • 2
  • 12
  • 26