La serialització consiteix en convertir un arbre d'objectes en un string o en una seqüència de bytes.

Introducció

La serialització de dades no és tant fàcil com pot semblar al principi, perquè la majoria de les dades que consumim o produim no s'ajusten exactament a la definició de les nostres classes.

Entorn de treball

La biblioteca "kotlinx.serialization" és també un plugin del compilador.

Instal.la la IDE Idea

Crea un nou projecte:

Edita el fitxer build.gradle.kts:

1.- Modifica la secció plugins del fitxer :

plugins {
    kotlin("jvm") version "2.0.0" // or kotlin("multiplatform") or any other kotlin plugin
    kotlin("plugin.serialization") version "2.0.0"
}

El plugin de serialització que ha de ternir la mateixa versió que el compilador.

2.- Afegeix la de dependència de la biblioteca JSON.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}

Serialització

La serialització consiteix en convertir un arbre d'objectes en un string o en una seqüència de bytes.

És un procés en dos pasos, perqué un mateix objecte s'ha de poder converir a formats diferents:

  1. Primer l'objecte s'ha de descomposar en un conjunt de valor primitius mitjançant un serialitzador.

  2. A continuació el conjunt de valors primitius es codifiquen a un format concret mitjançant un codificador.

Per exemple, si tenim un objecte de la classe Person:

class Person(val name: String, val address: Address)
class Address(val street: String, val city: String)

val person = Person("david", Address("Diagonal 322", "Barcelona")

La classe Person es descomposa en valor primitius anidats:

(name: String, address: (street: String, city: String))

I es pot respresentar en XML:

<person>
    <name>David</name>
    <address>
        <street>Diagonal 322</street>
        <city>Barcelona</city>
    </address>
</person>

En JSON:

{ 
    "name": "dev.xtec.data", 
    "address: {
        "street": "Diagonal 322",
        "city": "Barcelona"
    }
}

O en altres formats, inclosos formata binaris.

Activitat. Donat aquest diagrama UML crea les classes corresponents, una instància de l'element X, descomposa la classe en un conjunt de valors primitiuus anidats, i codifica l'objecte en XML i JSON.

TODO: diagrama

TODO: solució

JSON

JSON és el format més utilitzat per intercanvi de dades a Internet, i és el que utilitzarem en aquestes activitats.

Perqué la teva aplicació es pugui comunicar amb altres serveis has de convertir els teus objectes a JSON, i consumir dades en format JSON.

Encoding

Per convertir un objecte a format JSON has d'utilitzar la funció d'extensió Json.encodeToString, que serialitza l'objecte que es passa com a paràmetre i el codifica en un string JSON.

Comencem amb una classe que descriu un projecte i intentem obtenir la seva representació JSON.

package dev.xtec.data

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

data class Project(val name: String, val language: String)

fun main () {
    val project = Project("Data","Kotlin")
    println(Json.encodeToString(project))
}

Quan executem aquest codi es produeix una excepció perquè les classes serializables s'han de marcar explícitament.

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

La serialització de Kotlin no fa servir la reflexió de Java en temps d'execució perque és un procés lent, i més important encara, és un llenguatge multiplataforma que en molts casos no s'executa en una JVM.

Has d'afegir l'anotació @Serializable tal com ens indica el missatge d'error:

package dev.xtec.data

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

@Serializable
data class Project(val name: String, val language: String)

fun main () {
    val project = Project("Data","Kotlin")
    println(Json.encodeToString(project))
}

L'anotació @Serializable indica al plugin de serialització que ha de generar un serialitzador específic per aquesta classe quan es compila el programa.

Ara si executes el codi tens una representació JSON del projecte:

{"name":"Data","language":"Kotlin"}

Fixa't que en cap cas es guarda informació de la classe, en aquest cast dev.xtec.data.Project.

Decoding

Pots descodificar un string JSON en un objecte utilitzant la funció d'extensió Json.decodeFromString.

La funció no sap quina classe ha d'utilitzar per crear una instància a partir del contingut JSON perqué, com hem explicat abans, l'string JSON no té aquesta informació, i tampoc s'espera que tingui aquesta informació.

Per tant, la funció espera un paràmere de tipus per saber a quina classe ha de descodificar:

