SerdeV - Serde with Validation の紹介と黒魔術解説
Rust で
#[derive(Serialize, Deserialize, Debug)]
#[serde(validate = "Self::validate")]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn validate(&self) -> Result<(), impl std::fmt::Display> {
if self.x * self.y > 100 {
return Err("x * y must not exceed 100")
}
Ok(())
}
}
fn main() {
let point = serde_json::from_str::<Point>(r#"
{ "x" : 1, "y" : 2 }
"#).unwrap();
// Prints point = Point { x: 1, y: 2 }
println!("point = {point:?}");
let error = serde_json::from_str::<Point>(r#"
{ "x" : 10, "y" : 20 }
"#).unwrap_err();
// Prints error = x * y must not exceed 100
println!("error = {error}");
}
( README より引用 )
みたいなこと ( validation on deserializing without boilerplate ) できないのかなーということに数年前から興味があり、その後 Rust 力をつけた結果できそうな気がしてきたので SerdeV というクレートを開発してみたらできました。紹介を兼ねて研究レポート的な意味で中身の解説を置いておきます。
前提
-
Serde を fork して改造するのは嫌
- それでよければ割と簡単にできそうだが、その後本家の issue や PR に追従するのが面倒
- あくまで Serde のラッパーとして、proc macro の力でなんとか実現する
-
現実的に需要があるかはとりあえず気にしない
-
Parse, dont' validate という有名 (?) な標語の通り、パースした時点で型システムによって valid であることが保証されるような型設計ができれば、明示的なバリデーションというものはいらない
- そして Rust は ( 多くの場合 ) それができる言語である
- しかし、時に Serde の中で気軽にバリデーションを入れたい場面も存在する気はする
- 特に Web 開発で (?)
-
Parse, dont' validate という有名 (?) な標語の通り、パースした時点で型システムによって valid であることが保証されるような型設計ができれば、明示的なバリデーションというものはいらない
準備
準備1:ディレクトリ構成
例によって proc macro crate は proc macro しか export できなかったりするので、
.
├── serdev
│ └── src
└── serdev_derive
└── src
└── internal
として、serdev_derive
で Serialize
, Deserialize
の derive 実装を提供し、serdev
で
pub use serdev_derive::{Serialize/* macro */, Deserialize/* macro */};
pub use ::serde::ser::{self, Serialize/* trait */, Serializer};
pub use ::serde::de::{self, Deserialize/* trait */, Deserializer};
とします。
準備2:Serialize
SerdeV は ( validate
以外において ) Serde と 100% compatible を謳っているので、普通に
use serdev::Serialize;
#[derive(Serialize)]
struct Point {
x: i32,
y: i32,
}
のように書ける必要があります。そこで #[derive(serde::Deserialize)] の impl 部分
みたいなものを生成すればいいので、
#[doc(hidden)]
pub mod __private__ {
pub use ::serde;
}
として derive 実装で
quote! {
#[derive(::serdev::__private__::serde::Serialize)]
#[serde(crate = "::serdev::__private__::serde")]
#input
}
を返すのが素朴なアイデアです。
が、これだけではうまくいきません。というのも、#[derive(〜)] #input
というものは #input
を受け取って #input そのもの + derive macroが返したもの を返す
ので、上記の実装だと
-
もとの構造体は保存される
-
derive(serdev::Serialize)
が#[derive(::serdev::__private__::serde::Serialize)] #[serde(crate = "::serdev::__private__::serde")] #input
を返す
-
それが本家の
derive(serde::Serialize)
によって#input
そのもの +Serialize
impl に展開されるので、最終的にstruct Point { x: i32, y: i32, } struct Point { x: i32, y: i32, } /* Serialize impl */
となってコンパイルエラー
ということになります。これの解決にあたって黒魔術が必要になります。これです:
つまり、この consume
という attribute を serdev::__private__
から export しておき、
quote! {
#[derive(::serdev::__private__::serde::Serialize)]
#[serde(crate = "::serdev::__private__::serde")]
#[::serdev::__private__::consume]
#input
}
を返すようにするとうまくいきます。
ただし、このままだと、
use serdev::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(validate = "...")]
struct Point {
x: i32,
y: i32,
}
とした際に、( 本家の ) derive(Serialize)
にもそのまま #[serde(validate = "...")]
が渡されてしまい「そんなの知らないが?」と言われてしまうので、
のように #[serde(validate ...)]
があったら外してから渡してあげます。
本題:Deserialize
長々と準備してきましたが、実はあとは #[serde(try_from = "FromType")]
を使えばいけることに気づけばほぼやるだけ です:
-
対象の構造体 ( target とします ) の名前だけ変えたもの ( proxy とします ) を用意し、
#[derive(serde::Deseirlize)]
をつける -
proxy のフィールドを target に移し替え、バリデーションを行ってから返すような
impl TryFrom<{proxy}> for {target} ...
を用意する -
本家の
derive(Deserialize)
に#[serde(try_from = "{proxy}")]
をつけて target に対するDeserialize
実装を導出する
#[serde(validate ...)]
を抜き取る実装、target の generics の扱い、 TryFrom::Error
をどうするか ( 関数の path を渡されるだけではその error の型を認識できない ) など細かい課題はありますが、ここまで理解できた方なら
に行って眺めれば分かると思うので省略します。
1つだけ非自明かもしれない点があるので補足します。1. の proxy を作る処理
で ident
を変更したあと、なにやら attrs
を filter しているのですが、これは proxy に #[serde(〜)]
以外の attribute を残しておくと、それが他の derive macro 由来のものだった場合に
#[derive(::serdev::__private__::serde::Deserialize)]
#[serde(crate = "::serdev::__private__::serde")]
#[allow(non_camel_case_types)]
#proxy
の部分で ( その derive がないので ) コンパイルエラーになるからです。例:
// filter しない場合
#[derive(::serdev::__private__::serde::Deserialize)]
#[serde(crate = "::serdev::__private__::serde")]
#[allow(non_camel_case_types)]
struct serdev_proxy_SignupData {
#[validate(email)] // <-- ここでコンパイルエラー
mail: String,
〜
おわりに
Parse, dont' validate を踏まえて Serde with Validation についてどう思うか、いろんな人の意見を知りたい気持ちがあるので、何か思うことがある方はぜひこの記事のコメントか SNS にでも書いてください 👀 SerdeV を気に入った方は star をつけていただけると嬉しいです。
Discussion