La serialització ens permet transformar unes dades en un "string".
Introducció
Entorn de treball
Crea la carpeta serial
:
> md serial
> cd serial
Crea el projecte file
amb Gradle:
gradle init --package serial --project-name serial --java-version 21 --type kotlin-application --dsl kotlin --test-framework kotlintest --no-split-project --no-incubating --overwrite
Construeix el projecte i obre'l amb Idea:
> .\gradlew build
> idea .
Gradle
Per poder serialitzar dades necessites modificar el fitxer app/build.gradle.kts
.
Tens un exemple a https://gitlab.com/xtec/kotlin/file/-/blob/main/app/build.gradle.kts?ref_type=heads
Has d'afegir el plugin de serialització perquè gradle generi de manera automàtica el codi corresponent:
plugins {
...
kotlin("plugin.serialization") version "2.0.0"
}
També has d'afegir una dependència de la biblioteca kotlinx-serialization-json
per poder serialitzar les dades a format JSON (veure JSON - Objecte.
dependencies {
...
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}
Has d'indicar a Idea que carregui els canvis que s'han fet:
Serialitzar
En la nostra aplicació la informació està en Data.
Per guardar una dada primer l'has de convertir en un String
.
La manera més habitual és convertir una dada a JSON - Objecte
Has d'anotar la data class
com @Serializable
perqué el "plugin" de serialitzador del compilador generi el mètode serializer()
.
El mètode serializer()
descomposa l'objecte en un conjunt de valor primitius:
package serial
import kotlinx.serialization.*
@Serializable
data class Person(val name: String, val married: Boolean)
fun main() {
println(Person.serializer().descriptor)
}
Executa l'aplicació:
> gradlew run
serial.Person(name: kotlin.String, married: kotlin.Boolean)
La propietat descriptor
del serialitzador mostra que la classe file.Person
està formada pels elements primitius name
de tipus kotlin.String
i married
de tipus kotlin.Boolean
.
Si mires el fitxer Person.class
que s'ha generat dins de la carpeta build
pots veure que el "plugin" serialitzation ha generat el codi de Person.serializer()
de manera automàtica:
A continuació, codifiquem el conjunt de valors primitius en un format concret mitjançant un codificador.
En el nostre cas, utilitzem un codificador JSON:
package serial
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Person(val name: String, val married: Boolean)
fun main() {
val mary = Person("Mary",true)
val json = Json.encodeToString(mary)
println(mary)
println(json)
}
Person(name=Mary, married=true)
{"name":"Mary","married":true}
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.
Fixa't que en la representació JSON no es guarda informació de la classe, en aquest cast serial.Person
.
Activitat
Tenim aquest diagrama de dades:
classDiagram direction LR class Person { id: Long givenName: String familyName: String /name: String } class Address { street: String city: String } Person --> Address
1.- Crea les data class
coresponents.
Recorda que Address
també s'ha de marcar com @Serializable
.
2.- Crea algunes dades demo
✍
3.- Serialitza les dades demo a format Json:
✍
Referències
Quan serialitzem dades no estem guardant dades en una base de dades.
Referències repetides
No hi ha referències, hi ha redundància
Que vol dir que pot haver dades repetides perquè un objecte pot fer referència al mateix objecte de manera directa o indirecta.
Per exemple, en un projecte l'owner
i el mantainer
són del tipus User
:
@Serializable
class Project(val name: String, val owner: User, val maintainer: User)
@Serializable
class User(val name: String)
I poden ser la mateixa persona:
@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("Quantum System", david, david)
println(Json.encodeToString(data))
}
flowchart LR project["Quantum System"] david["👽 David"] project -- owner --> david project -- mantainer --> david
El procés de serialització no té en compte que són referències al mateix objecte:
{
"name": "Quantum System",
"owner": { "name":"david" },
"maintainer":{"name":"david"}
}
És per aquest motiu que les bases de dades documentals com MongoDB tenen dades redundants.
Referències circulars
Com ja hem explicat abans, la serialització no té en compte si un objecte ja s'havia serialitzat.
Això vol dir que a més de la redundància, la serialització només funciona amb arbres: un objecte arrel, ramificacions i tota acaba en fulles.
flowchart TB boeing["✈ Boeing 747"] pilot["🤠 Peter"] boeing -- pilot --> pilot passengers["[🤗 Julia, 🫣 Mary, 🙄 Tom, 😮💨 Jane, 😮 Albert, 🤢 Josephine, 🥴 Clara, 😵💫 Mike"] boeing -- passengers --> passengers
Es 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 exemple, si David
té una referència directa o indirecta amb Esther
, i Esther
té una referència directa o indirecta amb David
, la recursió no acabarà mai 💫💫💫💫💫 ... a no ser que 🙀🙀 ??
flowchart LR david["😺 David"] esther["🦝 Esther"] david --> esther esther --> david
Vam explicar a Data que quan representem dades totes les propietats són val
.
D'aquesta manera és impossible crear referències circulars !!
Pots veure que en aquest exemple, la sentència esther.partner = david
no compila:
@Serializable
class Person(val name: String, val partner: Person? = null)
fun main() {
val esther = Person("Esther")
val david = Person("David", esther)
esther.partner = david
}
El disseny correcte és aquest:
@Serializable
class Person(val name: String)
@Serializable
class Partners(val a: Person, val b: Person)
fun main() {
val esther = Person("Esther")
val david = Person("David")
println(Json.encodeToString(Partners(esther, david)))
{"a":{"name":"Esther"},"b":{"name":"David"}}
}
classDiagram direction LR class Partners {} class Person { name: String } Partners --> Person: a Partners --> Person: b
Això mai!
El que no has de fer mai quan representes dades, és utilitzar un var
com es mostra a continuació:
@Serializable
class Person(val name: String, var partner: Person? = null)
fun main() {
val esther = Person("Esther")
val david = Person("David",esther)
esther.partner = david
println(Json.encodeToString(david))
}
Quan executes aquest codi ...
> gradle run
Exception in thread "main" java.lang.StackOverflowError
at kotlinx.serialization.json.internal.WriteModeKt.switchMode(WriteMode.kt)
...
La serialització no acaba mai ...
El que acaba és el programa amb un "Stack Overflow Error".
Deserialitzar
Per convertir un string JSON en un objecte has d'utlitzar la funció d'extensió Json.decodeFromString.
Aquesta funció, a més de l'String
, espera un paràmere de tipus per saber a quina classe ha de descodificar perquè l'string JSON no té aquesta informació,
Tens dos opcions:
// Passar el parametre de tipus a la funció
val project1 = Json.decodeFromString<Project>(data)
// Que la variable tingui un tipus explícit
val project2: Project = Json.decodeFromString(data)
A continuació tens un exemple:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val data = """{"name":"Quantum System","language":"Borg"}"""
val project = Json.decodeFromString<Project>(data)
require(project == Project("Quantum System", "Borg"))
}
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 perqué té una estructura identica a Project
.
A continuació tens un exemple d'una classe que no és Project
i "consumeix" les mateixes dades:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Person(val name: String, val language: String)
fun main() {
val data = """{"name":"Raquel Durán","language":"Català"}"""
val raquel = Json.decodeFromString<Person>(data)
require(raquel == Person("Raquel Durán", "Català"))
}
El motiu és serialitzes dades per comunicar-te amb altres aplicacions i serveis, i cada aplicació fa servir el nom de la classe que més li convé:
package file
import kotlinx.serialization.*
import kotlinx.serialization.json.*
fun main() {
@Serializable
data class Kangaroo(val name: String)
@Serializable
data class Person(val name: String)
val json = """{"name":"toby"}"""
// Creem un cangur i una persona a partir del mateix JSON
val kangaroo = Json.decodeFromString<Kangaroo>(json)
val person = Json.decodeFromString<Person>(json)
// Verifiquem que kangaroo i person son objectes diferents, però que tenen el mateix nom
require(kangaroo != person)
require(kangaroo.name == person.name)
}
En alguna aplicació "Toby" pot ser un 🧒 i en una altre pot ser un 🦘.
Activitat
En l'activitat "Sky" de Data vas dissenyar un sistema de gestió de vols:
classDiagram direction TB class Flight { << 🐦🔥 >> id: Long 🔑 ufoSeen: Boolean 🛸 } class Plane { << ✈ >> id: Long 🔑 name: String seats: Int 💺 } class Airport { << ⛩ >> id: Long 🔑 name: String country: String } class Passenger { << 🙂 >> id: Long 🔑 name: String } Flight --> Plane Flight --> "1..* 🤗 🫣 🙄 😮" Passenger Flight --> Airport : departure 🛫 Flight --> Airport: arrival 🛬
TODO
Avaluació
1.-
fun main() {
@Serializable
data class Canguro(val name: String)
@Serializable
data class Person(val name: String)
val json = """{"name":"toby"}"""
println(Json.decodeFromString<Canguro>(json))
println(Json.decodeFromString<Person>(json))
}