⚠️

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