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やその他の形式のデータ検証も容易に実装できます。