Rustのgardeで日本語を文字数バリデーションするときの注意点
はじめに
これまでJavaを利用することが多かったのですが、最近業務でRust(axum)を使ったAPI開発に挑戦しました。
その中で、gardeを使った文字数バリデーションに少しハマることがあったので、学んだことをまとめておきます。
利用したバージョンは0.22.0です。
バリデーションでハマった例と原因調査
ユーザーからリクエストボディを受け取り、それを構造体にマッピングして扱うというケースはよくあることかと思います。
Javaの@Sizeアノテーションを使うような感覚で、Rustでも構造体に対してバリデーションを実装してみました。
Rustのコード
use garde::Validate;
#[derive(Debug, Validate)]
pub struct Request {
#[garde(length(max = 10))]
pub theme: String,
}
ただ、以下のテストコードを実行すると、文字列が10文字ぴったりのはずなのにバリデーションエラーが発生します。
#[cfg(test)]
mod tests {
use super::*;
use garde::Validate;
#[test]
fn test() {
// 1. setup
let request = Request {
theme: "あ".repeat(10),
};
// 2. execute
let actual = request.validate();
// 3. verify
assert!(actual.is_ok());
}
}
failures:
tests::test
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
いろいろ試しているうちに、4文字以上でバリデーションエラーが発生し、3文字まではテストが通ることがわかりました。
これはなぜか……?
gardeの仕様と文字数モード
調査したところ、garde はデフォルトで「バイト数」でバリデーションチェックを行っていることがわかりました。
"simple length" depends on the type. It is currently implemented for strings, where it validates the number of bytes
— garde公式ドキュメント
つまり、"あ" はUTF-8で1文字3バイトなので、10文字だと30バイトになります。max = 10 の制限をオーバーしてしまうため、バリデーションエラーになるというわけです。
モードの種類
gardeのlengthには以下の4つのモードが用意されています(公式ドキュメントより引用)。
#[derive(garde::Validate)]
struct Foo {
#[garde(length(bytes, min = 1, max = 100))]
a: String, // a.len()
#[garde(length(graphemes, min = 1, max = 100))]
b: String, // b.graphemes().count()
#[garde(length(utf16, min = 1, max = 100))]
c: String, // c.encode_utf16().count()
#[garde(length(chars, min = 1, max = 100))]
d: String, // d.chars().count()
}
日本語を含む文字列において、
-
見た目の文字数で制限したいなら
graphemes -
UTF-16ベースで制限したいなら
utf16 -
コードポイントの数を基準にするなら
chars
と、使い分けが可能です。
今回はフロントエンド側のバリデーションに合わせるため、 utf16 を使用することにしました。
#[derive(Debug, Validate)]
pub struct Request {
#[garde(length(utf16, max = 10))]
pub theme: String,
}
こうすることで、意図通りの動作になりました。
running 1 test
test tests::test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
用途や他のシステムの仕様に合わせて、適切なモードを選択するのが良さそうです。
さいごに
gardeを使って日本語の文字列バリデーションを行う際に注意すべき点をまとめました。
Javaを中心に開発してきた私にとって、Rustで文字列の長さをデフォルトでは“バイト数”で扱うという仕様は新鮮でした。Javaでは@Sizeアノテーションのように、バイト数等を考慮せずにバリデーションできることが多かったため、この違いは私にとって思わぬ落とし穴でした。
また、少し視点を変えると、GitHub Copilotを使ってコードを書く際に、その出力コードを検証することは当然ですが、提案されたライブラリが初めて使うものであれば、必ず公式ドキュメントを一度確認すべきだと感じました。
今回のように事前に gardeの公式ドキュメント全体に軽く目を通していれば、躓くことなく実装できたかもしれません。
Discussion