Sérialisation — serde
1. Qu'est-ce que serde
serde (SERialization/DEserialization) est le standard de facto pour convertir des structures Rust vers et depuis des formats de données : JSON, TOML, YAML, MessagePack, CBOR, etc.
Architecture en deux couches :
// Couche 1 — serde core
// Définit les traits Serialize et Deserialize.
// Tes types implémentent ces traits.
// Couche 2 — serde_json, toml, serde_yaml...
// Chaque crate de format sait comment lire/écrire son format.
// Elle utilise les traits de serde core pour traverser tes données.
Le principal avantage : tu annotas tes types une seule fois, et tu peux les sérialiser vers n'importe quel format sans modifier le code.
2. Ajouter serde au projet
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
# Formats courants (ajouter selon le besoin)
serde_json = "1"
toml = "0.8"
serde_yaml = "0.9"
La feature derive active les macros #[derive(Serialize, Deserialize)]. Sans elle, tu devrais tout implémenter à la main.
3. `#[derive(Serialize, Deserialize)]`
C'est la façon la plus courante d'utiliser serde. La macro génère le code d'(dé)sérialisation pour toi.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Utilisateur {
id: u32,
nom: String,
email: String,
actif: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct Article {
titre: String,
contenu: String,
auteur: Utilisateur,
tags: Vec<String>,
}
Les types standard (String, Vec, Option, HashMap, entiers, flottants, booléens) sont tous sérialisables nativement.
// Option<T> se sérialise en null si None, en valeur si Some
#[derive(Serialize, Deserialize)]
struct Profil {
nom: String,
bio: Option<String>, // null en JSON si absent
age: Option<u8>,
}
4. JSON avec `serde_json`
Sérialiser (Rust → JSON)
use serde_json;
let utilisateur = Utilisateur {
id: 1,
nom: "Alice".to_string(),
email: "alice@example.com".to_string(),
actif: true,
};
// Vers une String
let json = serde_json::to_string(&utilisateur)?;
// {"id":1,"nom":"Alice","email":"alice@example.com","actif":true}
// Vers une String formatée (indentée)
let json_pretty = serde_json::to_string_pretty(&utilisateur)?;
// Vers un Vec<u8> (pour écrire dans un fichier, une socket...)
let bytes = serde_json::to_vec(&utilisateur)?;
Désérialiser (JSON → Rust)
let json = r#"{"id": 2, "nom": "Bob", "email": "bob@example.com", "actif": false}"#;
// Depuis une &str ou String
let utilisateur: Utilisateur = serde_json::from_str(json)?;
// Depuis des bytes
let utilisateur: Utilisateur = serde_json::from_slice(&bytes)?;
// Depuis un reader (fichier, réseau...)
let fichier = std::fs::File::open("utilisateur.json")?;
let utilisateur: Utilisateur = serde_json::from_reader(fichier)?;
La valeur dynamique `serde_json::Value`
Quand la structure n'est pas connue à la compilation :
use serde_json::{json, Value};
// Construire du JSON sans struct
let payload = json!({
"action": "connexion",
"donnees": {
"id": 42,
"roles": ["admin", "user"]
}
});
// Naviguer dans un JSON arbitraire
let data: Value = serde_json::from_str(json_inconnu)?;
if let Some(nom) = data["utilisateur"]["nom"].as_str() {
println!("Nom : {nom}");
}
5. TOML avec `toml`
# config.toml
[serveur]
hote = "0.0.0.0"
port = 8080
[base_de_donnees]
url = "postgres://localhost/mydb"
pool_max = 10
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct Config {
serveur: ConfigServeur,
base_de_donnees: ConfigDb,
}
#[derive(Debug, Deserialize, Serialize)]
struct ConfigServeur {
hote: String,
port: u16,
}
#[derive(Debug, Deserialize, Serialize)]
struct ConfigDb {
url: String,
pool_max: u32,
}
// Lire un fichier TOML
let contenu = std::fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&contenu)?;
println!("Port : {}", config.serveur.port);
// Écrire en TOML
let toml_str = toml::to_string_pretty(&config)?;
std::fs::write("config_export.toml", toml_str)?;
6. Personnaliser la sérialisation
Les attributs #[serde(...)] permettent d'adapter le comportement sans écrire de code d'(dé)sérialisation manuellement.
`#[serde(rename)]` — renommer un champ
#[derive(Serialize, Deserialize)]
struct Commande {
#[serde(rename = "order_id")]
id_commande: u64,
#[serde(rename = "customer_name")]
nom_client: String,
}
// Sérialisé en : {"order_id": 1, "customer_name": "Alice"}
`#[serde(rename_all)]` — renommer tous les champs
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReponseApi {
user_id: u32, // → userId
nom_complet: String, // → nomComplet
date_creation: String, // → dateCreation
}
// Valeurs possibles : "camelCase", "snake_case", "PascalCase",
// "SCREAMING_SNAKE_CASE", "kebab-case"
`#[serde(skip)]` — exclure un champ
#[derive(Serialize, Deserialize)]
struct Utilisateur {
id: u32,
nom: String,
#[serde(skip)]
mot_de_passe_hash: String, // jamais sérialisé ni désérialisé
#[serde(skip_serializing)]
token_interne: String, // désérialisé depuis JSON, jamais écrit
}
`#[serde(default)]` — valeur par défaut si absent
#[derive(Serialize, Deserialize)]
struct Parametres {
langue: String,
#[serde(default)] // utilise Default::default() → false
notifications: bool,
#[serde(default = "taille_par_defaut")]
taille_page: u32,
}
fn taille_par_defaut() -> u32 { 20 }
`#[serde(alias)]` — accepter plusieurs noms à la désérialisation
#[derive(Deserialize)]
struct Payload {
#[serde(alias = "username", alias = "login")]
nom_utilisateur: String,
// Accepte "nom_utilisateur", "username" ou "login" dans le JSON entrant
}
7. Sérialisation d'enums
serde gère toutes les formes d'enums. La représentation JSON peut être contrôlée avec #[serde(tag)].
Forme par défaut (externally tagged)
#[derive(Serialize, Deserialize, Debug)]
enum Evenement {
Connexion { user_id: u32 },
Message { de: String, texte: String },
Deconnexion,
}
// Serialisé en :
// {"Connexion":{"user_id":1}}
// {"Message":{"de":"Alice","texte":"Bonjour"}}
// "Deconnexion"
`#[serde(tag = "type")]` — internally tagged
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Notification {
Email { destinataire: String, sujet: String },
Sms { numero: String, corps: String },
Push { token: String },
}
// Serialisé en :
// {"type":"Email","destinataire":"alice@...","sujet":"..."}
// {"type":"Sms","numero":"+33...","corps":"..."}
`#[serde(tag = "type", content = "data")]` — adjacently tagged
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Reponse {
Ok(String),
Erreur { code: u32, message: String },
}
// {"type":"Ok","data":"succès"}
// {"type":"Erreur","data":{"code":404,"message":"Non trouvé"}}
`#[serde(untagged)]` — sans tag
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Valeur {
Entier(i64),
Flottant(f64),
Texte(String),
Booleen(bool),
}
// Serde devine le variant selon la forme des données
8. Types génériques et serde
serde s'intègre naturellement avec les génériques, à condition d'ajouter les bounds appropriés.
use serde::{Deserialize, Serialize};
// T doit implémenter Serialize + Deserialize
#[derive(Serialize, Deserialize, Debug)]
struct Page<T> {
items: Vec<T>,
total: u64,
page: u32,
par_page: u32,
}
// Utilisation
let page: Page<Utilisateur> = Page {
items: vec![/* ... */],
total: 150,
page: 1,
par_page: 20,
};
let json = serde_json::to_string(&page)?;
Pour les bounds explicites dans les impl :
use serde::{de::DeserializeOwned, Serialize};
fn envoyer_json<T: Serialize>(valeur: &T) -> String {
serde_json::to_string(valeur).unwrap()
}
fn recevoir_json<T: DeserializeOwned>(json: &str) -> Result<T, serde_json::Error> {
serde_json::from_str(json)
}
// DeserializeOwned = Deserialize<'static> — pas de lifetime à gérer
Wrapper générique de réponse API
#[derive(Serialize)]
struct ApiReponse<T: Serialize> {
succes: bool,
donnees: Option<T>,
erreur: Option<String>,
}
impl<T: Serialize> ApiReponse<T> {
fn ok(donnees: T) -> Self {
ApiReponse { succes: true, donnees: Some(donnees), erreur: None }
}
fn err(message: impl Into<String>) -> ApiReponse<()> {
ApiReponse { succes: false, donnees: None, erreur: Some(message.into()) }
}
}
9. Exemples concrets avec Runique
Entités SeaORM qui dérivent Serialize
Dans Runique, les entités SeaORM sont dans demo-app/src/entities/. Pour les exposer via une API JSON, on dérive Serialize sur le Model :
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "articles")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub titre: String,
pub contenu: String,
pub publie: bool,
#[serde(skip)] // ne pas exposer dans l'API
pub auteur_id: i32,
#[serde(rename = "createdAt")] // convention camelCase pour le front
pub created_at: DateTimeUtc,
}
Réponses JSON dans un handler Axum
use axum::{extract::State, Json};
use serde::Serialize;
#[derive(Serialize)]
struct ArticleListeReponse {
articles: Vec<article::Model>,
total: u64,
}
pub async fn liste_articles(
State(db): State<DatabaseConnection>,
) -> Result<Json<ArticleListeReponse>, AppError> {
let articles = Article::find()
.filter(article::Column::Publie.eq(true))
.all(&db)
.await?;
let total = articles.len() as u64;
Ok(Json(ArticleListeReponse { articles, total }))
}
Désérialiser un body de requête
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreerArticlePayload {
titre: String,
contenu: String,
#[serde(default)]
publie: bool,
tags: Vec<String>,
}
pub async fn creer_article(
State(db): State<DatabaseConnection>,
Json(payload): Json<CreerArticlePayload>,
) -> Result<Json<article::Model>, AppError> {
let nouvel_article = article::ActiveModel {
titre: Set(payload.titre),
contenu: Set(payload.contenu),
publie: Set(payload.publie),
..Default::default()
};
let article = nouvel_article.insert(&db).await?;
Ok(Json(article))
}
10. Exercices pratiques
Exercice 1 : struct de configuration
Crée une struct AppConfig avec les champs : hote: String, port: u16, debug: bool (défaut false), max_connexions: u32 (défaut 100). Lis-la depuis le TOML suivant :
hote = "127.0.0.1"
port = 3000
Exercice 2 : enum de statut
Crée un enum StatutCommande avec les variants EnAttente, EnCours { depuis: String }, Livree, Annulee { raison: String }. Sérialise-le avec #[serde(tag = "statut")] et vérifie la sortie JSON.
Exercice 3 : pagination générique
Écris une struct PaginatedResponse<T> avec items: Vec<T>, total: u64, page: u32. Sérialise-la avec une liste de strings, puis une liste d'entiers, en vérifiant que le JSON produit est correct.
Exercice 4 : alias et renommage
Crée une struct LoginPayload qui accepte "email" ou "username" pour le champ identifiant, et "password" ou "mot_de_passe" pour le mot de passe. Utilise #[serde(alias)].
11. Aide-mémoire
| Besoin | Attribut / méthode |
|---|---|
| Renommer un champ | #[serde(rename = "nom")] |
| Renommer tous les champs | #[serde(rename_all = "camelCase")] |
| Exclure un champ | #[serde(skip)] |
| Exclure à la sérialisation | #[serde(skip_serializing)] |
| Valeur par défaut | #[serde(default)] ou #[serde(default = "fn")] |
| Accepter plusieurs noms | #[serde(alias = "autre")] |
| Enum avec tag interne | #[serde(tag = "type")] |
| Enum sans tag | #[serde(untagged)] |
| Sérialiser → JSON String | serde_json::to_string(&val)? |
| Désérialiser ← JSON String | serde_json::from_str::<T>(json)? |
| JSON formaté | serde_json::to_string_pretty(&val)? |
| JSON dynamique | serde_json::Value + macro json!{} |
| TOML → struct | toml::from_str::<T>(&contenu)? |
| struct → TOML | toml::to_string_pretty(&val)? |
| Bound générique désér. | T: DeserializeOwned |
Règle : Dériez toujours
Serialize+Deserializeensemble sauf besoin explicite de l'un sans l'autre. Préférez#[serde(rename_all)]au niveau de la struct plutôt que desrenamechamp par champ.