Per defecte la serialització és completament estàtica respecte els tipus, però en la serialització de estructures de dades polimòrfiques el tipus de les dades es determina en temps d'execució.

Per defecte la serialització és completament estàtica respecte els tipus: l'estructura del objectes codificats es determina en temps de compilació pel tipus dels objectes. Però en la serialització de estructures de dades polimòrfiques el tipus de les dades es determina en temps d'execució.

Closed polymorphism

Static types

Per mostrar la naturalesa estàtica de la serialització de Kotlin, fem la configuració següent: la classe open class Project només té la propietat name, mentre que la classe derivada class OwnedProject afegeix un propietat owner:

@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

Com que em declarat la variable data amb el tipus Project, l'instància data és serialitza com a Project encara sigui una instància de OwnedProject:

{ "name": "kotlinx.coroutines" }

Pots veure que encara que la instància tingui un valor per la propietat owner aquesta no es serialitza.

Si canviem el tipus de la variable data a OwnedProject:

@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

Però ara tenim un error en temps d'execució perquè la classe OwnedProject no és serializable:

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'OwnedProject' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Disseny de jerarquies serialitzables

La solució no és tan senzilla com marcar OwnedProject com @Serializable pel requisit que les propietats han d'estar declarades al constructor, i name no ho està.

Per fer que la jerarquia de classes sigui serializable, podem declararla classe Project com abstracta i marcar les propietats de la classe com abstract:

@Serializable
abstract class Project {
    abstract val name: String
}

class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

Però aquesta solució no és suficient tal com pots verificar si executes el codi:

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'OwnedProject' is not found in the polymorphic scope of 'Project'.
Check if class with serial name 'OwnedProject' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'.

A més de l'error, et dona la solució.

Sealed classes

La manera més senzilla d'utilitzar la serialització amb una jerarquia polimòrfica és marcar la classe base amb sealed.

Totes les subclasses d'una classe segellada s'han de marcar explícitament com a @Serializable:

package dev.xtec.data

import ...

@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data)) // Serializing data of compile-time type Project
}

Si mires el resultat, pots veure com es representa en JSON un dada polimòrfica:

{ 
   "type": "dev.xtec.data.OwnedProject",
   "name": "kotlinx.coroutines",
   "owner":"kotlin"
}

Pots veure que s'afageix la propietat type com a discriminador a l'objecte JSON.

I un altre detall molt important!

En el codi la variable data és de tipus Project encara que en temps d'execució tingui una instància de tipus OwnedProject.

Quan serialitzes jerarquies de classes polimòrfiques t'has d'assegurar que el tipus en temps de compilació del tipus a serializar sigui polimòrfic, en aquest cas Project, i no concret.

Vegem què passa si la variable data és de tipus OwnedProject:

package dev.xtec.data

import ...

@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data = OwnedProject("kotlinx.coroutines", "kotlin") // data: OwnedProject here
    println(Json.encodeToString(data)) // Serializing data of compile-time type OwnedProject
}  

Com que el tipus OwnedProject és concret i no és polimòrfic, no es genera un propietat type en l'objecte JSON:

{
    "name": "kotlinx.coroutines",
    "owner": "kotlin"
}

En general, la serialització de Kotlin està dissenyada per funcionar correctament només quan el tipus en temps de compilació utilitzat durant la serialització és el mateix que el tipus en temps de compilació utilitzat durant la deserialització.

Això vol dir que si serialitzo la instància data com OwnerProject l'haig de deserialitza com OwnerProject.

Ja saps que pots especificar el tipus a serialitzar de manera explícita en la funció de serialització:

Json.encodeToString<Project>(data)

D'aquesta manera encara que la variable data sigui de tipus OwnerProject es serialitza de manera polimòrfica com a tipus Project.

Custom subclass serial name

Per defecte, el valor de la propietat type de l'objecte JSON està totalment qualificat: en l'exemple anterior era dev.xtec.data.OwnerProject.

Si vols pots utilitzar l'anotació @SerialName per canviar el seu valor:

@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable         
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

D'aquesta manera pots tenir un nom estable com a discriminador que no es vegi afectat per canvis en el nom de la classe, dels noms dels paquets a la qual pertany o que la canviis de paquet:

{
    "type": "owned",
    "name":"kotlinx.coroutines",
    "owner":"kotlin"
}

Concrete properties in a base class

Una classe base en una jerarquia segellada pot tenir propietats amb camps de suport:

@Serializable
sealed class Project {
    abstract val name: String   
    var status = "open"
}
            
