プログラミング
Deser: Rustのシリアライゼーションの再考
Deser: Rethinking Rust Serialization (lucumr.pocoo.org)
要約
この記事は、RustのシリアライゼーションライブラリであるSerdeの限界と、それを克服するために開発された新しいライブラリ「Deser」について論じています。Serdeは広く使われていますが、内部タグ付きenum、フラット化されたキー、アダプターの非 composability といった問題があります。Deserは、Serdeとは異なるアーキテクチャを採用し、これらの問題を解決することを目指していますが、コンパイル時間やバイナリサイズ、実行時パフォーマンスにはトレードオフが存在します。
全文翻訳
Armin Ronacher's Thoughts and Writingsブログアーカイブプロジェクト旅行は、2026年9月29日に書かれたDeser: Rethinking Rust Serializationについて語る。SerdeはRustのための素晴らしいシリアライゼーションライブラリであり、長年私が生産性を感じてきた大きな理由の一つだ。しかし、Sentryにいた頃からそのいくつかの制限にかなり不満を感じていたが、実際にはそのエコシステムにおける力のため、Serdeを置き換えるのは難しい。また、いくつかの痛みを伴う妥協をせずに、実際に改善するのはかなり難しい。ここでは、Serdeの機能の相互作用の悪さや予期せぬ制限を示す、Serdeのコーナーケースの3つの例を挙げる。マップである数値内部タグ付きenum、serde_jsonのarbitrary_precision機能がオンの場合:#[derive(Deserialize)] #[serde(tag = "type")] enum Shape { Circle { radius: f64 }, } serde_json::from_str::<Shape>(r#"{"type": "Circle", "radius": 1.5}"#) // エラー: 無効な型: map、期待値 f64Serdeのデータモデルには任意精度の数値の場所がないため、serde_jsonはマジックキーを持つマップでインバンドシグナリングを使用する。enumはタグを見るまでフィールドをバッファする必要があるが、バッファはマジックキーを知らない。Cargoの機能は統合されているため、依存関係グラフ内の任意のクレートが機能をオンにすれば十分だ。フラット化が整数キーを壊す#[derive(Deserialize)] struct Stats { scores: HashMap<u32, u32>, } #[derive(Deserialize)] struct Report { name: String, #[serde(flatten)] stats: Stats, } serde_json::from_str::<Report>(r#"{"name": "x", "scores": {"42": 23}}"#) // エラー: 無効な型: string "42"、期待値 u32 行1列35でStats自体は{"scores": {"42": 23}}を問題なくパースする。JSONキーは常に文字列であり、serde_jsonは型がそれを要求した場合にのみ整数に変換する。しかし、flattenが値をバッファすると、「42」は単なる文字列になる。エラーはドキュメントの末尾を指しており、キーを指していない。アダプターは合成されないfn from_hex<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> { ... } #[derive(Deserialize)] struct Theme { #[serde(deserialize_with = "from_hex")] primary: u32, #[serde(deserialize_with = "from_hex")] accent: Option<u32>, } // error[E0308]: `?` operator has incompatible types // | // | #[serde(deserialize_with = "from_hex")] // | ^^^^^^^^^^ expected `Option<u32>`, found `u32` // | // help: try wrapping the expression in `Some` // | // | #[serde(deserialize_with = Some("from_hex"))] // | +++++ +関数は型パラメータとして渡すことができないため、Option、Vec、またはマップの内部にfrom_hexを適用する方法はない。ラッパーごとに別の関数を記述する必要があり、from_opt_hexを記述すると、#[serde(default)]を覚えている場合を除き、フィールドはもはやオプションではなくなる。これらのいずれもSerdeで修正しやすいバグではない。それらはその設計から派生しており、その設計はSerdeの安定性保証によって保護されている。2022年に、私はDeserという実験を開始した。これはRustのためのシリアライゼーションライブラリで、Serdeのユーザーエクスペリエンスを完全に異なるアーキテクチャの上に置き、miniserdeに触発されている。私はそれを完成させたことはなく、数年間放置されていた。私はそれを拾い上げ、今では検討する価値がある地点に達した。他の人がこの分野を探求したいかどうかを見るためのインスピレーションとしてさえも。名前とアイデア名前はSerdeで、その2つの半分が入れ替わっている。DeserはSerdeだが、逆の順序だ。Serdeでは、型がデシリアライゼーションプロセスを駆動する:Deserializeの実装は、デシリアライザーに期待する値の型を要求し、フォーマットはビジターにコールバックする。ネストされた各値は再帰によって処理されるため、Serdeのデシリアライゼーションは各ネストレベルでスタックを必然的に増加させる。一方、Deserはこれを逆転させ、フォーマットが次の値の型を伝え、イベントをシンクにプッシュする。シンクがネストされた値の開始にヒットすると、それにコールバックするのではなく、新しいシンクをドライバに渡す。ドライバはすべての状態をヒープ(実際にはアリーナ)に保持する。出力時には、エミッターは再帰するのではなく、ネストされた値を返す。これはまた、Deserが自己記述的でないprotobufのようなフォーマットをサポートできないことを意味する。それらは実際には意図的に設計から完全に除外されている。これは、Serdeを「修正」したい場合、他の妥協をする必要があるということだ。Deserのアイデアの理由のほとんどは、大量の信頼できないJSONを処理するSentry Relayに戻る。長年にわたり、Sentryにいた頃、私たちは同じ問題セットに繰り返し遭遇し、その多くはSerdeのバグではなく、その設計の結果だった。Serdeの安定性保証は、多くの問題を、すべてのフォーマットとすべての手書き実装を壊すことなく修正できないことを意味する。これらの問題のほとんどは、3つの決定から生じる:すべてのフォーマットのための1セットのトレイト。Serdeは、自己記述フォーマット(JSON、YAML、TOMLなど)と、リーダーが事前に型を知る必要があるフォーマット(postcard、bincode、protobufなど)の両方を提供する。これは信じられないほど有用だが、一部の機能は一部のフォーマットでしか機能せず、実行時に判明する。Serdeの場合、派生構造体がJSONでオブジェクトの代わりに配列を静かに受け入れるといった奇妙な問題もある。バッファリング時に情報が失われる固定データモデル。内部タグ付きenum、タグなしenum、フラット化は、何をすべきかを知る前に値をバッファする必要がある。バッファはフォーマットが知っていたすべてを保持できず、エラーは場所を失い、エコシステムの拡張は任意精度数のようなものを表現するためにインバンドシグナリングに依存する。コールスタック上の再帰。各ネストレベルはスタック空間を使用する。フォーマットは再帰制限でこれを保護するが、再帰制限がないコードパス(書き込み、動的値)を通過すると、深くネストされたデータはプロセスをダウンさせることができる。また、デシリアライゼーションがより多くの入力を待っている間に一時停止できないことも意味する。これらの対応するSerdeの問題の多くは何年もオープンになっており、私は以前Serdeを悪用することについて書いた。人々は何年にもわたってさまざまな角度からこれに取り組んできた。いくつかは最小限に抑え、ほとんどの機能を削除して、高速なコンパイルと再帰なしを実現した。dtolnayのminiserdeがその最良の例であり、deserのトレイト設計は元々それに倣ってモデル化された。他の最近の試みは、実行時リフレクション、またはバイナリフォーマットに焦点を当てた新しいデータモデルに向かった。Serdeの設計に関する収集された課題をすべて読みたい場合は、ここに長いリストを維持している。Serdeを退位させるまず第一に、Serdeを置き換えることができるとはあまり思っていない。孤児ルールは、Serdeをエコシステムに信じられないほどしっかりと定着させている。しかし、クレート作者の制御範囲内にあるものもある。Deserの場合、それは完全性だ。Deserは今日、YAML、JSON、TOML、CBOR、JSON5などのすべての重要な自己記述フォーマットを実装しているが、XMLとplistもギャップを完全に埋めるために実装している。特にXMLはSerdeがサポートを拒否したものであり、それは(以下で詳しく説明する)示唆に富む。少なくともフォーマットサポートはDeserを使用しない理由であってはならない。第二の問題は通常、Serdeの問題を実際に解決することは、コンパイル時間や実行時パフォーマンスに大きなコストがかかるということだ。Deserも例外ではない。Deserのコンパイル時間はSerdeより少し良いが、バイナリの肥大化はかなり悪く、実行時パフォーマンスはまちまちだ。数字を見るとほぼ同等だが、フォーマット構造によっては、一部のトレードオフで大幅に失われる。とはいえ、少なくとも原理的には、トレードオフがユーザーにとってうまく機能する可能性のあるドロップインリプレイスメントの状態にある。DeserのデザインDeserは、表面レベルではSerdeと大きく異なることを目指していない。ほとんどの使用では、SerializeとDeserializeを派生させ、選択したフォーマット実装クレートで使用を開始する。ほとんどの属性は非常にsi