El format JSON té una implementació per defecte que es pot configurar mitjançant diferents característiques.

Configuració Json

La implementació per defecte de Json és molt estricta respecte a inputs que no són vàlids. Obliga a que els tipus siguin segurs i restringeix els valors que es poden serialitzar de tal manera que les repesentacins Json que es deriven són estàndar.

Però si vols, pots crear una instància de format JSON amb moltes característiques que no són estàndar.

Per fer-ho, has de crear la teva pròpia instància de la classe Json a partir d'una altre instància, per exemple del objecte per defecte Jsonmitjançant la función constructora Json().

Has d'especificar els valors dels paràmetres mitjançant el DSL JsonBuilder.

El resultat és una instància de format Json inmutable i thread-safe que pots guardar en un propietat top-level.

Nota. És molt recomanable que aquesta instància la facis servir en tot el projecte per motius de rendiment perquè les implementacions de format poden tenir una cache d'informació adicional específica del format respecte les classes que serialitzen.

Pretty printing

Per defecte, la sortida Json és una única línia perquè els ordinadors no necessiten que la representació Json estigui indentada, amb salts de línea i fàcil de llegir per nosaltres. Més aviat al contrari, són caràcters que fan nosa i s'han d'ignorar.

En canvi, si nosaltres hem de llegir el Json podem configurar la propoietat prettyPrint amb el valor true:

val format = Json { prettyPrint = true }

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

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

Pots veure que la sortida és completament llegible:

{
    "name": "kotlinx.serialization",
    "language": "Kotlin"
}

Lenient parsing

De manera predeterminada, l'analitzador Json imposa diverses restriccions JSON perquè el resultat sigui el màxim de compatible amb les especificacions: RFC-4627.

Concretament, les claus i els literals de tipus string s'han d'escriure enre cometes (").

Les restriccion s'ignoren si el valor de la propietat isLenient és true :

val format = Json { isLenient = true }

enum class Status { SUPPORTED }

@Serializable
data class Project(val name: String, val status: Status, val votes: Int)

fun main() {
    val data = format.decodeFromString<Project>("""
        {
            name   : kotlinx.serialization,
            status : SUPPORTED,
            votes  : "9000"
        }
    """)
    println(data)
}

Pots veure que l'objecte Json és llegeix sense problemes encara que totes les claus, els string i l'enumeració no està entre cometes, mentre que el número si.

Project(name=kotlinx.serialization, status=SUPPORTED, votes=9000)

Ignorant les claus desconegudes

El format JSON s'utilitza sovint per llegir la sortida de serveis de tercers o en altres entorns dinàmics on es poden afegir noves propietats durant l'evolució de l'API.

De manera predeterminada, les claus desconegudes que es troben durant la deserialització produeixen un error.

Pots evitar-ho i ignorar aquestes claus establint la propieta ignoreUnknownKeys amb el valor true:

val format = Json { ignoreUnknownKeys = true }

@Serializable
data class Project(val name: String)

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

L'objecte es descodifica encara que la classe Project no té la propietat language:

Project(name=kotlinx.serialization)

Noms Json alternatius

A vegades passa que els atributs JSON canvien de nom a causa d'un canvi de versió de l'esquema.

Pots utilitzar l'anotació @SerialNamean per canviar el nom d'un camp JSON, però aquest canvi de nom bloqueja la capacitat de descodificar dades amb el nom antic.

Per admetre diversos noms JSON per a una propietat de Kotlin, hi ha l' anotació JsonNames:

@Serializable
data class Project(@JsonNames("title") val name: String)

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

Com pots veure, tant la propietat Json name com title corresponen a la propietat name:

Project(name=kotlinx.serialization)
Project(name=kotlinx.coroutines)

El suport per a l'anotació JsonNames està controlat pel flag JsonBuilder.useAlternativeNames.

A diferència de la majoria dels flags de configuració, aquest està habilitat per defecte.

Codificació predeterminada

Els valors per defecte de les propietats no es codifiquen de manera predeterminada perquè de totes maneres s'assignaran als camps que falten durant la descodificació.

Això és especialment útil per a propietats anul·lables amb valors nuls per defecte i evita escriure els valors nuls corresponents.

El comportament predeterminat es pot canviar configurant la propietat encodeDefaults a true:

val format = Json { encodeDefaults = true }

@Serializable
class Project(
    val name: String,
    val language: String = "Kotlin",
    val website: String? = null
)

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

Pots veure que aquesta configuració codifica tots els valors, incloent els valors per defecte:

{"name":"kotlinx.serialization","language":"Kotlin","website":null}

Nulls explícits

Per defecte, tots els valors null es codifiquen en strings JSON, però en alguns casos és possible que els vulguis ometre.

La codificació dels valors null es pot controlar amb la propietat explicitNulls i per defecte el seu valor és true.

Si configures la propietat a false, els camps amb valor null no es codifiquen en JSON encara que la propietat no tingui per defecte un valor null.

En descodificar aquest JSON, l'absència d'un valor de propietat es tracta com un null per a propietats anul·lables sense un valor predeterminat.

val format = Json { explicitNulls = false }

@Serializable
data class Project(
    val name: String,
    val language: String,
    val version: String? = "1.2.2",
    val website: String?,
    val description: String? = null
)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin", null, null, null)
    val json = format.encodeToString(data)
    println(json)
    println(format.decodeFromString<Project>(json))
}

