Search code examples
kotlingenericskotlinx.serialization

Is there any way to require an annotation on a type parameter in kotlin?


I am making a data structures that automatically persist data to the disk. To do this I am using kotlinx-serialization. Is there any way to require a generic type to have the @Serializabe annotation?

class PersistentList <@Serializable E> () : List<E> {...}

This example doesn't compile. Hopefully you understand what I mean. I want to constrain E to have the annotation. Any way to do that?


Solution

  • KSerializer constructor parameter

    You could add a KSerializer for E in the constructor of PersistentList.

    class PersistentList<E>(
        elementSerializer: KSerializer<E>
    ) : List<E> { 
      // ... 
    }
    

    While more verbose, it has an advantage over adding an annotation to E - a custom serializer could be assigned, meaning that PersistentList could contain classes from external code that do not have the @Serializable annotation.

    import kotlinx.serialization.KSerializer
    
    // Assume this class is from another library, meaning @Serializable cannot be added
    data class SomeExternalData(
      val name: String
    }
    
    // Create a custom serializer for SomeExternalData
    object SomeExternalDataSerializer : KSerializer<SomeExternalData> {
      // ...
    }
    
    fun main() {
      // Since a serializer is required, the `E` must be serializable
      val persistentList = PersistentList(SomeExternalDataSerializer)
    }
    
    
    class PersistentList< E: PersistentElement>(
      elementSerializer: KSerializer<E>,
    ) : List<E> {
      // ...
    }
    

    That's the only feasible way to ensure that E is serializable. Even an annotation couldn't do this!

    SerializersModule.serializer()

    To make it a little more automatic, you could use a SerializersModule, which has a function, serializer(), that can fetch the serializer via reflection magic.

    It's possible to create an extension function that works like Json.encodeToString(...), that will automatically determine the serializer for a specific format.

    /** Automatically determine the serializable type of [PersistentList] */
    inline fun <reified T> SerialFormat.createPersistentList(): PersistentList<SomeData> =
        PersistentList(serializersModule.serializer())
    

    However, this would not trigger any compile-time checks.

    Here's a more complete example:

    import kotlinx.serialization.KSerializer
    import kotlinx.serialization.SerialFormat
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.builtins.ListSerializer
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.serializer
    
    @Serializable
    data class SomeData(
      val name: String
    )
    
    fun main() {
      val persistentList = Json.createPersistentList(listOf(
          SomeData("1"), SomeData("2"), SomeData("3"),
        ))
    
      println(persistentList)
    
      val encoded = Json.encodeToString(persistentList.listSerializer, persistentList.actualList)
    
      println(encoded)
    }
    
    class PersistentList<E>(
      elementSerializer: KSerializer<E>,
      val actualList: List<E>,
    ) : List<E> by actualList {
      val listSerializer = ListSerializer(elementSerializer)
    }
    
    
    inline fun <reified E> SerialFormat.createPersistentList(actualList: List<E>): PersistentList<E> =
      PersistentList(serializersModule.serializer(), actualList)