S’abonner EN

Ingénierie · 20 avr. 2026

SurrealDB 3 & la sérialisation

Elwann Guillemot · 3 min de lecture

Au cours du dernier mois, nous avons apporté pas mal de changements à Scylla et à son architecture. L’une de nos plus belles victoires est arrivée avec la sortie de SurrealDB 3.0, qui nous a permis de supprimer entièrement un morceau important de notre code, en sérialisant directement nos entités de domaine vers la base, tout en restant fidèles à la Clean architecture.

L’ancienne structure

Avec la version 2.x, chaque entité de domaine portait un derive Serialize/Deserialize. Quand on écrivait un User, le SDK le faisait passer par serde_json, produisait une serde_json::Value, puis le convertissait dans son propre arbre de valeurs interne. Deux choses nous gênaient :

  1. Les IDs étaient stringly-typed. Nos UserId, AgentId, et compagnie étaient des wrappers autour de String qui devenaient de simples chaînes JSON sur le réseau. Le SDK ne pouvait pas distinguer un record ID utilisateur d’un record ID organisation, ni d’un "…" quelconque.
  2. La crate de domaine connaissait JSON. Un format de sérialisation s’était invité au milieu de nos types de domaine, alors qu’il n’avait rien à y faire.

Ce que la v3 a changé

SurrealDB 3 introduit un trait SurrealValue dans une nouvelle crate surrealdb-types. C’est la représentation native des valeurs côté SDK, le même arbre que voit le serveur, et les types peuvent soit le dériver, soit l’implémenter à la main. Plus aucun JSON au milieu.

Des record IDs typés

Nos IDs sont générés par une macro define_id!. Avec la v3, nous avons pu implémenter SurrealValue :

rust
impl surrealdb_types::SurrealValue for $name {
    fn kind_of() -> surrealdb_types::Kind {
        surrealdb_types::Kind::Record(vec![Table::from($table)])
    }

    fn into_value(self) -> surrealdb_types::Value {
        Value::RecordId(RecordId::new($table, self.0))
    }

    fn from_value(value: Value) -> Result<Self, Error> { /* … */ }
}

kind_of() est l’élément clé. Chaque UserId déclare qu’il appartient à la table users, et le SDK transporte cette information jusque dans SurrealQL. Passer un UserId là où la requête attend un OrganizationId est rejeté au niveau de la valeur, et non par la base de données trois sauts réseau plus loin.

Pour les requêtes RELATE, le code devient particulièrement élégant :

rust
self.db
    .query("RELATE $user_id -> user_organization -> $org_id")
    .bind(("user_id", user_id.clone().into_value()))
    .bind(("org_id", org_id.clone().into_value()))
    .await?;

into_value() produit un RecordId typé que le SDK utilise directement. Pas de formatage thing("users:…"), pas de bindings stringly-typed, et plus aucun risque qu’un ID malformé se retrouve traité comme une simple string dans la requête.

Plus de double encodage

Quand on appelle .content(user) sur un create, la valeur passe de User à SurrealValue puis à SurrealDB directement. Plus aucun besoin de serde_json, ni de gérer les bugs liés à la sérialisation dans SurrealDB : précision numérique, formes d’enums, timestamps, …

Un feature flag à la place de serde

Comme SurrealValue est un type du SDK, l’implémenter sur un type de domaine est un couplage à SurrealDB. Nous n’en voulions pas par défaut, donc nous avons mis les derives derrière une feature surrealdb :

toml
[features]
surrealdb = ["dep:surrealdb-types"]

Quand la feature est désactivée, notre domaine n’est que du Rust : aucune dépendance ou mention à Surreal. Quand elle est activée, chaque entité récupère une implémentation SurrealValue propre et les repositories de scylla-core savent comment la persister.

Derive vs implémentation manuelle

Pour les value objects simples (Username, OrganizationName, et compagnie), on utilise simplement #[derive(SurrealValue)] et on récupère l’impl. Exclusivement pour les types d’IDs générés, on l’écrit à la main afin de faire passer le nom de la table via kind_of(). Les deux approches cohabitent sans friction.

Pour conclure

Nous sommes fiers du résultat. Il reste évidemment des améliorations et optimisations possibles, et nous y travaillons. Nous reviendrons peut-être sur son évolution dans de prochains devlogs !

Stay tuned!

À lire aussi