Deserialize a field that can be one of two data types using Moshi

3.2k views Asked by At

I'm receiving some JSON from an OrientDB server that looks something like this:

{
    ...
    "out": ...,
    "in": ...,
    ...
}

Now these two fields out and in can be one of two types: String and my own custom object (let's call it a Record). For example, for one request I might receive this:

{
    ...
    "out": "#17:0",
    "in": {
        ...
    },
    ...
}

For another I might get:

{
    ...
    "out": {
        ...
    },
    "in": "#18:2",
    ...
}

And so on. Both might be Strings, both might be Records, one might be a String and the other a Record, et cetera et cetera. Now when I'm deserializing this kind of JSON using Moshi, I'd have two parameters out and in to hold the values of their respective keys; however, because these values aren't a fixed data type, that's easier said than done.

Creating multiple POJOs (or "POKO"s, I guess, because I'm using Kotlin) wouldn't work, because these objects can be found inside other JSON objects and stuff like that. I'd need a single object for which these parameters can take on a variable data type. So how would I do that?

Would I have to write a custom adapter in Moshi for serializing/deserializing these values? If so, how would I go about writing one that can assign a certain data type depending on the value of the parameter? Or is there some sort of Kotlin class/function/extension function I can find/write that can hold two possible data types?

If it's relevant, I'm also using Retrofit 2 + RxJava 2 to make my HTTP calls asynchronously, so if there's any data types or functions in these libraries that facilitates something like this, I'm all ears.

Even if anyone can only answer in Java that's okay, because I can convert the code myself. And if I'm missing something obvious, I apologize in advance.

1

There are 1 answers

1
Thiago Saraiva On

You can do something like what I did in this answer: https://stackoverflow.com/a/65106419/3543610

Basically you'd create a sealed class to be the type of both your properties in and out. You'd also need to wrap the primitive string one into a type holding a String, so you can make it extend the sealed class, something like this:

sealed class YourType {
    data class StringData(val value: String) : YourType()

    @JsonClass(generateAdapter = true)
    data class Record(
        val prop1: String,
        val prop2: Int
    ) : YourType()
}

Then your model that has these properties would look smth like:

@JsonClass(generateAdapter = true)
data class Model(
    ...
    val in: YourType,
    val out: YourType,
    ...
)

then finally you write your custom adapter for the YourType type:

class YourTypeCustomAdapter {
    @FromJson
    fun fromJson(jsonReader: JsonReader, delegate: JsonAdapter<Record>): YourType? {
        return if (jsonReader.peek() == BEGIN_OBJECT) {
            delegate.fromJson(jsonReader)
        } else {
            StringData(jsonReader.nextString())
        }
    }

    @ToJson
    fun toJson(jsonWriter: JsonWriter, yourType: YourType, delegate: JsonAdapter<Record>) {
        when (yourType) {
            is Record -> delegate.toJson(jsonWriter, yourType)
            is StringData -> jsonWriter.value(yourType.value)
        }
    }
}

and register with Moshi:

private val moshi = Moshi.Builder()
    .add(YourTypeCustomAdapter())
    .build()