@Serializable   
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val json = Json { encodeDefaults = true } // "status" will be skipped otherwise
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(json.encodeToString(data))
}

Les propietats de la superclasse es serialitzen abans que les propietats de la subclasse:

{
    "type": "owned",
    "status":"open",
    "name":"kotlinx.coroutines",
    "owner":"kotlin"
}

Objects

Les jerarquies segellades poden tenir objectes com a subclasses i també s'han de marcar com a @Serializable.

Utilitzarem un exemple diferent amb una jerarquia de classes Response:

@Serializable
sealed class Response
                      
@Serializable
object EmptyResponse : Response()

@Serializable   
class TextResponse(val text: String) : Response()   

A continuació serialitzem una llista de diferents respostes:

fun main() {
    val list = listOf(EmptyResponse, TextResponse("OK"))
    println(Json.encodeToString(list))
}

Un objecte es serialitza com si fos una classe sense propietats, utilitzant també el seu nom de classe totalment qualificat com a tipus per defecte:

[ { "type": "dev.xtec.data.EmptyResponse" },
  { "type": "dev.xtec.data.TextResponse",
    "text":"OK" }
]

Fins i tot si l'objecte té propietats, aquestes no es serialitzen.

Open polymorphism

La serialització pot funcionar amb classes open o abstract.

El que passa és que llista de subclasses que es serialitzen no es pot determinar en temps de compilació perqué hi ha la possibilitat que les subclasses es defineixin en qualsevol part del codi font, fins i tot en altres mòduls.

Per tant, s'han de registrar explícitament en temps d'execució.

Registered subclasses

Continuem amb les classes Project i OwnedProject, però aquest cop sense "sellar" la classe Project.

El que hem de fer en aquest cas es definir un SerializersModule utilitzant la funció constructora SerializersModule{}.

Dins del mòdul la classe base s'especifica al constructor polymorphic i cada subclasse es registra amb la funció subclass.

D'aquesta manera es pot crear una configuració JSON personalitzada amb aquest mòdul i utilitzar-la per a la serialització:

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

@Serializable
abstract class Project {
    abstract val name: String
}
            
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}    

Aquesta configuració addicional fa que el nostre codi funcioni de la mateixa manera que funcionava amb una classe segellada:

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Tingues en compte que aquest exemple només funciona en una JVM a causa de les rrestriccions de la funció serializer.

Per a JS i Native, s'ha d'utilitzar un serialitzador explícit: format.encodeToString(PolymorphicSerializer(Project::class), data)

Pots seguir aquest problema en aquest enllaç: linl

Serialització d'interfícies

Com que la classe Project no té estat la podem convertir en una interfície, però el que no podem fer és marcar la interficie Project amb l'anotació @Serializable.

Les interfícies s'utilitzen per permetre el polimorfisme i només es poden representar per instàncies de les seves classes derivade, de manera que es considera que totes les interfícies es poden serialitzar implícitament amb l'estrategia PolymorphicSerializer estratègia.

Només has de marcar les seves classes d'implementació com a @Serializable:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

import kotlinx.serialization.modules.*

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

Pots veure que hem declarat la variable data amb tipus Project i podem utlizar la funció format.encodeToString com havies fet fins ara:

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Nota: a Kotlin/Native, has d'utilitzar l'expresió format.encodeToString(PolymorphicSerializer(Project::class), data)) perqué la introsecció és molt limitada.

Property of an interface type

Anem a veure que passa si fem servir la interfície Project com a propietat en alguna altra classe serializable.

Les interfícies són implícitament polimòrfiques, de manera que només podem declarar una propietat d'un tipus d'interfície:

@Serializable
class Data(val project: Project) // Project is an interface

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

Sempre que hàgim registrat el subtipus real de la interfície que s'està serialitzant en el SerializersModule del nostre format, el codi funcionarà en temps d'execució:


{
    "project": {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
}

Static parent type lookup for polyorphism

Durant la serialització d'una classe polimòrfica, el tipus arrel de la jerarquia polimòrfica es determina estàticament.

El tipus arrel de OwnedProject és Project, però anem a veure que passa si la variable data la declarem de tipus Any enlloc de tipus Project:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

import kotlinx.serialization.modules.*

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

@Serializable
abstract class Project {
    abstract val name: String
}
            
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}    

Pots veure que es genera una excepció perquè el serialitzador utilitza el tipus Any enlloc de Project:

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

El problema és que a l'hora de registrar les classes per la serialització aquestes han ser les que es resolen en temps de compilació (tipus estàtic).

