Одно и то же поле имеет два разных типа, что создает проблемы с преобразователем Gson для Retrofit 2
3 ответа
Вот как я решил эту проблему:
Создайте в вашей модели адаптер нестандартного типа и проанализируйте его вручную;
public class AccountState {
//@SerializedName("rated") //NOPE, parse it manually
private Integer mRated; //also don't name it rated
public Integer getRated() {
return mRated;
}
public void setRated(Integer rated) {
this.mRated = rated;
}
public static class AccountStateDeserializer implements JsonDeserializer<AccountState> {
@Override
public AccountState deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
AccountState accountState = new Gson().fromJson(json, AccountState.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("rated")) {
JsonElement elem = jsonObject.get("rated");
if (elem != null && !elem.isJsonNull()) {
if(elem.isJsonPrimitive()){
accountState.setRated(null);
}else{
accountState.setRated(elem.getAsJsonObject().get("value").getAsInt());
}
}
}
return accountState ;
}
}
}
Здесь вы создаете свой GSON с помощью этого специального адаптера:
final static Gson gson = new GsonBuilder()
.registerTypeAdapter(AccountState.class, new AccountState.AccountStateDeserializer())
.create();
Добавьте его для модернизации следующим образом:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.ENDPOINT)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
TADADADADADADADDAD!
Вы можете заставить его работать без необходимости настраивать конвертер. Все, что вам нужно сделать, это установить общий тип "Object" для переменной, а затем просто проверить, какой это тип данных, выполнив это:
if(object.getClass == YourClass.class){
Whatever we = ((YourClass) object).getWhatever();
} else if(object.getClass == YourOtherClass.class){
String name = ((YourOtherClass) object).getName();
}
Вы можете добавить столько типов данных к этой переменной, сколько захотите. Вы также можете использовать java-типы "String.class", "Boolean.class" или что угодно.
У Gson есть приятная функция, позволяющая вводить пользовательский адаптер типа или фабрику адаптеров типа в определенное поле, что позволяет Gson управлять хост-объектом и сериализацией полей (де) последнего. Таким образом, вы можете быть уверены, что AccountState
может быть еще десериализован с ReflectiveTypeAdapterFactory
а также ReflectiveTypeAdapterFactory.Adapter
так что все стратегии десериализации, определенные в GsonBuilder, могут быть применены.
final class AccountState {
// This is what can make life easier. Note its advantages:
// * PackedBooleanTypeAdapterFactory can be reused multiple times
// * AccountState life-cycle can be managed by Gson itself,
// so it can manage *very* complex deserialization automatically.
@JsonAdapter(PackedBooleanTypeAdapterFactory.class)
final Boolean rated = null;
}
Далее как PackageBooleanTypeAdapterFactory
реализовано:
final class PackedBooleanTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate this itself, no need to expose
private PackedBooleanTypeAdapterFactory() {
}
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's the type we can handle ourself
if ( typeToken.getRawType() == Boolean.class ) {
final TypeAdapter<Boolean> typeAdapter = new PackedIntegerTypeAdapter(gson);
// Some Java "unchecked" boilerplate here...
@SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
// If it's something else, let Gson pick a downstream type adapter on its own
return null;
}
private static final class PackedIntegerTypeAdapter
extends TypeAdapter<Boolean> {
private final Gson gson;
private PackedIntegerTypeAdapter(final Gson gson) {
this.gson = gson;
}
@Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
@Override
public Boolean read(final JsonReader in)
throws MalformedJsonException {
// Pick next token as a JsonElement
final JsonElement jsonElement = gson.fromJson(in, JsonElement.class);
// Note that Gson uses JsonNull singleton to denote a null
if ( jsonElement.isJsonNull() ) {
return null;
}
if ( jsonElement.isJsonPrimitive() ) {
return jsonElement
.getAsJsonPrimitive()
.getAsBoolean();
}
if ( jsonElement.isJsonObject() ) {
return jsonElement
.getAsJsonObject()
.getAsJsonPrimitive("value")
.getAsBoolean();
}
// Not something we can handle
throw new MalformedJsonException("Cannot parse: " + jsonElement);
}
}
}
Демо-версия:
public static void main(final String... args) {
parseAndDump("{\"rated\":null}");
parseAndDump("{\"rated\":true}");
parseAndDump("{\"rated\":{\"value\":true}}");
}
private static void parseAndDump(final String json) {
final AccountState accountState = gson.fromJson(json, AccountState.class);
System.out.println(accountState.rated);
}
Выход:
ноль
правда
правда
Обратите внимание, что JsonSerializer
а также JsonDeserializer
оба имеют некоторую производительность и стоимость памяти из-за их древовидной модели (вы можете легко обходить деревья JSON, пока они находятся в памяти). Иногда для простых случаев предпочтительным может быть адаптер потокового типа. Плюсы: потребляет меньше памяти и работает быстрее. Минусы: сложно реализовать.
final class AccountState {
@JsonAdapter(PackedBooleanTypeAdapter.class)
final Boolean rated = null;
}
Обратите внимание, что rated
поле принимает адаптер типа напрямую, потому что это не нужно Gson
экземпляры для построения деревьев JSON (JsonElement
с).
final class PackedBooleanTypeAdapter
extends TypeAdapter<Boolean> {
// Gson still can instantiate this type adapter itself
private PackedBooleanTypeAdapter() {
}
@Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
@Override
public Boolean read(final JsonReader in)
throws IOException {
// Peeking the next JSON token and dispatching parsing according to the given token
final JsonToken token = in.peek();
switch ( token ) {
case NULL:
return parseAsNull(in);
case BOOLEAN:
return parseAsBoolean(in);
case BEGIN_OBJECT:
return parseAsObject(in);
// The below might be omitted, since some code styles prefer all switch/enum constants explicitly
case BEGIN_ARRAY:
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case END_DOCUMENT:
throw new MalformedJsonException("Cannot parse: " + token);
// Not a known token, and must never happen -- something new in a newer Gson version?
default:
throw new AssertionError(token);
}
}
private Boolean parseAsNull(final JsonReader in)
throws IOException {
// null token still has to be consumed from the reader
in.nextNull();
return null;
}
private Boolean parseAsBoolean(final JsonReader in)
throws IOException {
// Consume a boolean value from the reader
return in.nextBoolean();
}
private Boolean parseAsObject(final JsonReader in)
throws IOException {
// Consume the begin object token `{`
in.beginObject();
// Get the next property name
final String property = in.nextName();
// Not a value? Then probably it's not what we're expecting for
if ( !property.equals("value") ) {
throw new MalformedJsonException("Unexpected property: " + property);
}
// Assuming the property "value" value must be a boolean
final boolean value = in.nextBoolean();
// Consume the object end token `}`
in.endObject();
return value;
}
}
Этот должен работать быстрее. Выход остается прежним. Обратите внимание, что Gson не требует GsonBuilder
для обоих случаев. Насколько я помню, как работает Retrofit 2, GsonConverterFactory
все еще требуется (не уверен, что Gson не является сериализатором по умолчанию в Retrofit 2?).