@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val data = """
        {"name":"dev.xtec.data","language":"Kotlin"}
    """
    val project = Json.decodeFromString<Project>(data)
    println(project)
}

Si executes aquest codi pots verificar que a partir d'un string pots crear un objecte Project amb el contingut de l'string.

I molt important, la funció només espera una classe "serialitzable" que tingui una estructura a la qual pugui descodificar bé perqué:

  1. Té una estructura identica a Project.
  2. Tal com veurem més endavant, l'anotem de tal manera que pot utilitzar aquestes dades.

A continuació tens un exemple d'una classe que no és Project i "consumeix" les mateixes dades:

@Serializable
data class Person(val name: String, val language: String)

fun main() {
    val data = """
        {"name":"dev.xtec.data","language":"Kotlin"}
    """
    val person = Json.decodeFromString<Person>(data)
    println(person)
}

És cert que aquestes dades tindrien més sentit per un objecte Person:

{"name":"David de Mingo","language":"Catalan"}

Però també saps que les podries descodificar com Project sense cap problema.

El que unes dades tingui sentit és problema teu, no del compilador.

Propietats

Per defecte,

  1. Només es serialitzen les propietats d'una classe que tenen un atribut de suport.

  2. Les propietats amb un getter/setter que no tenen un "backing field" no es serialitzen.

  3. Les propietats delegades no es serialitzen.

A continuació tens un exemple:

@Serializable
class Project(
    // name is a property with backing field -- serialized
    var name: String
) {
    var stars: Int = 0 // property with a backing field -- serialized

    val path: String // getter only, no backing field -- not serialized
        get() = "kotlin/$name"

    var id by ::name // delegated property -- not serialized
}

fun main() {
    val data = Project("dev.xtec.data").apply { stars = 9000 }
    println(Json.encodeToString(data))
}

Si executs el codi pots veure que només les propietats name i starts estan presents a la sortida JSON:

{"name":"dev.xtec.data","stars":9000}

Nom de la propietat

Per defecte, el procés de serialització utilitza els noms de les propietats dels objectes per codificar o descodificar dades.

Però molt cops aquests noms no es poden fer servir perquè les dades codificades són per enviar a un altre servei que utilitza noms diferents o al revés.

I molt important! Els serveis de tercers no han de dictar els noms que hem de fer servir.

Amb l'anotació @SerialName podem indicar al procés de serialització que faci servir un altre nom enlloc del nom de la propietat.

Per exemple, enlloc de language potser has d'utlitzar el nom de lang per compatibilitat:

@Serializable
class Project(val name: String, @SerialName("lang") val language: String)

fun main() {
    val data = Project("dev.xtec.data", "Kotlin")
    println(Json.encodeToString(data))
}

Pots veure que ara el procés de serialitzado codifica el valor de language amb el nom de lang:

{ "name": "dev.xtec.cat", "lang": "Kotlin" }

Propietats transitòries

Una propietat es pot excloure de la serialització marcant-la amb l'anotació @Transient.

Com que aquesta propietat no es deserialitza, ha de tenir un valor predeterminat perquè el seu valor no es pot obtenir de les dades JSON.