En aquest cas has de registrar la classe Any, no la classe Project:

val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(OwnedProject::class)
    }
}

Però com que Any no és una interfície sinò una classe, i no és serializable:

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Has de passar de manera explícita una instància de PolymorphicSerializer per a la classe base Any com el primer paràmetre de la funció encodeToString:

fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(PolymorphicSerializer(Any::class), data))
}

I amb un serialitzador explícit el codi funciona:

{ "type":"owned","name":"kotlinx.coroutines","owner":"kotlin" }

Marcant explícitament les propietats d'una classe polimòrfica

Quan una classe serialitzable té una propietat de tipus interfície, aquesta propietat es considera implícitament polimòrfica ja que les interfícies es refereixen al polimorfisme en temps d'execució.

Però quan la propietat es de tipus classe, i aquesta no es serialitzable, has de proporcionar explícitament l'estrategia de serialització mitjançant l'anotació @Serializable.

Per especificar una estratègia de serialització polimòrfica d'una propietat, has d'utlilitzar l'anotació de propòsit específic @Polymorphic:

@Serializable
class Data(
    @Polymorphic // the code does not compile without it 
    val project: Any 
)

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

Registre de múltiples superclasses

Quan la mateixa classe es serialitza com a valor de propietats amb un tipus de temps de compilació diferent de la llista de les seves superclasses, l'hem de registrar en el SerializersModule per a cadascuna de les seves superclasses per separat.

És convenient extreure el registre de totes les subclasses en una funció separada i utilitzar-lo per a cada superclasse.

Per exemple, pots regstrar Any i Project a la vegada de tal manera que els dos tipus estàtics es puguin utilitzar per serialitzar la classe OwnedProject:

val module = SerializersModule {
    fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
        subclass(OwnedProject::class)
    }
    polymorphic(Any::class) { registerProjectSubclasses() }
    polymorphic(Project::class) { registerProjectSubclasses() }
}        

Pots utilitzar aquesta plantilla per escriure una funció similar.

Polimorfisme i classes genèriques

Els subtipus genèrics per a una classe serializable requereixen un tractament especial.

Considera aquesta jerarquia:

@Serializable
abstract class Response<out T>
            
@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()

La serialització no té una estratègia integrada per representar el tipus d'argument proporcionat realment per a paràmetre de tipus T en serialitzar una propietat de tipus polimòrfic OkResponse<T>.

Tenim que proporcionar un estratègia explícita quan es defineix el mòdul de serialitzadors per a Response:

val responseModule = SerializersModule {
    polymorphic(Response::class) {
        subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
    }
}

Pots veure que fem servir la funció OkResponse.serializer(...) per recuperar el serialitzador genèric generat pel plugin per a la classe OkResponse i l'instanciem amb la instància PolymorphicSerializer i la clase Any com a base.

D'aquesta manera, podem serialitzar una instància de OkResponse amb qualsevol propietat data que s'hagin registrar polimòrficament com a subtipus de Any.

Fusionar mòduls de serialitzadors de biblioteques

A medida que una aplicació creix aquesta es va dividint en mòduls.

En aquest casos, és millor tenir mòduls de serialització separats enlloc de tenir un únic mòdul amb totes les jerarquies de classes.

Si vols utilitzar les classes OwnedProject i OkResponse a la vegada, pots compondre el dos mòduls de serialització que has implementat abans amb l'operador plus de tal manera que els podem utilitzar tots dos en la mateixa instància de format Json.

val format = Json { serializersModule = projectModule + responseModule }

També pots utilitzar la funció include en el DSL SerializersModule{}.

D'aquesta manera les classes de les dos jerarquies es poden serialitzar i deserialitzar juntes.

fun main() {
    // both Response and Project are abstract and their concrete subtypes are being serialized
    val data: Response<Project> =  OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
    val string = format.encodeToString(data)
    println(string)
    println(format.decodeFromString<Response<Project>>(string))
}

Pots veure que la representació JSON és completament polimòrfica:

{"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}}
OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))

Si estas escrivint una biblioteca o un mòdul compartit amb una classe abstracta i algunes implementacions d'aquesta, pots exposar el teu propi mòdul de serialitzadors perquè els utilitzin els teus clients.

D'aquesta manera un client pot combinar el teu mòdul amb els seus mòduls.

Controlador de tipus polimòrfic predeterminat per a la deserialització

Què passa si deserialitzes una subclasse que no estava registrada?