Com pots veure, les propietats version, website i description no estan presents a la sortida JSON de la primera línia.

{"name":"kotlinx.serialization","language":"Kotlin"}

Després de la descodificació, la propietat anul·lable website sense valors predeterminats que falta ha rebut el valor null, mentres que les propietats anul·lables version i description s'omplen amb els seus valors per defecte:

Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null)

Fixat que la propietat version de l'objecte que has codificat tenia el valor null, mentres que la propietat version de l'objecte que has codificat té el valor 1.2.2.

La codificació/descodificació de propietats com aquesta (nul·lable amb un valor predeterminat no nul) esdevé asimètrica si explicitNulls està configurat a false.

És possible fer que el descodificador tracti algunes dades d'entrada no vàlides com a camps que falten per millorar la funcionalitat d'aquest flag.

Valors d'entrada coaccionants

Els formats JSON de tercers poden evolucionar, i a vegdes es modifica els tipus de camp.

Això pot provocar excepcions durant la descodificació quan els valors reals no coincideixen amb els valors esperats.

La implementació per defecte Json és estricta pel que fa als tipus d'entrada, però pots relaxar aquesta restricció utilitzant la propietat coerceInputValues .

Aquesta propietat només afecta la descodificació, i tracta un subconjunt limitat de valors d'entrada no vàlids com si faltes la propietat corresponent:

  • Entrades null per a tipus no anul·lables
  • Valors desconeguts per a enumeracions

Si falta un valor;

  • Es substitueix per un valor de propietat predeterminat si existeix
  • Amb null si el flag explicitNulls s'estableix a false i la propietat és anul.lable (per a enumeracions).

En aquest codi la propietat language no és anul.lable i per defecte el seu valor és "Kotlin", però la propietat language de l'objecte Json és null:

val format = Json { coerceInputValues = true }

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

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

Com que el flag coerceInputValues està actiu, l'execució del codi no dona error i la propietat languagede l'objecte decodificat té valor per defecte "Kotlin":

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

A continuació tens un exemple amb un enum amb els flag coerceInputValues activat i el flag explicitNulls desactivat:

enum class Color { BLACK, WHITE }

@Serializable
data class Brush(val foreground: Color = Color.BLACK, val background: Color?)

val json = Json { 
  coerceInputValues = true
  explicitNulls = false
}

fun main() {
    val brush = json.decodeFromString<Brush>("""{"foreground":"pink", "background":"purple"}""")
  println(brush)
}

Encara que l'enum Color no té els colors "pink" i "purple", la funció decodeFromString no dona error:

Brush(foreground=BLACK, background=null)

