androidjsonkotlindeserializationkotlinx.serialization

How to deserialize multiple json fields into single object with kotlinx serialization


Consider the following json structure representing a basic check-in information

{
  "id": "53481198005",
  "date": "1995-01-01 00:00:00",
  "latitude": "50.765391",
  "longitude": "60.765391"
}

During deserialization, I want to consume latitude and longitude and create a single object called Location. In other words, I want to parse this json with POJO classes similar to:

@Serializable
data class CheckInRecord(val id: String, val date: String, val location: Location)

@Serializable
data class Location(val latitude: String, val longitude: String)

How can I achieve this?


Solution

  • You can approach this with the surrogate serializer pattern, which is in exactly the same fashion as in the question I answered earlier today, but in reverse (as that was about encoding rather than decoding).

    To apply the pattern, you start off writing a serializable surrrogate object to match the JSON you want:

    @Serializable
    class CheckInRecordSurrogate(
        private val id: String,
        private val date: String,
        private val latitude: String,
        private val longitude: String
    ) {
        companion object {
            fun fromCheckInRecord(checkInRecord: CheckInRecord) = with(checkInRecord) {
                CheckInRecordSurrogate(id, date, location.latitude, location.longitude)
            }
        }
    
        fun toCheckInRecord() = CheckInRecord(id, date, Location(latitude, longitude))
    }
    

    This gives you the tools to implement the KSerializer interface for your target class CheckInRecord by 'plugging in the gaps' to translate the surrogate to your desired end object (and back, for completeness):

    class CheckInRecordSerializer : KSerializer<CheckInRecord> {
        private val surrogateSerializer get() =  CheckInRecordSurrogate.serializer()
        override val descriptor: SerialDescriptor get() = surrogateSerializer.descriptor
    
        override fun deserialize(decoder: Decoder) = surrogateSerializer.deserialize(decoder).toCheckInRecord()
    
        override fun serialize(encoder: Encoder, value: CheckInRecord) {
            surrogateSerializer.serialize(encoder, CheckInRecordSurrogate.fromCheckInRecord(value))
        }
    }
    

    Finally, you must tell the framework to use your new serializer on CheckInRecord (note we don't even need to make Location serializable with this approach):

    @Serializable(with = CheckInRecordSerializer::class)
    data class CheckInRecord(val id: String, val date: String, val location: Location)
    

    Now we can try it all out:

    println(
        Json.decodeFromString<CheckInRecord>("""
            {
                 "id": "53481198005",
                 "date": "1995-01-01 00:00:00",
                 "latitude": "50.765391",
                 "longitude": "60.765391"
            }
         """)
    ) 
    

    Which prints what we expect, showing CheckInRecord was successfully instantiated:

    CheckInRecord(id=53481198005, date=1995-01-01 00:00:00, location=Location(latitude=50.765391, longitude=60.765391))