validator
Rust用の構造体バリデーションライブラリ。derive マクロを使用してバリデーションルールを定義
概要
validatorは、Rustで構造体のデータ検証を行うための軽量で使いやすいライブラリです。derive マクロを使用して宣言的にバリデーションルールを定義でき、コンパイル時の型安全性と実行時のデータ検証を両立させています。Web APIやフォーム処理などの用途で広く使用されています。
主な特徴
- Deriveマクロベース:
#[derive(Validate)]で簡単にバリデーション機能を追加 - 豊富な組み込みバリデータ: 長さ、範囲、Email、URL、正規表現など
- カスタムバリデータ: 独自の検証ロジックを実装可能
- 詳細なエラー情報: フィールド別のバリデーションエラーを取得
- 軽量: 最小限の依存関係でパフォーマンス重視
- Serde統合: シリアライゼーション/デシリアライゼーションとの連携
インストール
[dependencies]
validator = "0.16"
# Serde統合を使用する場合
validator = { version = "0.16", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
# 国際化(i18n)サポートを使用する場合
validator = { version = "0.16", features = ["derive", "unic"] }
基本的な使い方
シンプルなバリデーション
use validator::{Validate, ValidationError};
#[derive(Debug, Validate)]
struct User {
#[validate(length(min = 3, max = 20))]
username: String,
#[validate(email)]
email: String,
#[validate(range(min = 18, max = 150))]
age: u8,
}
fn main() {
let user = User {
username: "john_doe".to_string(),
email: "[email protected]".to_string(),
age: 25,
};
match user.validate() {
Ok(()) => println!("バリデーション成功!"),
Err(e) => {
println!("バリデーションエラー:");
for (field, errors) in e.field_errors() {
for error in errors {
println!(" {}: {}", field, error.message.as_ref().unwrap_or(&"".to_string()));
}
}
}
}
}
複数のバリデーションルール
use validator::Validate;
#[derive(Debug, Validate)]
struct Product {
#[validate(length(min = 1, max = 100))]
#[validate(regex = "PRODUCT_NAME_REGEX")]
name: String,
#[validate(range(min = 0.01, max = 99999.99))]
price: f64,
#[validate(length(max = 500))]
description: Option<String>,
#[validate(url)]
product_url: Option<String>,
}
// 正規表現パターンを定義
lazy_static::lazy_static! {
static ref PRODUCT_NAME_REGEX: regex::Regex = regex::Regex::new(r"^[a-zA-Z0-9\s\-_]+$").unwrap();
}
組み込みバリデータ
文字列バリデーション
#[derive(Validate)]
struct StringValidation {
// 長さ検証
#[validate(length(min = 5, max = 50))]
title: String,
// 長さ(等しい)
#[validate(length(equal = 10))]
code: String,
// Email検証
#[validate(email)]
email: String,
// URL検証
#[validate(url)]
website: String,
// 正規表現
#[validate(regex = "PHONE_REGEX")]
phone: String,
// カスタムメッセージ
#[validate(length(min = 8), message = "パスワードは8文字以上必要です")]
password: String,
}
lazy_static::lazy_static! {
static ref PHONE_REGEX: regex::Regex = regex::Regex::new(r"^\d{3}-\d{4}-\d{4}$").unwrap();
}
数値バリデーション
#[derive(Validate)]
struct NumberValidation {
// 範囲検証
#[validate(range(min = 0, max = 100))]
percentage: u8,
// 最小値のみ
#[validate(range(min = 0.0))]
positive_amount: f64,
// 最大値のみ
#[validate(range(max = 1000))]
limited_quantity: i32,
// カスタムメッセージ
#[validate(range(min = 18), message = "18歳以上である必要があります")]
age: u32,
}
コレクションバリデーション
#[derive(Validate)]
struct CollectionValidation {
// 配列の長さ検証
#[validate(length(min = 1, max = 10))]
tags: Vec<String>,
// 必須項目(空でない)
#[validate(required)]
required_items: Option<Vec<String>>,
// カスタムメッセージ
#[validate(length(min = 1), message = "少なくとも1つのカテゴリが必要です")]
categories: Vec<String>,
}
カスタムバリデータ
関数ベースのカスタムバリデータ
use validator::{Validate, ValidationError};
fn validate_password(password: &str) -> Result<(), ValidationError> {
let mut has_uppercase = false;
let mut has_lowercase = false;
let mut has_digit = false;
let mut has_special = false;
for c in password.chars() {
if c.is_uppercase() {
has_uppercase = true;
} else if c.is_lowercase() {
has_lowercase = true;
} else if c.is_numeric() {
has_digit = true;
} else if "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c) {
has_special = true;
}
}
if password.len() < 8 {
return Err(ValidationError::new("password_too_short"));
}
if !has_uppercase || !has_lowercase || !has_digit || !has_special {
return Err(ValidationError::new("password_complexity"));
}
Ok(())
}
fn validate_username_unique(username: &str) -> Result<(), ValidationError> {
// データベースチェックなどのロジックをここに実装
let existing_usernames = vec!["admin", "root", "test"];
if existing_usernames.contains(&username) {
return Err(ValidationError::new("username_taken"));
}
Ok(())
}
#[derive(Validate)]
struct Account {
#[validate(length(min = 3, max = 20))]
#[validate(custom = "validate_username_unique")]
username: String,
#[validate(custom = "validate_password")]
password: String,
}
バリデーション関数の引数を使用
fn validate_age_with_context(age: u32, min_age: u32) -> Result<(), ValidationError> {
if age < min_age {
return Err(ValidationError::new("age_too_young"));
}
Ok(())
}
#[derive(Validate)]
struct UserRegistration {
name: String,
#[validate(custom(function = "validate_age_with_context", arg = "18"))]
age: u32,
}
条件付きバリデーション
#[derive(Validate)]
struct ConditionalValidation {
user_type: String,
// user_type が "premium" の場合のみ検証
#[validate(length(min = 10), custom = "validate_premium_code")]
premium_code: Option<String>,
// 条件付き必須フィールド
#[validate(required_if = "user_type == 'business'")]
company_name: Option<String>,
}
fn validate_premium_code(code: &str) -> Result<(), ValidationError> {
if !code.starts_with("PREM") {
return Err(ValidationError::new("invalid_premium_code"));
}
Ok(())
}
ネストした構造体のバリデーション
#[derive(Validate)]
struct Address {
#[validate(length(min = 1, max = 100))]
street: String,
#[validate(length(min = 1, max = 50))]
city: String,
#[validate(regex = "POSTAL_CODE_REGEX")]
postal_code: String,
#[validate(length(equal = 2))]
country_code: String,
}
#[derive(Validate)]
struct Customer {
#[validate(length(min = 1, max = 50))]
name: String,
#[validate(email)]
email: String,
// ネストした構造体のバリデーション
#[validate]
address: Address,
// オプショナルなネスト
#[validate]
billing_address: Option<Address>,
}
lazy_static::lazy_static! {
static ref POSTAL_CODE_REGEX: regex::Regex = regex::Regex::new(r"^\d{3}-\d{4}$").unwrap();
}
エラーハンドリング
詳細なエラー情報の取得
use validator::{Validate, ValidationErrors};
use std::collections::HashMap;
fn validate_and_format_errors<T: Validate>(data: T) -> Result<T, HashMap<String, Vec<String>>> {
match data.validate() {
Ok(()) => Ok(data),
Err(errors) => {
let mut error_map = HashMap::new();
for (field, field_errors) in errors.field_errors() {
let messages: Vec<String> = field_errors
.iter()
.map(|error| {
error.message
.as_ref()
.map(|m| m.to_string())
.unwrap_or_else(|| format!("バリデーションエラー: {}", error.code))
})
.collect();
error_map.insert(field.to_string(), messages);
}
Err(error_map)
}
}
}
// 使用例
fn example_error_handling() {
let user = User {
username: "ab".to_string(), // 短すぎる
email: "invalid-email".to_string(), // 無効なEmail
age: 200, // 範囲外
};
match validate_and_format_errors(user) {
Ok(valid_user) => println!("バリデーション成功: {:?}", valid_user),
Err(errors) => {
println!("バリデーションエラー:");
for (field, messages) in errors {
println!(" {}: {}", field, messages.join(", "));
}
}
}
}
カスタムエラーメッセージ
#[derive(Validate)]
struct UserWithCustomMessages {
#[validate(length(min = 3, max = 20, message = "ユーザー名は3-20文字で入力してください"))]
username: String,
#[validate(email(message = "有効なメールアドレスを入力してください"))]
email: String,
#[validate(range(min = 18, max = 150, message = "年齢は18-150歳の範囲で入力してください"))]
age: u8,
}
Serde統合
use validator::Validate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Validate)]
struct ApiRequest {
#[validate(length(min = 1, max = 100))]
title: String,
#[validate(range(min = 1, max = 1000))]
count: u32,
#[validate(email)]
contact_email: String,
#[validate(url)]
callback_url: Option<String>,
}
// JSON APIでの使用例
fn handle_api_request(json_str: &str) -> Result<ApiRequest, String> {
// JSONをデシリアライズ
let request: ApiRequest = serde_json::from_str(json_str)
.map_err(|e| format!("JSONパースエラー: {}", e))?;
// バリデーション実行
request.validate()
.map_err(|e| format!("バリデーションエラー: {:?}", e))?;
Ok(request)
}
// 使用例
fn example_api_usage() {
let json_data = r#"
{
"title": "新しい投稿",
"count": 5,
"contact_email": "[email protected]",
"callback_url": "https://example.com/callback"
}
"#;
match handle_api_request(json_data) {
Ok(request) => println!("有効なリクエスト: {:?}", request),
Err(error) => println!("エラー: {}", error),
}
}
Webフレームワークとの統合
Axumとの統合
use axum::{
extract::Json,
http::StatusCode,
response::{IntoResponse, Response},
routing::post,
Router,
};
use validator::Validate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Validate)]
struct CreateUserRequest {
#[validate(length(min = 3, max = 20))]
username: String,
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
#[derive(Serialize)]
struct CreateUserResponse {
id: u32,
username: String,
email: String,
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
details: Option<serde_json::Value>,
}
impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
let body = serde_json::to_string(&self).unwrap();
(StatusCode::BAD_REQUEST, body).into_response()
}
}
async fn create_user(Json(payload): Json<CreateUserRequest>) -> Result<Json<CreateUserResponse>, ErrorResponse> {
// バリデーション実行
if let Err(errors) = payload.validate() {
let error_details = serde_json::to_value(&errors).unwrap();
return Err(ErrorResponse {
error: "バリデーションエラー".to_string(),
details: Some(error_details),
});
}
// ユーザー作成ロジック(省略)
let user = CreateUserResponse {
id: 1,
username: payload.username,
email: payload.email,
};
Ok(Json(user))
}
fn app() -> Router {
Router::new()
.route("/users", post(create_user))
}
Actix Webとの統合
use actix_web::{web, App, HttpResponse, HttpServer, Result};
use validator::Validate;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Validate)]
struct UserForm {
#[validate(length(min = 3, max = 20))]
username: String,
#[validate(email)]
email: String,
}
async fn create_user(form: web::Json<UserForm>) -> Result<HttpResponse> {
match form.validate() {
Ok(()) => {
// ユーザー作成処理
Ok(HttpResponse::Ok().json(&*form))
}
Err(errors) => {
Ok(HttpResponse::BadRequest().json(serde_json::json!({
"error": "バリデーションエラー",
"details": errors
})))
}
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/users", web::post().to(create_user))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
高度な使用例
複雑なビジネスルール
use validator::{Validate, ValidationError};
use chrono::{NaiveDate, Utc};
#[derive(Validate)]
struct EventRegistration {
#[validate(length(min = 1, max = 100))]
event_name: String,
#[validate(custom = "validate_future_date")]
event_date: NaiveDate,
#[validate(range(min = 1, max = 1000))]
max_attendees: u32,
#[validate(range(min = 0.0))]
ticket_price: f64,
#[validate(custom = "validate_price_and_attendees")]
early_bird_discount: Option<f64>,
}
fn validate_future_date(date: &NaiveDate) -> Result<(), ValidationError> {
let today = Utc::now().naive_utc().date();
if *date <= today {
return Err(ValidationError::new("event_date_past"));
}
// 1年以上先のイベントは登録できない
let one_year_later = today + chrono::Duration::days(365);
if *date > one_year_later {
return Err(ValidationError::new("event_date_too_far"));
}
Ok(())
}
fn validate_price_and_attendees(registration: &EventRegistration) -> Result<(), ValidationError> {
if let Some(discount) = registration.early_bird_discount {
if discount >= registration.ticket_price {
return Err(ValidationError::new("discount_too_high"));
}
if registration.max_attendees < 10 && discount > 0.0 {
return Err(ValidationError::new("small_event_no_discount"));
}
}
Ok(())
}
国際化(i18n)サポート
use validator::{Validate, ValidationError};
use std::collections::HashMap;
// エラーメッセージの国際化
fn get_localized_message(code: &str, lang: &str) -> String {
let mut messages = HashMap::new();
// 日本語メッセージ
messages.insert(("username_too_short", "ja"), "ユーザー名が短すぎます");
messages.insert(("username_too_long", "ja"), "ユーザー名が長すぎます");
messages.insert(("invalid_email", "ja"), "無効なメールアドレスです");
// 英語メッセージ
messages.insert(("username_too_short", "en"), "Username is too short");
messages.insert(("username_too_long", "en"), "Username is too long");
messages.insert(("invalid_email", "en"), "Invalid email address");
messages.get(&(code, lang))
.unwrap_or(&"Validation error")
.to_string()
}
fn validate_username_length(username: &str) -> Result<(), ValidationError> {
if username.len() < 3 {
return Err(ValidationError::new("username_too_short"));
}
if username.len() > 20 {
return Err(ValidationError::new("username_too_long"));
}
Ok(())
}
#[derive(Validate)]
struct LocalizedUser {
#[validate(custom = "validate_username_length")]
username: String,
#[validate(email(code = "invalid_email"))]
email: String,
}
// ローカライズされたエラーメッセージを生成
fn validate_with_localization<T: Validate>(data: T, lang: &str) -> Result<T, Vec<String>> {
match data.validate() {
Ok(()) => Ok(data),
Err(errors) => {
let mut localized_errors = Vec::new();
for (_, field_errors) in errors.field_errors() {
for error in field_errors {
let message = get_localized_message(&error.code, lang);
localized_errors.push(message);
}
}
Err(localized_errors)
}
}
}
パフォーマンス考慮事項
use validator::Validate;
use once_cell::sync::Lazy;
use regex::Regex;
// 正規表現のキャッシュ
static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()
});
static PHONE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\+?[1-9]\d{1,14}$").unwrap()
});
#[derive(Validate)]
struct OptimizedValidation {
// 事前にコンパイルされた正規表現を使用
#[validate(regex = "EMAIL_REGEX")]
email: String,
#[validate(regex = "PHONE_REGEX")]
phone: String,
// シンプルな検証は組み込みバリデータを使用
#[validate(length(min = 3, max = 50))]
name: String,
#[validate(range(min = 0, max = 150))]
age: u8,
}
// バリデーション結果のキャッシュ(必要に応じて)
use std::sync::Mutex;
static VALIDATION_CACHE: Lazy<Mutex<std::collections::HashMap<String, bool>>> =
Lazy::new(|| Mutex::new(std::collections::HashMap::new()));
fn validate_with_cache(input: &str) -> bool {
let mut cache = VALIDATION_CACHE.lock().unwrap();
if let Some(&result) = cache.get(input) {
return result;
}
// 実際のバリデーション処理
let result = input.len() >= 3 && input.len() <= 20;
cache.insert(input.to_string(), result);
result
}
まとめ
validatorは、Rustにおける構造体バリデーションの標準的なライブラリとして、シンプルな使いやすさと強力な機能を両立しています。derive マクロによる宣言的な定義、豊富な組み込みバリデータ、柔軟なカスタム検証機能により、Web APIからデータ処理まで幅広い用途で活用できます。Serdeとの優れた統合により、JSONやその他の形式のデータ検証も容易に実装できます。