Représenter la réponse JSON avec des classes Kotlin
Notre client HTTP est maintenant capable de recevoir et de lire du JSON.
Cependant, Ktor ne sait pas encore dans quelles classes Kotlin placer les informations retournées par REST Countries.
Nous devons donc créer des classes dont la structure correspond à celle de la réponse JSON.
Pour rappel, voici une version simplifiée de la réponse :
{
"data": {
"objects": [
{
"names": {
"common": "Afghanistan"
},
"codes": {
"fifa": "AFG"
},
"flag": {
"emoji": "🇦🇫"
},
"_meta": {
"lastUpdatedTimestamp": 1783005246
}
}
]
}
}
Chaque niveau du JSON sera représenté par une classe Kotlin.
Qu’est-ce qu’un DTO ?
Les classes qui représentent les données reçues depuis une API sont souvent appelées des DTO.
DTO signifie Data Transfer Object, ou objet de transfert de données.
Un DTO a pour rôle de transporter les données entre le serveur et notre application. Sa structure suit généralement celle de la réponse de l’API.
Dans notre projet, les DTO seront différents de la classe Country déjà utilisée par l’interface :
data class Country(
val name: String,
val fifaCode: String,
val flag: String,
)
La classe Country est simple et adaptée aux besoins de l’écran.
Les DTO, en revanche, doivent respecter la structure plus imbriquée de la réponse JSON.
Créons nos DTOs
Dans le package data crée un fichier CountryDto.kt, et délcare la classe suivant
import kotlinx.serialization.Serializable
@Serializable
data class CountriesResponseDto(
val data: CountriesDataDto
)
...
La réponse commence par une propriété appelée data :
{
"data": {}
}
Nous créons donc une classe contenant une propriété portant le même nom.
Le type de data est CountriesDataDto, car son contenu est lui-même un objet.
Sous CountriesResponseDto, ajoute la classe CountriesDataDto :
...
@Serializable
data class CountriesDataDto(
val objects: List<CountryDto>
)
...
À l’intérieur de data, la propriété objects contient plusieurs pays :
{
"data": {
"objects": []
}
}
Chaque élément de cette liste sera représenté par un CountryDto. Sous CountriesDataDto, ajoute CountryDto :
...
@Serializable
data class CountryDto(
val names: CountryNamesDto,
val codes: CountryCodesDto,
val flag: CountryFlagDto,
)
...
Les propriétés names, codes et flag ne sont pas de simples chaînes de caractères. Chacune contient un autre objet.
Sous CountryDto, ajoute les classes suivantes :
...
@Serializable
data class CountryNamesDto(
val common: String
)
...
Cette classe correspond à la partie suivante du JSON :
"names": {
"common": "Afghanistan"
}
...
@Serializable
data class CountryCodesDto(
val fifa: String
)
...
Cette classe correspond à :
"codes": {
"fifa": "AFG"
}
Enfin :
import kotlinx.serialization.SerialName
...
@Serializable
data class CountryFlagDto(
@SerialName("emoji")
val flag: String
)
...
l'annotation @Serializable
Toutes nos classes DTO sont annotées avec @Serializable.
Cette annotation permet à kotlinx.serialization de générer le code nécessaire pour convertir le JSON en objets Kotlin.
Par défaut, les propriétés JSON sont associées aux propriétés Kotlin qui portent exactement le même nom.
Par exemple :
{
"fifa": "AFG"
}
correspond directement à :
val fifa: String
l'annotation @SerialName
La réponse JSON contient une propriété appelée emoji, mais nous préférons l’appeler flag dans notre code Kotlin.
@SerialName("emoji") indique à kotlinx.serialization que la propriété Kotlin flag correspond à la propriété JSON emoji.
La réponse contient également une propriété _meta. Nous ne la déclarons pas parce qu’elle n’est pas utile dans notre cas d’utilisation.
Grâce à ignoreUnknownKeys = true, configuré dans HttpClient.kt, cette propriété supplémentaire sera ignorée pendant la conversion.
Transformer un CountryDto en Country
Après la conversion du JSON, nous obtiendrons des objets CountryDto.
Cependant, notre interface utilise déjà la classe Country. Nous allons donc ajouter une fonction "mapper" toCountry() dans CountryDto.
Dans le même fichier, remplace la déclaration actuelle de CountryDto :
@Serializable
data class CountryDto(
val names: CountryNamesDto,
val codes: CountryCodesDto,
val flag: CountryFlagDto,
)
par :
...
@Serializable
data class CountryDto(
val names: CountryNamesDto,
val codes: CountryCodesDto,
val flag: CountryFlagDto,
) {
fun toCountry(): Country {
return Country(
name = names.common,
fifaCode = codes.fifa,
flag = flag.flag,
)
}
}
...
La fonction récupère les informations imbriquées dans le DTO et construit un objet Country directement utilisable par l’interface.
Par exemple, un CountryDto contenant Afghanistan, AFG et 🇦🇫 sera transformé en :
Country(
name = "Afghanistan",
fifaCode = "AFG",
flag = "🇦🇫"
)
la version finales de CountryDto.kt
À ce stade, CountriesResponseDto.kt doit contenir :
package com.composechef.countryexplorer.data
import com.composechef.countryexplorer.model.Country
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CountriesResponseDto(
val data: CountriesDataDto
)
@Serializable
data class CountriesDataDto(
val objects: List<CountryDto>
)
@Serializable
data class CountryDto(
val names: CountryNamesDto,
val codes: CountryCodesDto,
val flag: CountryFlagDto,
) {
fun toCountry(): Country {
return Country(
name = names.common,
fifaCode = codes.fifa,
flag = flag.flag,
)
}
}
@Serializable
data class CountryNamesDto(
val common: String
)
@Serializable
data class CountryCodesDto(
val fifa: String
)
@Serializable
data class CountryFlagDto(
@SerialName("emoji")
val flag: String
)
Nos classes correspondent maintenant à la réponse JSON de REST Countries.
Dans la prochaine partie, nous allons créer CountryApi, la classe qui utilisera le HttpClient pour envoyer la requête et convertir automatiquement la réponse en CountriesResponseDto.