Одно и то же поле имеет два разных типа, что создает проблемы с преобразователем Gson для Retrofit 2

Вот схема JSON:

введите описание изображения здесь

Как видите, рейтинг может быть как логическим, так и объектным.

Я использую Retrofit 2 и Gson конвертер. Как мне создать мою модель для этой схемы?

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?).

Другие вопросы по тегам