olfek
olfek

Reputation: 3520

Making Gson work for interface type determined by outer field

Question is similar to this but different in that the type is not contained within the JSON object being deserialized but is instead contained in the outer level (root).

Like so:

{
    "type": "A",
    "myObject" : { <- type is NOT stored in here
        ...
    }
}

The intention is to map this JSON object to Blah

// Code is in Kotlin

interface Foo {
    ...
}

class Bar : Foo { // Map to this if 'type' is A
    ...
}

class Baz : Foo { // Map to this if 'type' is B
    ...
}

class Blah {

    val type : String? = null
    val myObject : Foo? = null
}

How do I make myObject map to Bar if type is A and Baz if type is B?

Temporarily, I've resorted to reading in the root JSON object manually. Any help would be much appreciated. Thanks.

EDIT:

When trying to map the root JSON object to Blah using Gson fromJson method, I get this error: Unable to invoke no-args constructor for class Foo. - But this is incorrect anyway, because I need myObject to map specifically to either Baz or Bar.

Upvotes: 1

Views: 992

Answers (2)

olfek
olfek

Reputation: 3520

Here is my solution to this. Solution is in Kotlin.

Edit class Blah:

class Blah {

    val type : String? = null

    @ExcludeOnDeserialization // <- add this
    val myObject : Foo? = null
}

Inside some static class:

inline fun <T : Annotation>findAnnotatedFields(
    annotation: Class<T>,
    clazz : Class<*>,
    onFind : (Field) -> Unit
){

    for(field in clazz.declaredFields){

        if(field.getAnnotation(annotation)!=null){

            field.isAccessible = true

            onFind(field)
        }
    }
}

Create new class and annotation:

@Target(AnnotationTarget.FIELD)
annotation class ExcludeOnDeserialization

class GsonExclusionStrategy : ExclusionStrategy {

    override fun shouldSkipClass(clazz: Class<*>?): Boolean {
        return clazz?.getAnnotation(ExcludeOnDeserialization::class.java) != null
    }

    override fun shouldSkipField(f: FieldAttributes?): Boolean {
        return f?.getAnnotation(ExcludeOnDeserialization::class.java) != null
    }
}

Read root json object:

...

val gson = GsonBuilder()
    .addDeserializationExclusionStrategy(GsonExclusionStrategy())
    .create()

val rootJsonObject = JsonParser().parse(rootJsonObjectAsString)
val blah = gson.fromJson(rootJsonObject, Blah::class.java)

findAnnotatedFields(
    ExcludeOnDeserialization::class.java,
    Blah::class.java
){ foundExcludedField -> // foundExcludedField = 'myObject' declared in 'Blah' class

    val myObjectAsJsonObject
        = rootJsonObject.asJsonObject.getAsJsonObject(foundExcludedField.name)

    when (foundExcludedField.type) {

        Foo::class.java -> {

            when (blah.type) {

                "A" -> {

                    foundExcludedField.set(
                        blah,
                        gson.fromJson(myObjectAsJsonObject, Bar::class.java)
                    )
                }

                "B" -> {

                    foundExcludedField.set(
                        blah,
                        gson.fromJson(myObjectAsJsonObject, Baz::class.java)
                    )
                }

                else -> return null
            }
        }
    }
}

// The root json object has now fully been mapped into 'blah'

Inspiration for this solution came from this article

Upvotes: 0

taka
taka

Reputation: 1427

This is quite similar as the question you've mentioned.

You can define a deserializer for Blah and decide which class should be used.

Code will like below.

import com.google.gson.*

interface Foo {
    fun bark()
}

class Bar : Foo { // Map to this if 'type' is A
    override fun bark() {
        print("bar")
    }
}

class Baz : Foo { // Map to this if 'type' is B
    override fun bark() {
        print("baz")
    }
}

class Blah(val type : String? = null, val myObject : Foo? = null) {
    companion object {
        const val TYPE_A = "A"
        const val TYPE_B = "B"
    }
}

class BlahJsonDeserializer: JsonDeserializer<Blah> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Blah {
        val root = json?.asJsonObject
        val type = root?.get("type")?.asString
        var obj: Foo? = null
        when(type ?: "") {
            Blah.TYPE_A -> { obj = Bar() }
            Blah.TYPE_B -> { obj = Baz() }
        }
        val blah = Blah(type, obj)
        return blah
    }
}

val json = "{'type': 'A', 'myObject': {}}"

val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(Blah::class.java, BlahJsonDeserializer())
val gson = gsonBuilder.create()
val item = gson.fromJson<Blah>(json, Blah::class.java)

item.myObject?.bark() // bar

Upvotes: 1

Related Questions