Home > OS >  Gson: Derserialize a dynamic field
Gson: Derserialize a dynamic field

Time:03-03

I'm using a very strange api, it's data field type is dynamic.
If error occured, the data field will be a string like this:

{
  "code": 2001,
  "data": "Error!"
}

And if success, the data field will be a object:

{
  "code": 2000,
  "data": {
    "id": 1,
    "name": "example"
  }
}

I'm using the following Kotlin code to de-serialize it:

return Gson().fromJson(data, SimpleModel::class.java)

The model definition is down below:

import com.google.gson.annotations.SerializedName

class SimpleModel {
    @SerializedName("code")
    val code = 0

    @SerializedName("data")
    val data: SimpleData = SimpleData()
}

class SimpleData {
    @SerializedName("id")
    val id = ""

    @SerializedName("name")
    val name = ""
}

When no error occred, the code above works just fine. However when error occured, exception was thrown:

java.lang.IllegalStateException: Excepted BEGIN_OBJECT but was STRING at line 1 column x $path.data

Is there a way to de-serialize data field to an object or just anything and determine it's type by code manually?

CodePudding user response:

You would need to write a custom deserializer, which decides what type to deserialize data into, depending on the runtime type of the node, and register that deserializer with your Gson instance. Unfortunately i am not familiar with kotlin syntax, so i can only give you pseudo code.

  1. Field data in SimpleModel should be either Object, or make the class generic - SimpleModel<T>, and the field should be of type T as well.
  2. Parse the input to gson's node type - JsonElement.
  3. Get data field
JsonElement root = parseResponse();
root.getAsJsonObject().get("data").getAsString();
  1. Use getAs...() methods to check type.
  2. Get as string. If success, it's a string and set the string value in SimpleModel.
  3. If you get exception getting as string, get it as object - getAsJsonObject(), parse the object to SimpleData and set this new object in SimpleModel.

You could use my my answer here as inspiration. Although it's about object mapper, it does the same thing - decides object type depending on the node type, and follows roughly the same algorithm i described above.

Also this guide has info about how to write yor own deserialzer and registering it.

CodePudding user response:

Like the previous answer, I am not familiarized with Kotlin, and the following solution is in Java, but as I know it is easy to convert Java to Kotlin using IntelliJ built-in tools.

The success/error objects pair is a classic problem, and you can create your own way to solve it, but let's consider the following classes represent the success and error objects respectively (Java 17, pattern matching on switch enabled then):

abstract sealed class SimpleModel<T>
        permits SimpleModelSuccess, SimpleModelError {

    @SerializedName("code")
    final int code;

    SimpleModel(final int code) {
        this.code = code;
    }

}

final class SimpleModelSuccess<T>
        extends SimpleModel<T> {

    @SerializedName("data")
    final T data;

    private SimpleModelSuccess(final int code, final T data) {
        super(code);
        this.data = data;
    }

}

final class SimpleModelError<T>
        extends SimpleModel<T> {

    @SerializedName("data") // the annotation is helping here!
    final String message;

    private SimpleModelError(final int code, final String message) {
        super(code);
        this.message = message;
    }

}

The code above can explain itself. Now the core part that required more work than I thought before by providing you my first comment that appeared incomplete.

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
final class SimpleModelTypeAdapterFactory
        implements TypeAdapterFactory {

    @Getter
    private static final TypeAdapterFactory instance = new SimpleModelTypeAdapterFactory();

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( !SimpleModel.class.isAssignableFrom(typeToken.getRawType()) ) {
            return null;
        }
        // let's figure out what the model is parameterized with
        final Type type = typeToken.getType();
        final Type typeParameter;
        if ( type instanceof ParameterizedType parameterizedType ) {
            typeParameter = parameterizedType.getActualTypeArguments()[0];
        } else {
            throw new UnsupportedOperationException("Cannot infer type parameter from "   type);
        }
        // then borrow their respective type adapters for both success and error cases
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> successDelegate = (TypeAdapter<T>) gson.getDelegateAdapter(this, TypeToken.getParameterized(SimpleModelSuccess.class, typeParameter));
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> errorDelegate = (TypeAdapter<T>) gson.getDelegateAdapter(this, TypeToken.getParameterized(SimpleModelError.class, typeParameter));
        return new TypeAdapter<>() {
            @Override
            public void write(final JsonWriter out, final T value) {
                throw new UnsupportedOperationException();
            }

            @Override
            public T read(final JsonReader in) {
                // buffer the JSON tree first
                // note that this solution may be very inefficient under some circumstances
                final JsonObject buffer = Streams.parse(in).getAsJsonObject();
                final JsonElement dataElement = buffer.get("data");
                // is it's data is {...}, the consider it is success (by the way, what is code about?)
                if ( dataElement.isJsonObject() ) {
                    return successDelegate.fromJsonTree(buffer);
                }
                // if it's a primitive, consider it's an error
                if ( dataElement.isJsonPrimitive() ) {
                    return errorDelegate.fromJsonTree(buffer);
                }
                // well we've done our best...
                throw new JsonParseException(String.format("Cannot deduce the model for %s", buffer.getClass()));
            }
        };
    }

}
public final class SimpleModelTypeAdapterFactoryTest {

    private static final class SomeJsonProvider
            implements ArgumentsProvider {

        @Override
        public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
            return Stream.of(
                    Arguments.of(
                            """
                            {
                                "code": 2000,
                                "data": {
                                    "id": 1,
                                    "name": "example"
                                }
                            }
                            """
                    ),
                    Arguments.of(
                            """
                            {
                                "code": 2001,
                                "data": "Error!"
                            }
                            """
                    )
            );
        }

    }

    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @ToString
    private static final class SimpleData {

        private final String id;
        private final String name;

    }

    private static final Type simpleModelOfSimpleDataType = TypeToken.getParameterized(SimpleModel.class, SimpleData.class)
            .getType();

    @ParameterizedTest
    @ArgumentsSource(SomeJsonProvider.class)
    public void test(final String json) {
        final Gson gson = new GsonBuilder()
                .disableHtmlEscaping()
                .disableInnerClassSerialization()
                .registerTypeAdapterFactory(SimpleModelTypeAdapterFactory.getInstance())
                .create();
        final SimpleModel<SimpleData> model = gson.fromJson(json, simpleModelOfSimpleDataType);
        switch ( model ) {
        case SimpleModelSuccess<SimpleData> success -> System.out.println(success.data);
        case SimpleModelError<SimpleData> error -> System.out.println(error.message);
        }
    }

}

Here is what it prints to stdout:

SimpleModelTypeAdapterFactoryTest.SimpleData(id=1, name=example)
Error!

Well, yeah, this is a "bit" more tricky than it was suggested by my first comment.

  • Related