Aller au contenu

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.

Dans votre projet, ajoutez les deux crates. La feature derive permet à serde d’écrire le code de traduction pour vous :

Terminal window
cargo add serde --features derive
cargo add serde_json

Votre 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.

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
}
▶ Essayez par vous-même

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.

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 ans
▶ Essayez par vous-même

Deux 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_str renvoie un Result, car le texte n’est peut-être pas du JSON valide. Ici, main renvoie un Result, donc on peut utiliser ? pour faire remonter une éventuelle erreur.

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 15
Erreur : invalid type: string "old", expected u32 at line 1 column 27
▶ Essayez par vous-même

Les 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.

Et si un champ est vraiment facultatif ? Vous avez deux bonnes options :

  • Faire du champ une Option. Une clé absente (ou null) devient None.
  • Ajouter #[serde(default)]. Une clé absente reçoit la valeur par défaut du type : 0, false, une String vide ou un Vec vide.
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: [] }
▶ Essayez par vous-même

Utilisez Option quand « pas de valeur » veut dire autre chose que « zéro » ou « vide ». Utilisez default quand la valeur par défaut convient.

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"}
▶ Essayez par vous-même

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.

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 API
2 tâches restantes
▶ Essayez par vous-même

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 : 18
Texte brut : Lisbonne
Vent : null
▶ Essayez par vous-même

Remarquez 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.

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 }
▶ Essayez par vous-même

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.

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` type

Le 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.

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}]
▶ Essayez par vous-même

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.

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 }
▶ Essayez par vous-même

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.

  • Ajoutez serde (avec --features derive) et serde_json, puis #[derive(Serialize, Deserialize)] sur vos structs.
  • serde_json::to_string / to_string_pretty transforment une valeur en JSON ; serde_json::from_str transforme du JSON en valeur.
  • from_str renvoie un Result. Pour de vraies données, gérez-le avec ? ou match, pas avec .unwrap().
  • Utilisez Option ou #[serde(default)] pour les clés qui peuvent manquer, et rename / rename_all pour coller aux noms du JSON.
  • Utilisez serde_json::Value et json! quand vous ne connaissez pas la forme des données. Préférez les structs quand vous la connaissez.

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