@Serializable
data class Project(val name: String, @Transient val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

Si executes aquest codi el resultat serà aquest:

Project(name=kotlinx.serialization, language=Kotlin)

Una propietat @Transient no és llegeix de les dades JSON, però és que tampoc es permet la seva presència en les dades JSON encara que el seu valor sigui el valor per defecte de la propietat.

@Serializable
data class Project(val name: String, @Transient val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

Pots veure que les dades tenen un valor per la propietat language, que és identic al valor per defecte, i es produeix un error:

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $.name
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.

Valors per defecte

En principi, un objecte només es pot deserialitzar quan totes les seves propietats estan presents en l'string que s'ha de decodificar.

Per exemple, si executes aquest codi:


@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

Es produeix una excepció:

Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing at path: $

Kotlin permet propietats amb valor per defecte al crear un objecte en presència de valors nuls.

Aquest valor per defecte s'aplica tant quan és contrueix una instància amb codi com quan es deserialitza un objecte JSON.

@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

Com que hem afegt un valor per defecte a la propietat language, el nostre codi decodifica l'objecte JSON sense que es produeixi cap error, i la propietat language té el valor per defecte "Kotlin":

Project(name=kotlinx.serialization, language=Kotlin)

Valor per defecte obligatori

Com hem explicat abans, Kotlin permet propietats amb valors per defecte, i això també s'aplica al procés de serialització.

Però que passa si només volem valors per defecte quan el nostre codi crea objectes, no pas quan es deserialitza un objecte?

Si volem que al decodificar l'objecte JSON ha de tenir el valor per defete si o si, pots utilitzar l'anotació @Required:

@Serializable
data class Project(val name: String, @Required val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

Ara durant l'execució del codi es produeix un error:

Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing at path: $

Codificar els valors per defecte

En la configuració per defecte del format de codificació json, els valors per defecte no es codififiquen per estalviar espai:

@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Project("kotlinx.serialization")
    println(Json.encodeToString(data))
}

Pots veure que l'objecte JSON no té la propietat language perqué el seu valor és igual al predeterminat:

{"name":"kotlinx.serialization"}

Si vols codificar els valors per defecte:

  1. Pots modificar la configuració del format de cofificació tal com s'explica a TODO (enllaç JSON)

  2. Utilitzar l'anotació EncodeDefault:

@Serializable
data class Project(
    val name: String,
    @EncodeDefault val language: String = "Kotlin"
)

Aquesta anotació indica al framework que sempre ha de serialitzar la propietat, independentment del seu valor o la configuració del format de codificació.

Si has mofificat la configuració del format de codificació, pots configurar el comportament contrari mitjançant el paràmetre EncodeDefault.Mode:

@Serializable
data class User(
    val name: String,
    @EncodeDefault(EncodeDefault.Mode.NEVER) val projects: List<Project> = emptyList()
)

fun main() {
    val alice = User("Alice", listOf(Project("kotlinx.serialization")))
    val bob = User("Bob")
    println(Json.encodeToString(alice))
    println(Json.encodeToString(bob))
}

Com pots veure, la propietat language es conserva i projects s'omet:

{"name":"Alice","projects":[{"name":"kotlinx.serialization","language":"Kotlin"}]}
{"name":"Bob"}

Propietats anul·lables

Kotlin és un lleguatge segur perquè no permet valor nuls en les propietats d'un objecte a no ser que de manera explícita indiquis que un propietat pot tenir un valor nul.

Tal com hem vist abans, si intentes descodificar un valor null d'un objecte JSON en una propietat que no admet valors nuls:

@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","language":null}
    """)
    println(data)
}

Es produeix un error encara que la propietat language tingui un valor predeterminat:

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language
Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value.

Si vols que una propietat admeti valor nuls la pots marcar com anul.lable amb ?.

Les propietats anul·lables són compatibles de manera nativa amb la serialització de Kotlin.

A continuació tens un exemple amb la propietat renamedTo:

@Serializable
class Project(val name: String, val renamedTo: String? = null)

fun main() {
    val data = Project("kotlinx.serialization")
    println(Json.encodeToString(data))
}
```

Aquest exemple no codifica la propietat `renamedTo` amb el valor `null` perqué per defecte el valors nuls no es codifiquen:

```sh
{"name":"kotlinx.serialization"}
```

## Constructor

En Kotlin només pot existir un constructor primari i la validació de les dades es fa en un bloc específic.

### Validació de dades

Si vols validar les dades durant el procés de deserialització has de crear un block `init { ... }` on s'executarà la validació:

```kt
@Serializable
class Project(val name: String) {
    init {
        require(name.isNotEmpty()) { "name cannot be empty" }
    }
}
```

El procés de deserialització funciona com un constructor normal i executa tots els blocs `init`, de tal manera que no es pot instanciar una classe que no sigui valida.

Anem a provar-ho:

```kt
fun main() {
    val data = Json.decodeFromString<Project>("""
        {"name":""}
    """)
    println(data)
}
```

L'execució d'aquest codi produeix una excepció:

```sh
Exception in thread "main" java.lang.IllegalArgumentException: name cannot be empty
```

### Propietats del constructor

Encara que Kotlin és molt estricte respecte com es construeix un objecte i això facilita el procés de serialització, el plugin se serialització encara és més estricte.

Si anotates una classe `@Serializable` tots els paràmetres del constructor primari de la classe han de ser propietats.

Normalment això no és un problema.

Però que passa si vols definir la classe `Project` de tal manera que el constructor requereixi un paràmetre `path` de tipus String, i que després el descontrueixi amb les propietats corresponents: 

```kt
@Serializable
class Project(path: String) {
    val owner: String = path.substringBefore('/')
    val name: String = path.substringAfter('/')
}

fun main() {
    println(Json.encodeToString(Project("xtec/data")))
}
```

Es produeix un error com era de preveure:

```
TODO error
``` 

En aquest casos has de:

1. Definir un constructor primari privat amb les propietats de la classe.

2. Convertir el constructor que volies en secundari.

```kt
@Serializable
class Project private constructor(val owner: String, val name: String) {
    constructor(path: String) : this(
        owner = path.substringBefore('/'),
        name = path.substringAfter('/')
    )

    val path: String
        get() = "$owner/$name"
}

fun main() {
    println(Json.encodeToString(Project("xtec/data")))
}
```

Ara el compilador no dona error, i si executes el codi la sortida és l'esperada:

```
{"owner":"xtec","name":"data"}
```

## Referències

Fins ara només hem treballat amb objectes en que les totes les seves propietats són valors primaris.

Però molts objectes tenen propietats que són objectes complexes.

Si el tipus de la propietat és una classe amb l'anotació `@Serializable` no hi ha cap problema.

```kt
@Serializable
class Project(val name: String, val owner: User)

@Serializable
class User(val name: String)

fun main() {
    val owner = User("kotlin")
    val data = Project("kotlinx.serialization", owner)
    println(Json.encodeToString(data))
}
```

Quan es codifica en JSON, el resultat és un objecte JSON imbricat.

```sh
{"name":"kotlinx.serialization","owner":{"name":"kotlin"}}

Però que passa si la classe no està marcada amb l'anotació `@Serializable?

Si la propietat és transitoria, està marcada amb l'anotació @Transient tot funciona correctament perquè la propietat s'ignora.

En els altres casos, casi tots, has d'indicar de manera específica el serialitzador que s'ha de fer servir per la propietat concreta tal com s'explica a TODO(link serailizers).

Referències repetides

Un objecte pot fer referència al mateix objecte de manera directa o indirecta.

En aquest exemple, la propietat owner i mantainer fan referència al la mateixa instància:

@Serializable
class Project(val name: String, val owner: User, val maintainer: User)

@Serializable
class User(val name: String)

fun main() {
    val david = User("david")
    val data = Project("dev.xtec.data", david, david)
    println(Json.encodeToString(data))
}

I la pregunta, la serialització com ho fa?

Doncs el procés de serialització els considera objectes diferents, perqué la majoria dels formats no codifiquen "referències":

{ 
  "name": "dev.xtec.data",
  "owner": { "name":"david" },
  "maintainer":{"name":"david"}
}

És l'aplicació, això vol dir el codi que tu has d'escriure, el que ha de resoldre el problema d'objectes duplicats i referències erronees quan deserialitzes dades.

Referències circulars

TODO exemples i gràfic

La serialització només funciona amb arbres: és comença amb un objecte i es van processant de manera recursiva totes les propietats serialitzables que són referències fins que no queda cap objecte referenciat per processar.

Però que passa si hi ha una referència circular?

Doncs que la funció recursiva de serialització es processarà fins que es produeix un "stack overflow".

Propietat genèrica

Un classe genèrica pot tenir propietats genèriques.

Per exemple, considera una classe serialitzable genèrica Box<T>:

@Serializable
class Box<T>(val contents: T)

La classe Box<T> es pot utilitzar:

  1. Amb tipus predefinits com Int
  2. Amb tipus que has definit tu mateix, com és Project:
@Serializable
class Data(
    val a: Box<Int>,
    val b: Box<Project>
)

fun main() {
    val data = Data(Box(42), Box(Project("dev.xtec.data", "Kotlin")))
    println(Json.encodeToString(data))
}

Pots veure que el procés de serialització no té problemes amb un paràmetre genèric sempre que el seu tipus sigui serialitzable:

{
  "a": {"contents":42},
  "b": {"contents":{
    "name":"dev.xtec.data",
    "language":"Kotlin"}}}

En canvi, si el tipus real no es pot serialitzar, es produirà un error en temps de compilació.

TODO exemple