Pots veure que a la propietat foreground se li assigna el seu valor per defecte i a la propietat background se li assigna el valor nullperuè el flag explicitNulls` està desactivat.

Permet claus de mapa estructurat

El format JSON no admet de forma nativa el concepte de mapa amb claus estructurades.

En els objectes JSON les claus són strings i només es poden utilitzar per representar valor primaris o enumeracions.

Amb el flag allowStructuredMapKeys pots activar el suport no estàndard per a claus estructurades.

A continuació pots veure com podem serialitzar un mapa amb claus d'una classe definida per l'usuari:

val format = Json { allowStructuredMapKeys = true }

@Serializable
data class Project(val name: String)

fun main() {
    val map = mapOf(
        Project("kotlinx.serialization") to "Serialization",
        Project("kotlinx.coroutines") to "Coroutines"
    )
    println(format.encodeToString(map))
}

El mapa amb claus estructurades no es pot representar amb un objecte Json, sinó que s'ha de representar amb un array JSON amb els elements següents: [key1, value1, key2, value2,...] tal com pots veure a continuació:

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

Permet valors especials de coma flotant

De manera predeterminada, els valors especials de coma flotant com Double.NaN i els infinits no s'admeten a JSON perquè l'especificació JSON ho prohibeix.

Podeu habilitar la seva codificació mitjançant la propietat allowSpecialFloatingPointValues:

val format = Json { allowSpecialFloatingPointValues = true }

@Serializable
class Data(
    val value: Double
)

fun main() {
    val data = Data(Double.NaN)
    println(format.encodeToString(data))
}

L'objecte JSON no és estàndard, encara que eś una codificació molt utilitzada per valors especials en el món JVM:

{"value":NaN}

Discriminador de classes per al polimorfisme

Per defecte, per determinar el tipus d'una dada polimòrfica es fa servir la propietat type en l'objecte Json.

Amb la propietat classDiscriminator pots canviar el nom d'aquesta propietat de manera global:

val format = Json { classDiscriminator = "#class" }

@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(format.encodeToString(data))
}

Pots veure que enlloc de type l'objecte Json utilitza #class:

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

Però si tens diferents jerarqies, i cada jerarquia necsseita un discriminador de classe amb nom diferent, pots utilitzar l'anotació JsonClassDiscriminator directament a la classe base serializable:

@Serializable
@JsonClassDiscriminator("message_type")
sealed class Base

Aquesta anotació és heretable , de manera que totes les subclasses de Base tindran el mateix discriminador:

@Serializable // Class discriminator is inherited from Base
sealed class ErrorClass: Base()

Tingues en compte que no pots especificar de manera explícita diferents discriminadors de classe a les subclasses de Base.

Només les jerarquies amb interseccions buides poden tenir diferents discriminadors.

El discriminador especificat a l'anotació té prioritat sobre el discriminador a la configuració Json:

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

@Serializable
@JsonClassDiscriminator("message_type")
sealed class Base

@Serializable // Class discriminator is inherited from Base
sealed class ErrorClass: Base()

@Serializable
data class Message(val message: Base, val error: ErrorClass?)

@Serializable
@SerialName("my.app.BaseMessage")
data class BaseMessage(val message: String) : Base()

@Serializable
@SerialName("my.app.GenericError")
data class GenericError(@SerialName("error_code") val errorCode: Int) : ErrorClass()


val format = Json { classDiscriminator = "#class" }

fun main() {
    val data = Message(BaseMessage("not found"), GenericError(404))
    println(format.encodeToString(data))
}

Pots veure que el format Json utlilitza el discriminador de la classe Base:

{    
    "message": {"message_type": "BaseMessage", "message":"not found"},
    "error": {"message_type": "GenericError","error_code":404}}

Mode de sortida del discriminador de classe

El discriminador de classes proporciona informació per serialitzar i deserialitzar jerarquies de classes polimòrfiques .

Com es mostra més amunt, per defecte el discriminador només s'afegeix a classes polimòrfiques.

En cas que vulguis codificar més o menys informació per a diverses API de tercers sobre els tipus de la classe en l'objecte JSON, pots controlar l'addició del discriminador de classes amb la propietat JsonBuilder.classDiscriminatorMode .

Per exemple, quan la teva aplicació envia dades Json, l'API que les rep potser no vol aquesta propietat.

Pots configurar la propietat classDiscriminatorMode amb el valor ClassDiscriminatorMode.NONE per no afegeix cap discriminador de classe:

val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }

@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(format.encodeToString(data))
}

Aquesta és l'objecte Json, sense descriminador de classe:

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

Els altres dos valor que pots utilitzar són ClassDiscriminatorMode.POLYMORPHIC (comportament per defecte) i ClassDiscriminatorMode.ALL_JSON_OBJECTS (afegeix el discriminador sempre que sigui possible).

Descodificació de enumeracions sense distingir entre majúscules i minúscules

En les enumeracions, en els noms de les variants s'acostuma utilitzar majúscules separades per un guió baix o noms en formant "upper camel case".

Però a vegades, els JSON de tercers no utilizen aquest format.

En aquest cas, pots descodificar els valors d'enumeració sense distingir entre majúscules i minúscules mitjançant la propietat JsonBuilder.decodeEnumsCaseInsensitive :

val format = Json { decodeEnumsCaseInsensitive = true }

enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }

@Serializable
data class CasesList(val cases: List<Cases>)

fun main() {
  println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}""")) 
}

Aquesta configuració afecta tant als noms per defecte de serialització com amb els noms configurats amb l'anotació JsonNames, de tal manera que tots dos valors es descodifiquen correctament:

CasesList(cases=[VALUE_A, VALUE_B])

Aquesta propietat no afecta la codificació de cap manera.

Estratègia global de noms

Si el nom d'una propietat d'un objecte json no coincideix amb el nom de la propietat de la classe pots utilitzar l'anotació @SerialName.

Però hi ha situacions en que es més eficient gestionar-ho de manera blobal, com quan modifiques el framekor que fa servir el teu projece ho has d'incorporar codi antiquat.

En aquest casos pots implementar una namingStrategy per a una instància Json.

Per exemple, en la biblioteca kotlinx.serialization tens la implementació JsonNamingStrategy.SnakeCase:

@Serializable
data class Project(val projectName: String, val projectOwner: String)

val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

fun main() {
    val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
}

Pots veure que la classe Project utilitza noms en format "camel case" mentres que l'objecte Json utilitza noms en format "snake case", i la (de)serialització funciona correctament:

{ 
    "project_name": "kotlinx.serialization",
    "project_owner": "Kotlin"
}

Molts llenguatges utlitzen noms en "snake case".

Però has d'anar en compte al utlitzar una estrategia global perqué:

  • Les anotacions @SerialName no es tenen en compte, només pots utilitzar l'anotació @JsonNames.

  • Els noms "globals" no es veuen reflectits en el codi i poden coincidir sense voler amb noms d'una altra propietat de la classe o de les anotacions JsonNames i es produirà un error en temps d'execució.

  • Com que aquests noms no estan en el codi no pots utilitzar ajudes de la IDE com Cercar usos/Canviar el nom, o fer cerques amb grep. Per a aquestes eines el nom "global" no existeix, i si només canvies el nom del codi es poden produir errors inesperats i incrementes la dificultat de mantenir el codi.

Elements Json

A vegades no es convenient converir un objecte json en una classe i és millor treballar directament amb ell perqué les dades no estan estructurades, s'han de modificar abans de processar, etc.

En aquest casos el que es va es convertir l'objecte json en una instància de la classe JsonElement mitjançant la funció Json.parseToJsonElement:

fun main() {
    val element = Json.parseToJsonElement("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(element)
}

L instànica element representa un objecte json i s'imprimeix com a tal:

{"name":"kotlinx.serialization","language":"Kotlin"}

Tipus d'elements Json

Una classe JsonElement té tres subtipus directes que reflecteixen la gramàtica JSON:

  • JsonPrimitive representa elements JSON primitius, com ara string, number, boolean i null, i es representen amb un propietat contentp de tipus string. També tens la funció constructora [JsonPrimitive()](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-primitive.html) que està sobrecarregada i accepta diversos tipus primitius de Kotlin per convertir-los a JsonPrimitive`.

  • [JsonArray`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-array/index.html) representa un array JSON `[...]` i és una `List` d'elements de tipus `JsonElement`.

  • JsonObject representa un objecte JSON {...} i és un Map de claus String a valors JsonElement.

La clase JsonElement té extensions per fer un "cast" als seus subtipus corresponents: jsonPrimitive, jsonArray i jsonObject.

Al seu torn, la classe JsonPrimitive proporciona convertidors als tipus primitius de Kotlin com int, intOrNull, long, longOrNull, etc.

fun main() {
    val element = Json.parseToJsonElement("""
        {
            "name": "kotlinx.serialization",
            "forks": [{"votes": 42}, {"votes": 9000}, {}]
        }
    """)
    val sum = element
        .jsonObject["forks"]!!
        .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
    println(sum)
}

El coid suma tots els votes dels objectes de l'array forks, ignorant els objectes que no tenen votes:

9042

Si l'estructura de dades es diferent de l'esperada es produirà un error en temps d'execució.

Constructors d'elements Json

A vegades has de construir un objecte Json directament.

Per fer-ho pots utilitzar les funcions constructores buildJsonArray i buildJsonObject que proporcionen un DSL per definir l'estructura JSON resultant:

fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        putJsonObject("owner") {
            put("name", "kotlin")
        }
        putJsonArray("forks") {
            addJsonObject {
                put("votes", 42)
            }
            addJsonObject {
                put("votes", 9000)
            }
        }
    }
    println(element)
}

Pots veure que el reultat és l'esperat:

{"name":"kotlinx.serialization","owner":{"name":"kotlin"},"forks":[{"votes":42},{"votes":9000}]}

Descodificació d'elements Json

Una instància de la classe JsonElement es pot descodificar en un objecte serialitzable mitjançant la funció Json.decodeFromJsonElement:

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

fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        put("language", "Kotlin")
    }
    val data = Json.decodeFromJsonElement<Project>(element)
    println(data)
}
Project(name=kotlinx.serialization, language=Kotlin)

Transformació Json

Si vols modificar la forma i el contingut de la sortida JSON (o al revés) pots implementar un serialitzador personalitzat.

Però si són petites modificacions és millor utilitzar la classe abstracta JsonTransformingSerializer que implementa KSerialize.

Enlloc de tenir que interactuar directament amb Encoder o Decoder, el que fas es escriure transformacions per a l'arbre json que està representat amb una instància de la classe JsonElement mitjançant els mètodes transformSerialize i transformDeserialize.

Array wrapping

El primer exemple és una implementació de JSON "array wrapping" per a llistes.

Imagina els cas d'una API REST que torna un array JSON d'objectes User, o un únic objecte User sense estar dins un array si només hi ha un resultat.

En el model de dades utilitzes l'anotació @Serializable per especificar un serialitzador personalitzat per ala propietat users: List<User>.

@Serializable
data class Project(
    val name: String,
    @Serializable(with = UserListSerializer::class)
    val users: List<User>
)

@Serializable
data class User(val name: String)

Com que només ens interessa el procés de deserialització només has de sobreescriure el mètode transformDeserialize.

També pots veure que per construir un objecte JsonTransformSerializer passem el serialitzador que es faria servir per defecte, i que es el reponsable de serialitzar la propietat al JsonElement que rebem en la funció transformDeserialize:

object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
    // If response is not an array, then it is a single object that should be wrapped into the array
    override fun transformDeserialize(element: JsonElement): JsonElement =
        if (element !is JsonArray) JsonArray(listOf(element)) else element
}

Podem provar el codi amb un array JSON o un únic objecte JSON:

fun main() {
    println(Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
    """))
    println(Json.decodeFromString<Project>("""
        {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
    """))
}

I tots dos casos es deserialitzen correctament en una List:

Project(name=kotlinx.serialization, users=[User(name=kotlin)])
Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])

Array unwrapping

També pots implementar la funció transformSerialize per tractar el ca en que es serialitza la llista amb només un usuari:

    override fun transformSerialize(element: JsonElement): JsonElement {
        require(element is JsonArray) // this serializer is used only with lists
        return element.singleOrNull() ?: element
    }

Ara, si serialitzes una llista d'objectes d'un sol element:

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

El resultat és un únic objecte JSON, no un array amb un element:

{"name":"kotlinx.serialization","users":{"name":"kotlin"}}

Manipulació del valors per defecte

Un altre tipus de transformació útil és ometre valors específics de la sortida JSON.

Imagina que per algun motiu no pots especificar un valor predeterminat per a la propietat language en el model de dades Project, però en el JSON no ha d'aparèixer el valor "kotlin".

Pots escriure un ProjectSerializer que utilitza el serialitzador generar pel plugin per a la classe Project:

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

object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
    override fun transformSerialize(element: JsonElement): JsonElement =
        // Filter out top-level key value pair with the key "language" and the value "Kotlin"
        JsonObject(element.jsonObject.filterNot {
            (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
        })
}

En el codi que es mostra a continuació passem de manera explícita el nostre serialitzador a la funció Json.encodeToString:

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data)) // using plugin-generated serializer
    println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
}

Pots veure que el serialitzador personalitzat funciona correctament:

{"name":"kotlinx.serialization","language":"Kotlin"}
{"name":"kotlinx.serialization"}

Deserialització polimòrfica basada en contingut

La serialització polimòrfica necessita una propietat type en l'objecte json (també conegut com a discriminador de classe) per determinar quin serialitzador s'ha d'utilitzar per deserialitzar l'objecte.

Però a vegades aquesta propietat no està a l'entrada i cal endevinar el tipus real en funció de les propitats de l'objecte JSON, per exemple per la presència d'una clau específica.

JsonContentPolymorphicSerializer proporciona una implementació "skeleton" per a aquesta estratègia: has de sobreesciure el mètode selectDeserializer.

Observació En aquest cas que la classe base sigui "sealed" no proporciona cap benefici perquè no aprofitarás el codi generat pel plugin que selecciona automàticament la subclasse adequada.

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

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


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

Si només ens fixem en les propietats de les subclasses podem distingir BasicProject i OwnedProject per la propietat owner, i en base a això podem escriure un serialitzador que discrimina en base a aquesta propietat:

object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
    override fun selectDeserializer(element: JsonElement) = when {
        "owner" in element.jsonObject -> OwnedProject.serializer()
        else -> BasicProject.serializer()
    }
}

Quan utilitzes aquest serialitzador l'has de passar de manera explícita les funcions encodeToString i decodeFromSrting:

fun main() {
    val data = listOf(
        OwnedProject("kotlinx.serialization", "kotlin"),
        BasicProject("example")
    )
    val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
    println(string)
    println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
}

Pots veure que no s'afegeix cap discriminador de classe a la sortida JSON:

[{"name":"kotlinx.serialization","owner":"kotlin"},{"name":"example"}]
[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]

Mantenir atributs JSON personalitzats

Quan deserialitzes un objecte json pot ser que algunes propietats no tinguin correspondica amb la classe que es fa servir per deserializar l'objecte, i enlloc d'ignorar-les les vols guardar en una propietat de la classe de tipus JsonObject.

Afegim al model de dades una classe UnknownProject amb una propietat details de tipus JsonObject:

data class UnknownProject(val name: String, val details: JsonObject)

Però el serialitzador predeterminat generat pel plugin vol que details sigui un objecte JSON diferent i això no és el que volem.

Per tant, has d'escriure un serialitzador que utilitzi el fet que aquesta classe només funciona amb el format Json:

object UnknownProjectSerializer : KSerializer<UnknownProject> {
   
     override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
        element<String>("name")
        element<JsonElement>("details")
    }

    override fun deserialize(decoder: Decoder): UnknownProject {
        // Cast to JSON-specific interface
        val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
        // Read the whole content as JSON
        val json = jsonInput.decodeJsonElement().jsonObject
        // Extract and remove name property
        val name = json.getValue("name").jsonPrimitive.content
        val details = json.toMutableMap()
        details.remove("name")
        return UnknownProject(name, JsonObject(details))
    }

    override fun serialize(encoder: Encoder, value: UnknownProject) {
        error("Serialization is not supported")
    }
}

Ja pots utilitzar aquest serialitzador:

fun main() {
    println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
}

Pots veure que totes les propietats de l'objecte json, excepte "name", estan a details:

``sh UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})