androidjsonkotlingsonmoshi

Android Kotlin Moshi: Custom adapter for Moshi not working


I have a json than can have two possible values

{"school.name": "A school name"}

or

{"name": "Another school name"}

And I have a common "College" class to serialize/deserialize both.

With Gson I am able to set an annotation like this:

@SerializedName(
    value = "school.name",
    alternate = ["name"]
)

But I'm currently moving to Moshi and I am not being able to replicate that behaviour.

My class "College" with the adapter looks like this:

@JsonClass(generateAdapter = false)
internal class CollegeUS(
    @Json(name = "school.name")
    val college: String?
) {

    @JsonClass(generateAdapter = false)
    class CollegeWorldwide(
        @Json(name = "name")
        val college: String?
    )

    companion object {
        val JSON_ADAPTER: Any = object : Any() {
            @ToJson
            private fun toJson(collegeWorldwide: String): String {
                throw NotImplementedError("Required to make test pass")
            }

            @FromJson
            private fun fromJson(collegeWorldwide: CollegeWorldwide): CollegeWorldwide {
                if (collegeWorldwide.college != null) return CollegeWorldwide(collegeWorldwide.college)
                throw JsonDataException("No school.name or name key found.")
            }
        }
    }
}

My goal is -obviously- that depending on the input string json to return a CollegeUS instance where "college" property has the corresponding json value.

I'm testing with this piece of code:

val collegeUS = "{\"school.name\": \"US\"}"
val collegeWorldwide = "{\"name\": \"Worldwide\"}"

val oCollegeUS = collegeUS.toCollege()
val oCollegeWorldwide = collegeWorldwide.toCollege()

and this extension:

private fun String.toCollege(): CollegeUS.CollegeWorldwide? {
    val moshi = Moshi.Builder().add(CollegeUS.JSON_ADAPTER).addLast(KotlinJsonAdapterFactory()).build()
    val adapter = moshi.adapter(CollegeUS.CollegeWorldwide::class.java)
    return adapter.fromJson(this)
}

But this way is only working for CollegeWorldwide json.

I sure I'm doing something wrong defining/implementing the adapter, but I'm new to Moshi, so cannot figure out what.

Edit:

To clarify, I expect the same exact behaviour as I was having with Gson, just only one CollegeUS class. CollegeWorldwide is what in the example given below he calls "Intermediate" (I just wanted to have a more appropriate name), but I expect to end with only CollegeUS.

I took the example code from here

Edit 2:

In order to clarify even more:

I've set here only one property for CollegeUS to simplify, but -considering the full class- from the next two possible json:

{
    "name" : "Any college",
    "state-province" : "Any province", 
    "country" : "Any country",
    "page" : "1"
}

and

{
    "college_name" : "Any college",
    "state" : "Any state",
    "page" : "1"
}

I need to get a "CollegeUS" instance with the corresponding properties (null or empty string when there is no such property in json).

I was following the code I've pasted below because it's the only way I've found to accomplish what I need, but according @Marcono1234 I have to use "KotlinJsonAdapterFactory()", and if I'm not wrong KotlinJsonAdapterFactory() uses reflection, so it's a bit slower.

I'd like to know if this is the best/only way to accomplish that, or if there is a way without using reflection that could be more efficient.


Solution

  • If you want to use an intermediate class as shown in the GitHub issue you linked, then that intermediate class needs both fields. And then it might make more sense to indeed call the class ...Intermediate (or similar) because it really is just that, an intermediate representation which has two properties for the same field. So it is neither a "college US" nor a "college worldwide" but a combination of both.

    So your code could then look like this:

    @JsonClass(generateAdapter = false)
    internal class CollegeUS(
        val college: String
    ) {
        @JsonClass(generateAdapter = false)
        class CollegeIntermediate(
            // Name can be either provided as `name` or as `school.name` property
            val name: String?,
            @Json(name = "school.name")
            val schoolName: String?
        )
    
        companion object {
            val JSON_ADAPTER: Any = object : Any() {
                @FromJson
                private fun fromJson(collegeIntermediate: CollegeIntermediate): CollegeUS {
                    val name = collegeIntermediate.name ?: collegeIntermediate.schoolName
                        ?: throw JsonDataException("No school.name or name key found.")
    
                    return CollegeUS(name)
                }
            }
        }
    }
    

    And you could then use it for example like this:

    val moshi = Moshi.Builder()
        .add(CollegeUS.JSON_ADAPTER)
        .addLast(KotlinJsonAdapterFactory())
        .build()
    val adapter = moshi.adapter(CollegeUS::class.java)
    println(adapter.fromJson("{\"name\": \"US\"}")?.college)