Figen Güngör
Figen Güngör

Reputation: 12559

Same field has two different types gives trouble with Gson converter for Retrofit 2

Here is the json schema:

enter image description here

As you can see, rated can be both boolean and object.

I am using Retrofit 2 and Gson converter. How should I create my model for this schema?

Upvotes: 7

Views: 4925

Answers (3)

shadowpath
shadowpath

Reputation: 39

You can make it work without having to implement a custom converter. All you have to do is put a general "Object" type for the variable and then you just check which data type it is by doing this:

if(object.getClass == YourClass.class){
  Whatever we = ((YourClass) object).getWhatever();
} else if(object.getClass == YourOtherClass.class){
  String name = ((YourOtherClass) object).getName();
}

You can add as many data types to this variable as you like. You can also use the java types "String.class", "Boolean.class" or whatever you like.

Upvotes: 2

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21115

Gson has a nice feature allowing to inject a custom type adapter or a type adapter factory to a certain field therefore letting Gson to manage the host object and the latter's fields (de)serialization. So, you can be sure that AccountState could be still deserialized with ReflectiveTypeAdapterFactory and ReflectiveTypeAdapterFactory.Adapter so all deserialization strategies defined in GsonBuilder could be applied.

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;

}

Next, how PackageBooleanTypeAdapterFactory is implemented:

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

    }

}

Demo:

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

Output:

null
true
true

Note that JsonSerializer and JsonDeserializer both have some performance and memory cost due to its tree model design (you can traverse JSON trees easily as long as they are in memory). Sometimes, for simple cases, a streaming type adapter may be preferable. Pros: consumes less memory and works faster. Cons: hard to implement.

final class AccountState {

    @JsonAdapter(PackedBooleanTypeAdapter.class)
    final Boolean rated = null;

}

Note that the rated field accepts a type adapter directly because it does not need Gson instances to build JSON trees (JsonElements).

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

}

This one should work faster. The output remains the same. Note that Gson does not require a GsonBuilder for both cases. As far as I remember how Retrofit 2 works, GsonConverterFactory is still required (not sure, Gson is not the default serializer in Retrofit 2?).

Upvotes: 0

Figen G&#252;ng&#246;r
Figen G&#252;ng&#246;r

Reputation: 12559

Here's how I solved this issue:

Create a custom type adapter in your model and parse rated manually;

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

}

Here you create your gson with this custom adapter:

final static Gson gson = new GsonBuilder()
            .registerTypeAdapter(AccountState.class, new AccountState.AccountStateDeserializer())
            .create();

Add it to retrofit like that:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BuildConfig.ENDPOINT)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .client(okHttpClient)
                .build();

TADADADADADADADDAD!

Upvotes: 4

Related Questions