Использование Gson с типами интерфейсов

Я работаю над некоторым кодом сервера, где клиент отправляет запросы в виде JSON. Моя проблема заключается в том, что существует ряд возможных запросов, каждый из которых отличается небольшими деталями реализации. Поэтому я решил использовать интерфейс запроса, который определяется как:

public interface Request {
    Response process ( );
}

Оттуда я реализовал интерфейс в классе с именем LoginRequest, как показано ниже:

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

Чтобы работать с JSON, я создал экземпляр GsonBuilder и зарегистрировал InstanceCreator, как показано:

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

который затем я использовал, как показано ниже в следующем фрагменте:

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

и я получаю ожидаемый результат.

То, что я хочу сделать, это заменить строку Request request = parser.fromJson(completeInput, LoginRequest.class); чем-то похожей на Request request = parser.fromJson(completeInput, Request.class);, но это не сработает, так как Request - это интерфейс.

Я хочу, чтобы мой Gson возвращал соответствующий тип запроса в зависимости от полученного JSON.

Пример JSON, который я передал серверу, показан ниже:

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

Чтобы повторить, я ищу способ разобрать запросы (в JSON) от клиентов и вернуть объекты классов, реализующие интерфейс Request

Ответы

Ответ 1

Предполагая, что различные возможные запросы JSON, которые у вас могут быть, не очень отличаются друг от друга, я предлагаю другой подход, более простой, на мой взгляд.

Скажем, что у вас есть три разных запроса JSON:

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

Gson позволяет вам иметь один класс для обертывания всех возможных ответов, например:

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
}

Используя аннотацию @SerializedName, когда Gson пытается разобрать запрос JSON, он просто выглядит для каждого именованного атрибута в классе, если в запросе JSON есть одно имя. Если такого поля нет, атрибут в классе просто устанавливается на null.

Таким образом, вы можете анализировать множество разных ответов JSON, используя только ваш класс Request, например:

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

После того, как ваш JSON-запрос проанализирован в вашем классе, вы можете перенести данные из класса wrap в конкретный объект XxxxRequest, например:

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

Обратите внимание, что этот подход становится немного более утомительным, если у вас много разных запросов JSON, и эти запросы сильно отличаются друг от друга, но даже я считаю, что это хороший и очень простой подход...

Ответ 2

Полиморфное отображение описанного типа недоступно в Gson без некоторого уровня пользовательского кодирования. Существует дополнительный адаптер расширения как дополнительный, который обеспечивает большую часть функциональности, которую вы ищете, с оговоркой, что полиморфные подтипы должны быть заранее объявлены адаптеру. Вот пример его использования:

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

Обратите внимание, что вам действительно не нужно определять свойство type для отдельных объектов Java - оно существует только в JSON.

Ответ 3

Genson библиотека обеспечивает поддержку полиморфных типов по умолчанию. Вот как это работает:

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

Вы также можете использовать псевдонимы для своих типов.

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

По умолчанию GSON не может отличать классы, сериализованные как JSON; другими словами, вам нужно явно указать парсеру, какой класс вы ожидаете.

Решение может быть настраиваемой десериализации или с помощью адаптера типа, как описано здесь.