Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GSON Deserialization of subtypes in Kotlin

I'm not sure if this is a limitation, a bug or just bad use of GSON. I need to have a hierarchy of Kotlin objects (parent with various subtypes) and I need to deserialize them with GSON. The deserialized object has correct subtype but its field enumField is actually null.

First I thought this is because the field is passed to the "super" constructor but then I found out that "super" works well for string, just enum is broken.

See this example:

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory

open class Parent(val stringField: String,
                  val enumField: EnumField) {

    enum class EnumField {
        SUBTYPE1,
        SUBTYPE2,
        SUBTYPE3
    }
}


class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2)
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type)

val subtypeRAF = RuntimeTypeAdapterFactory.of(Parent::class.java, "enumField")
        .registerSubtype(Subtype1::class.java, Parent.EnumField.SUBTYPE1.name)
        .registerSubtype(Subtype2::class.java, Parent.EnumField.SUBTYPE2.name)
        .registerSubtype(Subtype3::class.java, Parent.EnumField.SUBTYPE3.name)

fun main() {
    val gson = GsonBuilder()
            .registerTypeAdapterFactory(subtypeRAF)
            .create()

    serializeAndDeserialize(gson, Subtype1()) // this works (but not suitable)
    serializeAndDeserialize(gson, Subtype2("s2")) // broken
    serializeAndDeserialize(gson, Subtype3("s3", Parent.EnumField.SUBTYPE3)) // broken
}

private fun serializeAndDeserialize(gson: Gson, obj: Parent) {
    println("-----------------------------------------")
    val json = gson.toJson(obj)
    println(json)
    val obj = gson.fromJson(json, Parent::class.java)
    println("stringField=${obj.stringField}, enumField=${obj.enumField}")
}

Any ideas how to achieve to deserialization of enumField?

(deps: com.google.code.gson:gson:2.8.5, org.danilopianini:gson-extras:0.2.1)

P.S.: Note that I have to use RuntimeAdapterFactory because I have subtypes with different set of fields (I did not do it in the example so it is easier to understand).

like image 452
zdenda.online Avatar asked Sep 27 '19 12:09

zdenda.online


2 Answers

Gson requires constructors without arguments to work properly (see deep-dive into Gson code below). Gson constructs raw objects and then use reflection to populate fields with values.

So if you just add some argument-less dummy constructors to your classes that miss them, like this:

class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2) {
    constructor() : this("")
}
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type) {
    constructor() : this("", EnumField.SUBTYPE3)
}

you will get the expected output:

-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3

Gson deep-dive

If you want to investigate the internals of Gson, a tip is to add an init { } block to Subtype1 since it works and then set a breakpoint there. After it is hit you can move up the call stack, step through code, set more breakpoints etc, to reveal the details of how Gson constructs objects.

By using this method, you can find the Gson internal class com.google.gson.internal.ConstructorConstructor and its method newDefaultConstructor(Class<? super T>) that has code like this (I have simplified for brevity):

    final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); // rawType is e.g. 'class Subtype3'
    Object[] args = null;
    return (T) constructor.newInstance(args);

i.e. it tries to construct an object via a constructor without arguments. In your case for Subtype2 and Subtype3, the code will result in a caught exception:

    } catch (NoSuchMethodException e) { // java.lang.NoSuchMethodException: Subtype3.<init>()
      return null; // set breakpoint here to see
    }

i.e. your original code fails since Gson can't find constructors without arguments for Subtype2 and Subtype3.

In simple cases, the problem with missing argument-less constructors is worked around with the newUnsafeAllocator(Type, final Class<? super T>)-method in ConstructorConstructor, but with RuntimeTypeAdapterFactory that does not work correctly.

like image 117
Enselic Avatar answered Oct 05 '22 04:10

Enselic


I may be missing something in what you're trying to achieve, but is it necessary to use the RuntimeTypeAdapterFactory? If we take out the line where we register that in the Gson builder, so that it reads

val gson = GsonBuilder()
    .create()

Then the output returns the enum we would expect, which looks to be serialising / deserialising correctly. I.e. the output is:

-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3

It also may be an idea to implement Serializable in Parent. i.e.

open class Parent(val stringField: String, val enumField: EnumField) : Serializable {

    enum class EnumField {
        SUBTYPE1,
        SUBTYPE2,
        SUBTYPE3
    }
}
like image 43
Nelson Wright Avatar answered Oct 05 '22 04:10

Nelson Wright