fun main() {
    println(format.decodeFromString<Project>("""
        {"type":"unknown","name":"example"}
    """))
}

Com que el type és "unkown" és produeix aquesta excepció:

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'unknown' is not found in the polymorphic scope of 'Project' at path: $
Check if class with serial name 'unknown' exists and serializer is registered in a corresponding SerializersModule.

En molts casos no tenim control sobre les dades que processem, i enlloc de capturar l'exepció i ignorar la dada "erronea" potser volem processar-la de totes formes.

Per exemple, podem tenir un subtipus BasicProject per representar tots aquell projectes amb un valor type que no tenen una classe corresponent:

@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
data class BasicProject(override val name: String, val type: String): Project()

@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()

Pots veure que la classe BasicProject té una propietat type per guardar el valor type del JSON.

A continuació has de registrar un gestor de deserialitzador predeterminat mitjançant la funció defaultDeserializer en el DLS polymorphic { ... } que defineix una estratègia que mapeja l'srting type del JSON a l' estratègia de deserialització . En el codi que es mostra a continuació no utilitzem el valor type perquè només utilitzes una única classe per defecte, i et limites a retorar el serialitzador generat pel plugin de la classe BasicProject:

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
        defaultDeserializer { BasicProject.serializer() }
    }
}

Mitjançant aquest mòdul ara podem deserialitzar una instància de OwnerProject i una altre d'un tipus no registrat:

val format = Json { serializersModule = module }

fun main() {
    println(format.decodeFromString<List<Project>>("""
        [
            {"type":"unknown","name":"example"},
            {"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"} 
        ]
    """))
}

Fixa't com la propietat type de la instància de BasicProject té el valor "unknown":

[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]

En aquest cas has utilitzat un serialitzador generat pel plugin com a serialitzador predeterminat, cosa que implica que l'estructura de les dades "unknown" es coneix per endavant.

En una API del món real, rarament és així. Per a això es necessita un serialitzador personalitzat i menys estructurat.

TODO Veureu l'exemple d'aquest serialitzador a la secció futura sobre el manteniment d'atributs JSON personalitzats .

Controlador de tipus polimòrfic predeterminat per a la serialització

De vegades, cal triar de forma dinàmica quin serialitzador utilitzar per a un tipus polimòrfic en funció de la instància.

Per exemple, si no tens accés a la jerarquia de tipus completa o aquesta canvia molt.

Per a aquesta situació, podts registrar un serialitzador predeterminat.

interface Animal {
}

interface Cat : Animal {
    val catType: String
}

interface Dog : Animal {
    val dogType: String
}

private class CatImpl : Cat {
    override val catType: String = "Tabby"
}

private class DogImpl : Dog {
    override val dogType: String = "Husky"
}

object AnimalProvider {
    fun createCat(): Cat = CatImpl()
    fun createDog(): Dog = DogImpl()
}

Registrem un controlador de serialitzador predeterminat mitjançant la funció polymorphicDefaultSerializer en el DLS SerializersModule { ... } que defineix una estratègia que pren una instància de la classe base i proporciona una estratègia de serialització.

En l'exemple que es mostra a continuació, fem servir un bloc when per comprovar el tipus de l'instància sense fer referència en cap cas a les classes d'implementació privades.

val module = SerializersModule {
    polymorphicDefaultSerializer(Animal::class) { instance ->
        @Suppress("UNCHECKED_CAST")
        when (instance) {
            is Cat -> CatSerializer as SerializationStrategy<Animal>
            is Dog -> DogSerializer as SerializationStrategy<Animal>
            else -> null
        }
    }
}

object CatSerializer : SerializationStrategy<Cat> {
    override val descriptor = buildClassSerialDescriptor("Cat") {
        element<String>("catType")
    }
  
    override fun serialize(encoder: Encoder, value: Cat) {
        encoder.encodeStructure(descriptor) {
          encodeStringElement(descriptor, 0, value.catType)
        }
    }
}

object DogSerializer : SerializationStrategy<Dog> {
  override val descriptor = buildClassSerialDescriptor("Dog") {
    element<String>("dogType")
  }

  override fun serialize(encoder: Encoder, value: Dog) {
    encoder.encodeStructure(descriptor) {
      encodeStringElement(descriptor, 0, value.dogType)
    }
  }
}

Amb aquest mòdul ara podem serialitzar instàncies de Cat i Dog:

val format = Json { serializersModule = module }

fun main() {
    println(format.encodeToString<Animal>(AnimalProvider.createCat()))
}
{"type":"Cat","catType":"Tabby"}