Reputation: 3520
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
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
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