I'm using moshi with codegen. I have a class `User...
# squarelibraries
p
I'm using moshi with codegen. I have a class
User
with many fields and class
CurrentUser
which should have all the fields from
User
plus some other keys. That's how I'm receiving it from the server:
CurrentUser
just has some additional fields that should be presented in json. e.g. user is
{"name":"Phil", "avatarUrl":"<https://some.url>"}
and current user is
{"name":"Phil", "avatarUrl":"<https://some.url>", "token":"lorem ipsum"}
I could've inherit, but it'd require me to repeat all of user fields. I solved it by making a custom adapter factory that reads extra properties from json and than pass it to
UserAdapter
to fill it. But it takes a lot of boilerplate code, maybe there's some built in solution for such case? I'd like to add some annotation to
user
property so codegen would write something similar for me. Or is there a way to build a general factory for such cases?
Copy code
data class CurrentUser(
    val token: String,
    val user: User,
) {
    companion object {
        private const val tokenKey = "token"

        val adapterFactory = JsonAdapter.Factory { type, _, moshi ->
            if (type != CurrentUser::class.java) {
                return@Factory null
            }

            val userJsonAdapter = moshi.adapter(User::class.java)
            object : JsonAdapter<CurrentUser>() {
                override fun fromJson(reader: JsonReader): CurrentUser? {
                    val jsonValue = reader.readJsonValue()
                        ?: return null
                    if (jsonValue !is Map<*, *>) {
                        throw IllegalStateException("${this@Companion} readJsonValue is not map: $jsonValue")
                    }
                    return CurrentUser(
                        token = jsonValue[tokenKey] as? String
                            ?: throw IllegalStateException("$tokenKey not found in $jsonValue"),
                        user = userJsonAdapter.fromJsonValue(jsonValue)
                            ?: throw IllegalStateException("Failed to parse user from $jsonValue"),
                    )
                }

                override fun toJson(writer: JsonWriter, value: CurrentUser?) {
                    value?.user
                        ?.let(userJsonAdapter::toJsonValue)
                        ?.asMap()
                        ?.plus(mapOf(tokenKey to value.token))
                        ?.let(writer::jsonValue)
                }
            }
        }
    }
}
c
Zac def knows better than I and I'm still not an amazing software engineer, but if this were me I think I would just treat that "DTO" object as always being
UserResponseDTO
and then in my domain or something, I would just check if
token
exists (or some other invariant) and then map to a different domain model (
User
or
CurrentUser
) based on that invariant. Seems dead simple to me that way (which is nice?)
p
@Zac Sweers the issues seems to be relevant, but it doesn't seems to be resolved to me, and I didn't get how the sample from the comment can help. So the problem is to read a flat json - not
"{\"type\":1,\"rawJson\":{\"a\":2,\"b\":3,\"c\":[1,2,3]}}"
but
"{\"type\":1,\"a\":2,\"b\":3,\"c\":[1,2,3]}"
into some object which will be a composition of other objects.
@Colton Idle I guess that's an option, but it still requires a lot of properties duplication when mapping to the domain model
I guess from Kotlin perspective perfectly both user and current user should be interfaces, and code generation would build classes based on inherited properties. But I don't think Moshi can do something like this.
c
If at the root of your object you had a
type
field then you could use PolymorphicTypeAdapter from moshi to go into a User or CurrentUser pretty easily (if I'm understanding the question correctly).
Maybe you can use PTA to have a discriminator based on the presence of a field in json...?