ProgrammationDéveloppeur Rust Backend/API

Comment implémenter la sérialisation et la désérialisation des données en Rust ? Quels problèmes typiques surviennent lors de l'intégration avec des formats externes (JSON, TOML) et comment assurer la sécurité de la conversion des structures ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En Rust, la sérialisation et la désérialisation des données sont des tâches assez courantes, surtout lors de l'intégration avec des services web, des bases de données ou l'échange de messages entre composants. La bibliothèque la plus populaire pour cela est serde, qui est devenue la norme de facto pour travailler avec la sérialisation en Rust.

Contexte de la question

Les développeurs Rust ont rencontré la nécessité d'une sérialisation flexible et performante. Initialement, il existait une multitude de solutions spécialisées, mais c'est serde qui a proposé une intégration pratique avec des macros derive, un schéma flexible de support pour divers formats (JSON, CBOR, BSON, TOML, YAML et d'autres).

Problème

Il est nécessaire de garantir une transformation correcte des structures Rust en formats d'échange de données (par exemple JSON), et vice versa, sans perdre la sécurité de type et sans permettre des "erreurs silencieuses". Les problèmes sont souvent causés par une incompatibilité de structure de données ou par la tentative de sérialiser des types non pris en charge.

Solution

Pour sérialiser des structures, il faut implémenter les traits Serialize et Deserialize, généralement à l'aide de #[derive(Serialize, Deserialize)]. Pour des cas non standards, on peut implémenter ces traits manuellement ou utiliser des attributs qui gèrent le schéma de sérialisation.

Exemple de code :

use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct Person { name: String, age: u8, } fn main() { let data = Person { name: "Bob".into(), age: 32 }; let json = serde_json::to_string(&data).unwrap(); println!("{}", json); let obj: Person = serde_json::from_str(&json).unwrap(); println!("{:?}", obj); }

Caractéristiques clés :

  • derive implémente automatiquement les traits nécessaires pour la plupart des cas.
  • serde prend en charge de nombreux formats et est facilement extensible.
  • Il est possible de décrire précisément le mapping via des attributs (rename, default, skip, etc.).

Questions pièges.

Comment sérialiser/désérialiser des champs Option ou des champs avec des valeurs par défaut ?

On peut utiliser #[serde(default)] pour indiquer une valeur par défaut ou #[serde(skip_serializing_if = "Option::is_none")], pour ne pas sérialiser None.

Exemple de code :

#[derive(Serialize, Deserialize)] struct Config { #[serde(default)] timeout: Option<u32>, }

Peut-on sérialiser des structures avec des champs personnalisés ou des valeurs calculées (par exemple fn get_hash()) ?

Oui, mais ces champs doivent être marqués avec #[serde(skip)], s'ils ne doivent pas être sérialisés, ou implémenter Serialize/Deserialize manuellement, si des valeurs calculées doivent être sérialisées.

Que se passe-t-il si la structure en Rust ne correspond pas aux champs de l'objet JSON (champ manquant ou nouveau) ?

Par défaut, serde ignore les champs inutiles dans JSON (sauf si le mode strict est activé), et renverra une erreur en cas d'absence de champs obligatoires dans la structure (si #[serde(default)] n'est pas spécifié).

Erreurs typiques et anti-patterns

  • Attente d'une correspondance stricte des formats sans spécifier default/skip — conduit à des erreurs d'analyse.
  • Sérialisation de structures avec des valeurs privées ou non valides sans vérification supplémentaire.
  • Implémenter manuellement Serialize/Deserialize sans nécessité.

Exemple de la vie réelle

Cas négatif

Un JSON externe arrive avec des champs supplémentaires et manquants, et la structure n'utilise pas #[serde(default)],

Avantages :

  • Intégration rapide.

Inconvénients :

  • Crashes à la première mauvaise JSON.
  • Difficile à étendre.

Cas positif

Utilisation de #[serde(default)] et #[serde(skip_serializing_if)], tous les champs sont validés, les surplus sont ignorés.

Avantages :

  • Résilience face aux changements de schéma.
  • Compatibilité descendante simple.

Inconvénients :

  • Un peu plus de boilerplate pour la configuration.
  • Tous les cas non standards ne sont pas couverts automatiquement, une implémentation manuelle est parfois nécessaire.