Tots el tipus primitius i algunes classes de la llibreria estàndard de Kotlin es serialitzen per defecte.

Introducció

A continuació anem a veure tots els tipus que es serialitzen per defecte.

Primitives

La serialització de Kotlin té aquest deu "primitives": Boolean, Byte, Short, Int, Long, Float, Double, Char, String, i enumeracions.

Els tipus Boolean, Char i String es poden utilitzar sense cap tipus de consideració al respecte, però els altres necessiten algunes aclaracions.

Números

Kotlin té 4 tipus per representar números enters i 2 tipus per representar números en coma flotat, i com hem dit abans, tots són serialitzables.

@Serializable
class Data(
    val answer: Int,
    val pi: Double
)                     

fun main() {
    val data = Data(42, PI)
    println(Json.encodeToString(data))
}

Pots veure que aquests números es codifiquen sense problemes en JSON:

{ 
  "answer": 42,
  "pi": 3.141592653589793
}

Codificar un Long

Hi ha llenguatges de programació que tenen més tipus, i altres menys, però el que ens interessa en el procés de serialització és el tipus que pot representar el format al qual codifiquem les dades.

JSON utilitza els tipus de Javascript, i Javascritpt només té el tipus number que equival a un Double.

Ja saps que qualsevol valor de tipus Byte, Short, Int o Float es pot representar amb un Double, peró no tots els valors de tipus Long es poden representar amb un Double.

Per defecte, un Long amb un valor molt gran es codifica tal qual:

TODO els dos literals coincidents

@Serializable
class Data(val signature: Long)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
}

Pot veure que signature es codifica com un número encara que un number (o un Double) no pugui representar aquest número:

{ "signature": 2067120338512882656 }

Això no representa cap problema sempre i quant aquest número no el decodifiquin mètodes natius Javascript (Kotlin/JS no té problemes).

En aquests casos, pots anotar una propietat amb LongAsStringSerializer per representar el número amb un string i no perdre precisió.

@Serializable
class Data(
    @Serializable(with=LongAsStringSerializer::class)
    val signature: Long
)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
}

Pots veure que ara el valor de signature no és un number sinó un string:

{ "signature": "2067120338512882656" }

Descodificar números

El motiu d'utilizar tipus com Byte, Short, Int o Float en processadors de 64 bits es estalviar espai en memòria principal o secundària.

Encara que un valor tingui un tipus Byte aquest valor és processa en la CPU igual que si fos de tipus Long: el temps de processament és el mateix.

I en dades d'intercanvi, sobretot quan has de descodificar dades, utilitzar aquests tipus no aporta cap benefici i molts problemes.

El dos únics tipus que has d'utlitzar per llegir dades JSON són Int i Float.

TODO: demostra el problema , classe amb short, etc. i ...

Enum classes

En principi, totes les classes d'enumeració es poden serialitzar sense haver de marcar-les amb l'anotació @Serializable.

A continuació tens un exemple:

enum class Status { CLOSED, SUPPORTED }
        
@Serializable
class Project(val name: String, val status: Status) 

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

En JSON, una enumeració es codifica amb un string amb el nom del discriminador de l'enum:

{ "name": "dev.xtec.data", "status": "SUPPORTED" }

Però hi ha dues exepcions!

1.- A Kotlin/JS i Kotlin/Native has de marcar la classe enum amb l'anotació @Serializable si la vols utilitzar com a objecte arrel en el procés de serialització.

encodeToString<Status>(Status.SUPPORTED).

