Rust小ネタ:Serdeで型パラメータを含む構造体をシリアライズする
フェアリーデバイセズ株式会社プロダクト開発部のCubbitです。趣味でプログラミング言語Rustを触っていて、いつか業務でもRustを使ってみたいなと思っていたら、今の会社ではWebアプリケーションをRustで開発するお仕事ができています。先日コーディング中につまずいて、社内の人に教えてもらって勉強になった小ネタを紹介します。
Deserializeの導出
RustのSerdeを使うと、#[derive(Deserialize)]を付与するだけで構造体が簡単にデシリアライズできるようになります。ただし、その構造体に含まれる各フィールドもすべてデシリアライズできなくてはなりません。たとえば、以下のように std::fs::Fileをフィールドに含む構造体を定義しようとしたとします。
#[derive(Deserialize)]
struct Foo {
result: std::fs::File,
}
std::fs::Fileはデシリアライズできないので、このコードはthe trait bound `File: serde::Deserialize<'de>` is not satisfiedというエラーになります。
エラーメッセージ
error[E0277]: the trait bound `File: serde::Deserialize<'de>` is not satisfied
--> src/main.rs:14:13
|
14 | result: std::fs::File,
| ^^^^^^^^^^^^^ the trait `Deserialize<'_>` is not implemented for `File`
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `File` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `Deserialize<'de>`:
&'a Path
&'a [u8]
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
and 140 others
note: required by a bound in `next_element`
--> /Users/username/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.228/src/de/mod.rs:1771:12
|
1769 | fn next_element<T>(&mut self) -> Result<Option<T>, Self::Error>
| ------------ required by a bound in this associated function
1770 | where
1771 | T: Deserialize<'de>,
| ^^^^^^^^^^^^^^^^ required by this bound in `SeqAccess::next_element`
コードの動作を考えても、OSのファイルハンドルをそのままデータとして保存しておいてあとから自由に復元するなどできるはずがありません。このように、#[derive(Deserialize)]をつける構造体のフィールドは何でもOKなわけではなく、Deserializeできる型でなければならないという制約があります。
発端
いま開発しているプロダクトの機能を拡張するにつれ、ひとつのフィールドの型だけが異なる、以下のような2つの構造体が出てきました(実際のコードより単純化しています)。
#[derive(Deserialize)]
struct A {
result: X,
}
#[derive(Deserialize)]
struct B {
result: Y,
}
これらの構造体は意味としても使われ方も似通っていて、できればひとつの構造体にまとめたいと思いました。また、将来的にはXやYだけでなくもっと他のデータ型の値を扱いたくなることもありそうです。そこで、フィールドresultの型を型パラメータTとして一般化しつつ全体をシリアライズできるようにしたいと思います。フィールドはデシリアライズできる制約が必要であるという先ほどの話を踏まえると、ジェネリックな型TにはT: Deserializeみたいな制約をつければよさそうです。ChatGPTに訊いたところ、DeserializeOwnedというトレイトを付ければいいよ、というような返答だったので、これを使ってみます。
#[derive(Deserialize)]
struct Generic<T: DeserializeOwned>{
result: T,
}
しかしこれをコンパイルすると、type annotations needed: cannot satisfy T: ...というようなエラーになります。
エラーメッセージ
error[E0283]: type annotations needed: cannot satisfy `T: Deserialize<'_>`
--> src/main.rs:4:8
|
4 | struct Generic<T: DeserializeOwned>{
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: multiple `impl`s or `where` clauses satisfying `T: Deserialize<'_>` found
--> src/main.rs:3:10
|
3 | #[derive(Deserialize, Debug)]
| ^^^^^^^^^^^
4 | struct Generic<T: DeserializeOwned>{
| ^^^^^^^^^^^^^^^^
note: required for `Generic<T>` to implement `Deserialize<'de>`
--> src/main.rs:3:10
|
3 | #[derive(Deserialize, Debug)]
| ^^^^^^^^^^^ unsatisfied trait bound introduced in this `derive` macro
4 | struct Generic<T: DeserializeOwned>{
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
エラーメッセージが何を言っているのかよくわからないですが……?DeserializeOwnedではなくDeserialize<'de>でライフタイムを明示したりすればうまくいったりしないでしょうか……?
#[derive(Deserialize)]
struct Generic<'de, T>
where
T: Deserialize<'de>,
{
result: T,
}
これもcannot deserialize when there is a lifetime parameter called 'deだとかlifetime parameter 'de is never usedと言われて、どうしたらいいかよくわかりませんでした。
エラーメッセージ
error: cannot deserialize when there is a lifetime parameter called 'de
--> src/main.rs:4:16
|
4 | struct Generic<'de, T>
| ^^^
error[E0392]: lifetime parameter `'de` is never used
--> src/main.rs:4:16
|
4 | struct Generic<'de, T>
| ^^^ unused lifetime parameter
|
= help: consider removing `'de`, referring to it in a field, or using a marker such as `PhantomData`
error[E0277]: the trait bound `Generic<'_, std::string::String>: serde::Deserialize<'de>` is not satisfied
--> src/main.rs:20:35
|
20 | let parsed: Generic<String> = serde_json::from_str(json_str).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `Deserialize<'_>` is not implemented for `Generic<'_, std::string::String>`
--> src/main.rs:4:1
|
4 | struct Generic<'de, T>
| ^^^^^^^^^^^^^^^^^^^^^^
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Generic<'_, std::string::String>` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `Deserialize<'de>`:
&'a Path
&'a [u8]
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
and 141 others
note: required by a bound in `serde_json::from_str`
--> /Users/username/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_json-1.0.145/src/de.rs:2699:8
|
2697 | pub fn from_str<'a, T>(s: &'a str) -> Result<T>
| -------- required by a bound in this function
2698 | where
2699 | T: de::Deserialize<'a>,
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `from_str`
解決策その1 (手動でboundを定義)
RustのSerdeでは以下のように#[serde(bound = ...)]という属性を書くと、手動で型に制約(bound)を設定することができるようです。
#[derive(Deserialize)]
#[serde(bound = "T: Deserialize<'de>")]
struct Generic<T> {
result: T,
}
構造体本体の定義のほうにwhere T: Deserialize<'de>のように書いてはダメなんですね……。Tに制約が必要なのはたしかなのですが、書く場所が違いました。'deが急に出てくるのがちょっと奇妙な感じがします。
解決策2 (自動で解決)
解決策1では手動で制約を指定しましたが、そもそもSerdeは制約をある程度自動で解決できるので、大抵は手動で明示する必要はないようです。以下のように T に何の制約も与えなくてもうまくいきます。
#[derive(Deserialize)]
struct Generic<T>{
result: T,
}
ドキュメントにもちゃんと書いてあります。
When deriving Serialize and Deserialize implementations for structs with generic type parameters, most of the time Serde is able to infer the correct trait bounds without help from the programmer.
As with most heuristics, this is not always right and Serde provides an escape hatch to replace the automatically generated bound by one written by the programmer.
ジェネリック型パラメータを持つ構造体の Serialize と Deserialize の実装を導出するさい、ほとんどの場合、Serdeはプログラマの助けを借りずに正しいトレイト境界を推論できます。
多くのヒューリスティックと同様に、これは常に正しいとは限らないため、Serdeは自動的に生成された境界をプログラマが記述した境界に置き換えるためのエスケープハッチを提供しています。
手動でDeserializeの制約をつける必要があると思い込んでましたが、自動推論に任せるだけでよかったんですね。むしろ手動で制約をつけると、自動での解決とバッティングしておかしくなってしまうようです。マクロのエラーメッセージが何を言っているのかわからないのもあって、少し遠回りをしてしまいました。
さいごに
弊社のSlackにはRustユーザーのためのチャンネルがあって、このあたりの疑問を投げたら社内の人たちが15分くらいで答えてくれて解決しました。社内にRustに詳しい方が多いのは心強いです。

それとドキュメントはちゃんと目を通したほうがいいですね。Serdeの公式ドキュメントにちゃんと書いてあったのに、読んでいませんでした。
Discussion