Search code examples
kotlinserializationkotlinx.serialization

How to serialize Any with Kotlin serialization?


How can I serialize the below data? This is AppConfig and the value of value could be any type and I can't use Any for serialization.

[
 {
    "key": "PROFILE_PHOTO",
    "value": "url"
  },
  {
    "key": "RELATIONSHIP",
    "value": 0.6
  },
  {
    "key": "SEX",
    "value": [ "Man", "Woman"]
  },
  {
    "key": "IOS_VER",
    "value": 5
  }
]

Solution

  • Polymorphism is the answer

    The short answer is: with polymorphism. We need different types to represent the different kinds of objects that can be received.

    To a large extent, the choice of types depends on what the meaning and use of the objects are, not just looking at the raw data as we are here.

    However, we can guess at suitable types based on what you've given.

    A sealed class hierarchy

    Now we want to write a sealed class hierarchy to represent the incoming objects because this makes polymorphic serialization straightforward (docs):

    @Serializable
    @JsonClassDiscriminator("key")
    sealed class ApiObject
    
    @Serializable @SerialName("PROFILE_PHOTO")
    data class ProfilePhoto(@SerialName("value") val url: String) : ApiObject()
    
    @Serializable @SerialName("RELATIONSHIP")
    data class Relationship(@SerialName("value") val coefficient: Double) : ApiObject()
    
    @Serializable @SerialName("SEX")
    data class Sex(@SerialName("value") val sexes: List<SexType>) : ApiObject()
    
    @Serializable @SerialName("IOS_VER")
    data class IosVersion(@SerialName("value") val versionNumber: Int) : ApiObject()
    
    enum class SexType { Man, Woman }
    

    We've also used a few features of the library to give us a more usable class hierarchy. In particular:

    • We've given a name to every possible type of message we can receive from the API, and we've used the SerialName annotation to allow meaningful names to be given to the incoming "value" fields.

    • The "key" field is going to be used as our 'discriminator', which tells the library which one of the classes to pick. This is the "type" field by default, so we need to tell the library to use "key" instead with the JsonClassDiscriminator annotation.

    • And finally, because the "key" fields are given in screaming snake case, we need to adjust the serialized names of the classes with the SerialName annotation in order that our class names can conform to customary Kotlin practice.

    Testing the code

    Now we are ready to test our code:

    val testMessage: String = """
    [
     {
        "key": "PROFILE_PHOTO",
        "value": "url"
      },
      {
        "key": "RELATIONSHIP",
        "value": 0.6
      },
      {
        "key": "SEX",
        "value": [ "Man", "Woman"]
      },
      {
        "key": "IOS_VER",
        "value": 5
      }
    ]
    """
    val deserializedMessage: List<ApiObject> = Json.decodeFromString(testMessage)
    
    val expectedDeserializedMessage = listOf(
        ProfilePhoto("url"),
        Relationship(0.6),
        Sex(listOf(SexType.Man, SexType.Woman)),
        IosVersion(5)
    )
    println("Does our code work? ${expectedDeserializedMessage == deserializedMessage}")