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"}