Десериализация 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())