JSON avec Serde en Rust
Le JSON est partout : fichiers de configuration, API web, sauvegardes de jeux. En Rust, presque tout le monde utilise deux crates pour le manipuler. serde (contraction de serialize / deserialize, sérialiser / désérialiser) sait transformer des valeurs Rust en données et inversement. serde_json lui apprend le format JSON.
Voyez serde comme un traducteur. Vous décrivez vos données une seule fois, sous forme de struct Rust ordinaire, et serde les traduit en JSON et inversement à votre place. Vous n’écrivez jamais le texte JSON à la main, et vous ne le décortiquez jamais caractère par caractère.
Mise en place
Section intitulée « Mise en place »Dans votre projet, ajoutez les deux crates. La feature derive permet à serde d’écrire le code de traduction pour vous :
cargo add serde --features derivecargo add serde_jsonVotre Cargo.toml contient maintenant :
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Si Cargo.toml et cargo add sont nouveaux pour vous, lisez d’abord Cargo et les crates. Le Rust Playground dispose déjà des deux crates : chaque bouton « Essayer » de cette page fonctionne sans rien installer.
D’une struct vers du JSON
Section intitulée « D’une struct vers du JSON »Ajoutez #[derive(Serialize)] à une struct, et serde_json::to_string pourra la transformer en texte JSON :
use serde::Serialize;
#[derive(Serialize)]struct User { name: String, age: u32, admin: bool,}
fn main() { let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap(); println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap(); println!("{}", pretty);}Sortie :
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string vous donne du JSON compact, idéal pour l’envoyer sur le réseau. to_string_pretty ajoute des retours à la ligne et des espaces, plus agréable pour des fichiers que des humains liront. Les clés gardent le même ordre que les champs de votre struct.
Du JSON vers une struct
Section intitulée « Du JSON vers une struct »Le chemin inverse s’appelle la désérialisation. Ajoutez Deserialize à la liste des derive, puis appelez serde_json::from_str. Vous lui indiquez le type voulu avec une annotation de type :
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]struct User { name: String, age: u32, admin: bool,}
fn main() -> Result<(), serde_json::Error> { let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?; println!("{:?}", user); println!("{} a {} ans", user.name, user.age); Ok(())}Sortie :
User { name: "Ana", age: 28, admin: false }Ana a 28 ansDeux choses à remarquer :
r#"..."#est une chaîne brute (raw string). À l’intérieur, vous pouvez écrire"librement, ce qui est très pratique pour le JSON.from_strrenvoie unResult, car le texte n’est peut-être pas du JSON valide. Ici,mainrenvoie unResult, donc on peut utiliser?pour faire remonter une éventuelle erreur.
Quand le JSON n’est pas bon
Section intitulée « Quand le JSON n’est pas bon »Le JSON réel ne ressemble pas toujours à ce qu’on attend. Un champ manque, ou un nombre arrive sous forme de texte. serde vérifie tout et vous donne une erreur claire au lieu de planter. Utilisez match pour la gérer :
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct User { name: String, age: u32,}
fn main() { let inputs = [ r#"{"name": "Ana", "age": 28}"#, r#"{"name": "Ben"}"#, r#"{"name": "Cy", "age": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK : {:?}", user), Err(e) => println!("Erreur : {}", e), } }}Sortie :
OK : User { name: "Ana", age: 28 }Erreur : missing field `age` at line 1 column 15Erreur : invalid type: string "old", expected u32 at line 1 column 27Les messages d’erreur viennent de serde et sont en anglais : le premier signale un champ age manquant (« missing field »), le second un type invalide (« invalid type ») : une chaîne "old" au lieu d’un u32. Les deux indiquent la ligne et la colonne du problème dans le texte JSON.
La partie ::<User> s’appelle le « turbofish ». C’est une autre façon d’indiquer le type voulu, utile quand il n’y a pas de let user: User sur lequel mettre l’annotation.
Clés manquantes : Option et #[serde(default)]
Section intitulée « Clés manquantes : Option et #[serde(default)] »Et si un champ est vraiment facultatif ? Vous avez deux bonnes options :
- Faire du champ une
Option. Une clé absente (ounull) devientNone. - Ajouter
#[serde(default)]. Une clé absente reçoit la valeur par défaut du type :0,false, uneStringvide ou unVecvide.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Profile { name: String, bio: Option<String>, #[serde(default)] followers: u32, #[serde(default)] tags: Vec<String>,}
fn main() { let full = r#"{"name": "Ana", "bio": "Adore Rust", "followers": 120, "tags": ["rust"]}"#; let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap(); let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Sortie :
Profile { name: "Ana", bio: Some("Adore Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Utilisez Option quand « pas de valeur » veut dire autre chose que « zéro » ou « vide ». Utilisez default quand la valeur par défaut convient.
Renommer les champs
Section intitulée « Renommer les champs »Rust utilise le snake_case pour les noms de champs, mais beaucoup d’API web utilisent le camelCase, et certaines utilisent des noms qui ne sont même pas valides en Rust (comme type). Pas besoin de renoncer à de jolis noms Rust : utilisez rename_all pour toute la struct, et rename pour un seul champ :
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct UserProfile { user_id: u64, display_name: String, is_verified: bool, #[serde(rename = "type")] account_type: String,}
fn main() { // le genre de réponse qu'une API web pourrait renvoyer let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) a un compte {}", profile.display_name, profile.user_id, profile.account_type);
// la sérialisation utilise les mêmes noms : le JSON fait l'aller-retour println!("{}", serde_json::to_string(&profile).unwrap());}Sortie :
Ana Silva (id 42) a un compte premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}Par défaut, serde ignore tout simplement les clés JSON que votre struct n’a pas. Vous n’avez donc à décrire que les champs qui vous intéressent, même si l’API en envoie cinquante.
Des listes : un Vec de structs
Section intitulée « Des listes : un Vec de structs »Un tableau JSON devient un Vec. Les structs peuvent aussi contenir d’autres structs et des vecteurs : vous pouvez donc décrire du JSON profondément imbriqué rien qu’en imbriquant des types :
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Apprendre serde", "done": true}, {"title": "Écrire un fichier de config", "done": false}, {"title": "Appeler une API", "done": false} ]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks { let mark = if task.done { "x" } else { " " }; println!("[{}] {}", mark, task.title); } let left = tasks.iter().filter(|t| !t.done).count(); println!("{} tâches restantes", left);}Sortie :
[x] Apprendre serde[ ] Écrire un fichier de config[ ] Appeler une API2 tâches restantesDu JSON sans type : Value et json!
Section intitulée « Du JSON sans type : Value et json! »Parfois, vous ne connaissez pas la forme du JSON à l’avance, ou vous n’en voulez qu’un petit morceau. Dans ce cas, utilisez serde_json::Value. Il peut contenir n’importe quel JSON : un objet, un tableau, une chaîne, un nombre, un booléen ou null. La macro json! construit une Value avec une syntaxe proche du JSON :
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisbonne", "temps": [18, 21, 19], "sunny": true });
println!("Ville : {}", data["city"]); println!("Première température : {}", data["temps"][0]);
// as_str() vous donne un simple &str au lieu d'une chaîne JSON if let Some(city) = data["city"].as_str() { println!("Texte brut : {}", city); }
// une clé absente donne Null au lieu de planter println!("Vent : {}", data["wind"]);}Sortie :
Ville : "Lisbonne"Première température : 18Texte brut : LisbonneVent : nullRemarquez que l’affichage de data["city"] montre des guillemets : c’est toujours une chaîne JSON. Utilisez .as_str(), .as_i64() ou .as_bool() pour obtenir de simples valeurs Rust. Chacune renvoie une Option, car la valeur peut être d’un autre type.
Value est souple, mais vous perdez l’aide du compilateur. Préférez une struct dès que vous connaissez la forme des données.
Lire et écrire un fichier JSON
Section intitulée « Lire et écrire un fichier JSON »Voici un exemple concret : enregistrer les réglages d’une application dans config.json, puis les recharger. Il combine cette page avec Lire et écrire des fichiers. std::fs::write enregistre le texte, et std::fs::read_to_string le relit :
use serde::{Deserialize, Serialize};use std::error::Error;use std::fs;
#[derive(Serialize, Deserialize, Debug)]struct Config { username: String, theme: String, font_size: u32,}
fn main() -> Result<(), Box<dyn Error>> { let config = Config { username: String::from("ana"), theme: String::from("sombre"), font_size: 14, };
// enregistrer let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// recharger let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Chargé : {:?}", loaded); Ok(())}Sortie :
Chargé : Config { username: "ana", theme: "sombre", font_size: 14 }Pourquoi Box<dyn Error> ? Ce main peut échouer de deux façons différentes : une erreur de fichier (std::io::Error) ou une erreur JSON (serde_json::Error). Box<dyn Error> veut dire « n’importe quel type d’erreur », donc ? fonctionne pour les deux. La page Erreurs personnalisées l’explique plus en détail.
Erreurs courantes
Section intitulée « Erreurs courantes »Oublier de dériver Deserialize. Si votre struct ne dérive que Serialize, vous pouvez écrire du JSON mais pas le lire. L’erreur est longue, mais voici les lignes importantes :
error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied |10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` typeLe compilateur vous dit que User n’implémente pas le trait Deserialize, et vous suggère d’ajouter #[derive(serde::Deserialize)]. La solution : #[derive(Serialize, Deserialize)]. Si vous voyez plutôt « cannot find derive macro Serialize », c’est que vous avez ajouté serde sans la feature derive. Lancez cargo add serde --features derive.
Utiliser .unwrap() sur des données que vous ne contrôlez pas. C’est acceptable dans de petits exemples, mais le JSON qui vient d’un utilisateur ou d’un site web peut être cassé. Un fichier invalide fera planter votre programme avec une panic. Utilisez ? ou match comme montré plus haut.
S’attendre à ce que data["name"] s’affiche sans guillemets. Une Value s’affiche en JSON. Appelez .as_str() pour obtenir du texte brut.
Exercices
Section intitulée « Exercices »1. Enregistrer une liste de courses
Section intitulée « 1. Enregistrer une liste de courses »Créez une struct Item avec un name (String) et une qty (u32). Mettez deux articles dans un Vec, transformez tout le vecteur en JSON en un seul appel, et affichez-le.
Indice : seule la struct a besoin de #[derive(Serialize)]. Un Vec d’éléments sérialisables est lui aussi sérialisable, et il devient un tableau JSON.
Voir la solution
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("pommes"), qty: 3 }, Item { name: String::from("pain"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Sortie :
[{"name":"pommes","qty":3},{"name":"pain","qty":1}]serde sait déjà gérer Vec, String et les nombres : dériver Serialize sur Item suffit. Essayez to_string_pretty pour voir la même liste répartie sur plusieurs lignes.
2. Des réglages avec valeurs par défaut
Section intitulée « 2. Des réglages avec valeurs par défaut »Lisez ce JSON dans une struct Settings : {"siteTitle": "Mon blog", "postsPerPage": 5}. La struct doit avoir site_title, posts_per_page et un theme qui peut manquer. Lisez ensuite aussi {"siteTitle": "Notes"}, où posts_per_page doit retomber à 0. Affichez les deux avec {:?}.
Indice : il vous faut #[serde(rename_all = "camelCase")] sur la struct, Option<String> pour theme, et #[serde(default)] sur posts_per_page.
Voir la solution
use serde::Deserialize;
#[derive(Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct Settings { site_title: String, #[serde(default)] posts_per_page: u32, theme: Option<String>,}
fn main() { let full = r#"{"siteTitle": "Mon blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Notes"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Sortie :
Settings { site_title: "Mon blog", posts_per_page: 5, theme: None }Settings { site_title: "Notes", posts_per_page: 0, theme: None }rename_all fait correspondre siteTitle à site_title et postsPerPage à posts_per_page. Aucun des deux JSON n’a de theme, donc il vaut None les deux fois, et le postsPerPage absent du second devient 0. Retirez #[serde(default)] et le second unwrap() panique avec « missing field postsPerPage » (champ manquant). Le .unwrap() est acceptable ici parce que nous avons écrit le JSON nous-mêmes ; pour de vraies données, utilisez ? ou match.
Récapitulatif
Section intitulée « Récapitulatif »- Ajoutez
serde(avec--features derive) etserde_json, puis#[derive(Serialize, Deserialize)]sur vos structs. serde_json::to_string/to_string_prettytransforment une valeur en JSON ;serde_json::from_strtransforme du JSON en valeur.from_strrenvoie unResult. Pour de vraies données, gérez-le avec?oumatch, pas avec.unwrap().- Utilisez
Optionou#[serde(default)]pour les clés qui peuvent manquer, etrename/rename_allpour coller aux noms du JSON. - Utilisez
serde_json::Valueetjson!quand vous ne connaissez pas la forme des données. Préférez les structs quand vous la connaissez.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel derive permet de transformer du texte JSON en votre struct ?
2. Un objet JSON peut ne pas contenir la clé bio. Quel est un bon type pour ce champ ?
3. Votre struct a un champ display_name, mais le JSON utilise displayName. Quelle est la solution la plus simple pour toute la struct ?
4. Que fait serde par défaut avec les clés JSON que votre struct n'a pas ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- serde.rs — le guide officiel de Serde (en anglais) : une vue d’ensemble du fonctionnement de serde, avec des exemples pour de nombreux formats de données.
- Container attributes — serde.rs (en anglais) : les options pour une struct ou une enum entière, comme
rename_alletdeny_unknown_fields. - Field attributes — serde.rs (en anglais) : les options pour un seul champ, comme
rename,default,skipetskip_serializing_if. serde_json— docs.rs (en anglais) : la page d’accueil de la crate, avec des exemples de JSON typé et non typé et de la macrojson!.serde_json::Value— docs.rs (en anglais) : toutes les méthodes pour lire du JSON non typé, commeas_str,as_i64etget.