0

GSON fails to convert Errorneous to JSON properly when it's inside of other Object.

But it works well when it's converted as a top level object. Why, and how to fix it?

Example:

import com.google.gson.GsonBuilder

sealed class Errorneous<R> {}
data class Success<R>(val result: R) : Errorneous<R>()
data class Fail<R>(val error: String) : Errorneous<R>()

class Container(val value: Errorneous<String>)

fun main() {
  print(GsonBuilder().create().toJson(Container(Fail("some error"))))

  print(GsonBuilder().create().toJson(Fail<String>("some error")))
}

Output

{"value":{}}

{"error":"some error"}

But it should be

{"value":{"error":"some error"}}

{"error":"some error"}
Alex Craft
  • 13,598
  • 11
  • 69
  • 133

2 Answers2

1

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.

  • Thank you for taking time and writing the solution code! It's not good that you have to write JSON mapping code manually, but there's probably no better way... – Alex Craft Jan 27 '21 at 18:33
  • 1
    @AlexCraft Unfortunately, type adapter factories accept only the declaration site type via type tokens, so the actual runtime type should be resolved in other way (e.g. manually). I guess that the Gson authors decided not to include it by default because of several issues, one of which I mentioned above and suppressed with an unsupported operation exception stub (no easy way to deserialize). It's only a guess, as they also mention that non static anonymous classes cannot serialized because of a similar reason (no way to deserialize its inner `this`). – terrorrussia-keeps-killing Jan 27 '21 at 18:39
0

Answer in Kotlin copied from the similar Java Question

import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type

sealed class Errorneous<R> {}
data class Success<R>(val result: R) : Errorneous<R>()
data class Fail<R>(val error: String) : Errorneous<R>()

class Container(
  val value: Errorneous<String>
 )

fun main() {
  val builder = GsonBuilder()
  builder.registerTypeAdapter(
    Errorneous::class.java, ErrorneousSerializer()
  )
  val gson = builder.create()
  print(gson.toJson(Container(Fail("some error"))))
  print(gson.toJson(Fail<String>("some error")))
}

class ErrorneousSerializer : JsonSerializer<Errorneous<Any>> {
  override fun serialize(
    o: Errorneous<Any>, type: Type, ctx: JsonSerializationContext
  ): JsonElement {
    return ctx.serialize(o as Any)
  }
}
Alex Craft
  • 13,598
  • 11
  • 69
  • 133