Десериализация JSON в класс с универсальным аргументом с использованием GSON или Джексона

Я получаю ответы от моего сервера, которые выглядят так:

{
  "timestamp" : 1,
  "some other data": "blah",
  "result" : {
     ...
  }
}

для разнообразных звонков. Что я хочу сделать на стороне клиента:

class ServerResponse<T> {
    long timestamp;
    T result;

}

и затем десериализовать это с GSON или Джексоном. Я не смог сделать это благодаря стиранию типов. Я обманул это, используя такие подклассы:

class SpecificResponse extends ServerRequest<SpecificType> {}

но это требует кучу бесполезных классов, чтобы лежать вокруг. У кого-нибудь есть способ получше?

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

2 ответа

Решение

Типичным решением для стирания типа в этом случае является взлом токена типа, который использует анонимные классы, поддерживающие информацию суперкласса для использования с отражением.

Джексон обеспечивает TypeReference типа, а также ObjectMapper#readValue перегрузка, чтобы использовать это.

В вашем примере вы бы использовали

ServerResponse response = objectMapper.readValue(theJsonSource, new TypeReference<ServerResponse<SpecificType>>() {});

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


Что касается поддержки как отдельных значений, так и массивов в JSON, вы можете изменить свое поле на Collection тип. Например,

List<T> results

Затем включите DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY,

Функция, которая определяет, допустимо ли принудительно использовать значения, не относящиеся к массиву (в JSON), для работы с коллекцией Java (массивами, java.util.Collection) типы. Если этот параметр включен, десериализаторы коллекций будут пытаться обрабатывать значения, не относящиеся к массиву, как если бы они имели "неявное" окружение массива JSON.

Я поддерживаю @Pillar решение использовать Jackson потому что это так просто. 2 строки кода...

Вот версия Gson, которая будет работать точно так же, но для этого вам понадобится специальный десериализатор и немного размышлений.

public static class CustomDeserializer implements JsonDeserializer<ServerResponse> {
    @Override
    public ServerResponse deserialize(JsonElement je, Type respT,
                          JsonDeserializationContext jdc) throws JsonParseException {

        Type t = (respT instanceof ParameterizedType) ?
                ((ParameterizedType) respT).getActualTypeArguments()[0] :
                Object.class;

        JsonObject jObject = (JsonObject) je;

        ServerResponse serverResponse = new ServerResponse();

        //can add validation and null value check here
        serverResponse.timestamp = jObject.get("timestamp").getAsLong();

        JsonElement dataElement = jObject.get("result");

        if (dataElement != null) {
            if (dataElement.isJsonArray()) {
                JsonArray array = dataElement.getAsJsonArray();

                // can use ((ParameterizedType) respT).getActualTypeArguments()
                // instead of new Type[]{t} 
                // if you 100% sure that you will always provide type
                Type listT = ParameterizedTypeImpl.make(List.class, new Type[]{t}, null);

                serverResponse.result  = jdc.deserialize(array, listT);
            } else if(dataElement.isJsonObject()) {
                serverResponse.result = new ArrayList();
                serverResponse.result.add(jdc.deserialize(dataElement, t));
            }
        }
        return serverResponse;
    }
}

Случай использования очень похож на Джексона:

Gson gson = new GsonBuilder()
           .registerTypeAdapter(ServerResponse.class, new CustomDeserializer())
           .create();

ServerResponse<MyObject> s = gson.fromJson(json, new TypeToken<ServerResponse<MyObject>>(){}.getType())
Другие вопросы по тегам