2.- Vols canviar el nom que es fa sevir pel discriminador de l'enum quan es serialitza amb l'anotació `@SerialName.

En aquest cas també has d'anotar la classe amb @Serializable:

@Serializable
enum class Status { CLOSED, @SerialName("maintained") SUPPORTED }
        
@Serializable
class Project(val name: String, val status: Status) 

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

Pots veure que ara el valor de status és "maintained" enlloc de "SUPPORTED":

{ "name":"dev.xtec.data", "status": "maintained" }

Objectes compostos

En Kotlin, els tipus primaris boolean, byte, etc. són objectes simples.

Els objectes compostos estan formats per objectes simple o compostos de manera recursiva definits per la classe corresponent.

Nomes les classes de la biblioteca estàndard de Kotlin que veurem a continuació es poden serialitzar per defecte.

Pair i triple

Les classes de dades simples Pair i Triple de la biblioteca estàndard de Kotlin són serialitzables.

@Serializable
class Project(val name: String)

fun main() {
    val pair = 1 to Project("dev.xtec.data")
    println(Json.encodeToString(pair))
}  

Pots veure que un objecte de tipus Pair es serialitza com un objecte json amb dos propietats: first i second.

{ "first": 1, "second": { "name":"dev.xtec.data"} }

Col.leccions

Totes les col.leccions en que els seus elements es poden serialitzar es poden serialitzar.

Codificar

Totes les col.leccions, execepte Map, es representen amb un "array" JSON.

Per exemple, una List en que tots els seus elements són serialitzables es pot serialitzar.


@Serializable
class Project(val name: String)

fun main() {
    val list = listOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(list))
}  

Tal com hem explicat al principi, la llista es representa com un array JSON:

[{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]

Pots veure que un Set també es poden serialitzar:

@Serializable
class Project(val name: String)

fun main() {
    val set = setOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(set))
}

I el Set també es representa com un array JSON:

[{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]

Decodificar

JSON només té el tipus array i totes les col.leccions es codifiquen com arrays.

Per tant, al decodificar un string JSON el tipus de la col.leció el determina el tipus de la propietat de la classe o el paràmetre de tipus de la funció de descodificació (TODO posar exemple).

A continuació tens un exemple en que es mostra com el mateix array JSON es deserialitza en dues propietats amb tipus diferent:

@Serializable
data class Data(
    val a: List<Int>,
    val b: Set<Int>
)
     
fun main() {
    val data = Json.decodeFromString<Data>("""
        {
            "a": [42, 42],
            "b": [42, 42]
        }
    """)
    println(data)
}

Com que la propietat data.b és un Set, els valors duplicats no apareixen:

Data(a=[42, 42], b=[42])

Map

Un Map amb claus primitives o enumeracions, i valors serialitzables es pot serialitzar

@Serializable
class Project(val name: String)

fun main() {
    val map = mapOf(
        1 to Project("kotlinx.serialization"),
        2 to Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(map))
}

A diferència del reste de col.leccions, un Map es codifica com un objecte JSON (un objecte JSON equival a un map).

L'única diferència és que les claus sempre es codifiquen en string tal com pots veure en el resultat:

{
  "1": { "name": "kotlinx.serialization" },
  "2": { "name": "kotlinx.coroutines" }
}

Duration

La classe Duration també és serialitzable:

fun main() {
    val duration = 1000.toDuration(DurationUnit.SECONDS)
    println(Json.encodeToString(duration))
}

La durada es serialitza com una string en el format ISO-8601-2:

"PT16M40S"

Objectes Unit i singletons

El tipus Unit també es serialitzable.

Unit és un singleton object i es gestiona igual que els altres objectes.

Conceptualment, un singleton és una classe amb només una instància, el que significa que l'estat no defineix l'objecte, però l'objecte defineix el seu estat.

A JSON, els objectes es serialitzen com a estructures buides:

@Serializable
object SerializationVersion {
    val libraryVersion: String = "1.0.0"
}

fun main() {
    println(Json.encodeToString(SerializationVersion))
    println(Json.encodeToString(Unit))
}

Tot i que pot semblar inútil a primera vista, això és útil per a la serialització de classes "sealed" com explicarem a TODO Polymorphism.Objects:

{}
{}

Nothing

Per defecte, Nothing és una classe serialitzable.

Tanmateix, com que no hi ha instàncies d'aquesta classe, és impossible codificar o descodificar els seus valors; qualsevol intent provocarà una excepció.

Aquest serialitzador s'utilitza quan sintàcticament es necessita algun tipus, però en realitat no s'utilitza en la serialització.

Per exemple, quan s'utilitzen classes base polimòrfiques parametritzades:

@Serializable
sealed class ParametrizedParent<out R> {
    @Serializable
    data class ChildWithoutParameter(val value: Int) : ParametrizedParent<Nothing>()
}

fun main() {
    println(Json.encodeToString(ParametrizedParent.ChildWithoutParameter(42)))
}
```

Quan es codifica, el serialitzador per `Nothing` no s'ha utilitzat:

```json
{ "value": 42 }
```