kotlinserialization

How can I serialize a calculated property?


I have a data class with a calculated property:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Mcve (
    val account: String
) {
    @SerialName("type")
    val type =
        if (account.startsWith("1"))
            "preferred"
        else
            "regular"
}

fun main() {
    println(Json.encodeToString(Mcve.serializer(), Mcve("12345")))
}

I want to have that type property serialized, but the serialization framework ignores it. The above main just prints:

{"account":"12345"}

I've tried to use lazy like below, but the result is the same, the type is not included in the JSON.

    @SerialName("type")    
    val type : String by lazy {
        if (account.startsWith("1"))
            "preferred"
        else
            "regular"
    }

How can I include the type in the JSON object?


Solution

  • This feature is not provided at present by the library, as the GitHub issue linked by @AlexT in the comments testifies. It looks like the library authors will be forced to look at this again in the near future though, as Kotlin is shortly implementing a new backing fields feature.

    I note this is not a problem if you are serializing to transfer an object between two Kotlin programs that share the class definition in common code, as the deserializer will recreate the property when the class is instantiated. But it is when, as presumably in your case, you are passing the object to a non-Kotlin recipient.

    The way to solve the problem for now is to use a surrogate serializer1. In your case this would be to write a new class to represent the form of JSON you want with the type property as a regular property:

    @Serializable
    class McveSurrogate(
        private val account: String, private val type: String
    ) {
        companion object { fun fromMcve(mcve: Mcve) = with(mcve) { McveSurrogate(account, type) } }
    
        fun toMove(): Mcve = Mcve(account)
    }
    

    Then you can use this as the basis of a new serializer:

    class McveSerializer : KSerializer<Mcve> {
        private val surrogateSerializer get() = McveSurrogate.serializer()
    
        override val descriptor: SerialDescriptor get() = surrogateSerializer.descriptor
    
        override fun deserialize(decoder: Decoder): Mcve = surrogateSerializer.deserialize(decoder).toMove()
    
        override fun serialize(encoder: Encoder, value: Mcve) {
            surrogateSerializer.serialize(encoder, McveSurrogate.fromMcve(value) )
        }
    }
    

    And finally you can instruct the framework to use this on your original class:

    @Serializable(with = McveSerializer::class)
    class Mcve
    

    1This is a very common pattern if you are going to use this framework regularly and with non-standard classes.