I made some comments regarding Gson behavior right under the post (in short: not enough runtime type information), so this is only code to make it work and make it actual type-aware.
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<? super T> rawType = typeToken.getRawType();
if ( rawType != Errorneous.class ) {
return null;
}
final ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
@SuppressWarnings("unchecked")
final TypeToken<Success<?>> successTypeToken = (TypeToken<Success<?>>) TypeToken.getParameterized(Success.class, parameterizedType.getActualTypeArguments());
@SuppressWarnings("unchecked")
final TypeToken<Fail<?>> failTypeToken = (TypeToken<Fail<?>>) TypeToken.getParameterized(Fail.class, parameterizedType.getActualTypeArguments());
final TypeAdapter<Success<?>> successTypeAdapter = gson.getDelegateAdapter(this, successTypeToken);
final TypeAdapter<Fail<?>> failTypeAdapter = gson.getDelegateAdapter(this, failTypeToken);
final TypeAdapter<Errorneous<?>> concreteTypeAdapter = new TypeAdapter<Errorneous<?>>() {
@Override
public void write(final JsonWriter out, final Errorneous<?> value)
throws IOException {
if ( value instanceof Success ) {
final Success<?> success = (Success<?>) value;
successTypeAdapter.write(out, success);
return;
}
if ( value instanceof Fail ) {
final Fail<?> fail = (Fail<?>) value;
failTypeAdapter.write(out, fail);
return;
}
throw new AssertionError(); // even null cannot get here: it is protected with .nullSafe() below
}
@Override
public Errorneous<?> read(final JsonReader in) {
throw new UnsupportedOperationException();
}
};
@SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = ((TypeAdapter<T>) concreteTypeAdapter)
.nullSafe();
return typeAdapter;
}
})
.create();
@AllArgsConstructor
@SuppressWarnings("unused")
private abstract static class Errorneous<R> {
}
@AllArgsConstructor
@SuppressWarnings("unused")
private static final class Success<R>
extends Errorneous<R> {
private final R result;
}
@AllArgsConstructor
@SuppressWarnings("unused")
private static final class Fail<R>
extends Errorneous<R> {
private final String error;
}
@AllArgsConstructor
@SuppressWarnings("unused")
private static class Container {
private final Errorneous<String> value;
}
public static void main(final String... args) {
System.out.println(gson.toJson(new Container(new Fail<>("some error"))));
System.out.println(gson.toJson(new Fail<>("some error")));
}
As you can see, the type adapter factory first resolves type adapters for both Success
and Fail
, and then picks a proper one based on the actual class of the Errorneous
value with instanceof
().
Here is what it prints:
{"value":{"error":"some error"}}
{"error":"some error"}
The deserialization is made an unsupported operation since it must decide how the JSON can be deserialized: 1) either on a type designator field (see RuntimeTypeAdapterFactory
in Gson extras in their repository on GitHub; it's not bundled and published as an artifact); 2) or analyze the structure of the object making heuristics analysis (much harder to implement and may face with ambiguous cases).
I don't do Kotlin, but the Java code above can be probably easily converted to its Kotlin counterpart right in IntelliJ IDEA.