valico

バリデーションライブラリRustJSON SchemaJSON検証RESTフレームワーク型変換DSL

ライブラリ

valico

概要

valicoは、Rust向けのJSON検証・型変換ライブラリです。RESTフレームワークや外部からのJSON入力を処理するツールのサポートライブラリとして設計されており、JSONオブジェクトの検証と型変換を効率的に行います。2つの主要機能として、シンプルなDSL(Domain Specific Language)によるバリデーターとJSON Schema Draft v7の実装を提供し、詳細なエラーメッセージの生成とJSON値の直接変更によるデータ変換を特徴としています。

詳細

valicoは、RustエコシステムにおけるJSON処理の課題を解決するために開発された包括的なバリデーションライブラリです。Grapeライブラリからインスピレーションを得たDSLアプローチを採用し、宣言的で読みやすい検証ルールの定義を可能にします。JSON Schema Draft v7の完全実装により、標準準拠のスキーマベース検証も提供。内部的には型安全性を重視した設計により、コンパイル時エラー検出とランタイムパフォーマンスの両立を実現。JSON-Schema-Test-Suiteのほぼ全てのテストケースをパスし、産業標準レベルの品質を保証しています。

主な特徴

  • DSLベースの検証: 直感的で読みやすい検証ルール定義
  • JSON Schema v7対応: 標準準拠のスキーマベース検証機能
  • 型変換機能: 文字列から数値など、自動的なデータ型変換
  • ネストした構造対応: 制限のない階層レベルでの複雑なオブジェクト処理
  • 詳細エラーレポート: 具体的で理解しやすいエラーメッセージ
  • RESTフレームワーク統合: WebAPIでの入力検証に最適化

メリット・デメリット

メリット

  • 宣言的なDSLによる読みやすい検証コード
  • JSON Schema標準への準拠で相互運用性が高い
  • 型変換と検証を同時に実行可能
  • 詳細で具体的なエラーメッセージの提供
  • ネストした複雑なデータ構造への対応
  • Rustの型安全性を活かした堅牢な実装
  • RESTful APIでの入力検証に特化した設計

デメリット

  • メンテナンスが停滞気味で最新版のアップデートが少ない
  • 他のRustバリデーションライブラリと比較して機能が限定的
  • ドキュメントが限られており学習コストが高い
  • Rustの所有権システムの理解が必要
  • 大規模なスキーマでのパフォーマンス課題の可能性
  • コミュニティサポートが少ない

参考ページ

書き方の例

Cargoでの依存関係設定

# Cargo.toml
[dependencies]
valico = "4.0"
serde_json = "1.0"

# 追加の依存関係(必要に応じて)
serde = { version = "1.0", features = ["derive"] }

DSLを使用した基本的な検証

extern crate valico;
extern crate serde_json;

use valico::json_dsl;
use serde_json::{from_str, to_string_pretty};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // DSLでパラメータ検証ルールを定義
    let params = json_dsl::Builder::build(|params| {
        // 必須フィールドの定義
        params.req_typed("name", json_dsl::string());
        params.req_typed("age", json_dsl::u64());
        params.req_typed("email", json_dsl::string());
        
        // オプショナルフィールド
        params.opt_typed("bio", json_dsl::string());
        params.opt_typed("active", json_dsl::boolean());
        
        // 配列フィールド
        params.opt_typed("tags", json_dsl::array_of(json_dsl::string()));
    });

    // 検証対象のJSONデータ
    let mut valid_data = from_str(r#"
    {
        "name": "田中太郎",
        "age": "30",
        "email": "[email protected]",
        "tags": ["developer", "rust"]
    }
    "#)?;

    // 検証と型変換を実行
    let state = params.process(&mut valid_data, &None);
    
    if state.is_valid() {
        println!("✓ 検証成功:");
        println!("{}", to_string_pretty(&valid_data)?);
    } else {
        println!("✗ 検証エラー:");
        for error in state.errors {
            println!("  - {}", error);
        }
    }

    // 無効なデータのテスト
    let mut invalid_data = from_str(r#"
    {
        "name": "",
        "email": "invalid-email"
    }
    "#)?;

    let state = params.process(&mut invalid_data, &None);
    if !state.is_valid() {
        println!("\n無効データの検証エラー:");
        for error in state.errors {
            println!("  - パス: {}, メッセージ: {}", error.get_path(), error.get_detail());
        }
    }

    Ok(())
}

ネストしたオブジェクトの検証

use valico::json_dsl;
use serde_json::from_str;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 複雑なネスト構造の検証ルール定義
    let params = json_dsl::Builder::build(|params| {
        // ユーザー情報のネストオブジェクト
        params.req_nested("user", json_dsl::object(), |user_params| {
            user_params.req_typed("id", json_dsl::u64());
            user_params.req_typed("name", json_dsl::string());
            
            // プロフィール情報のさらなるネスト
            user_params.opt_nested("profile", json_dsl::object(), |profile_params| {
                profile_params.opt_typed("bio", json_dsl::string());
                profile_params.opt_typed("website", json_dsl::string());
                profile_params.opt_typed("location", json_dsl::string());
            });

            // 友達IDの配列
            user_params.opt_typed("friend_ids", json_dsl::array_of(json_dsl::u64()));
        });

        // 設定情報
        params.opt_nested("settings", json_dsl::object(), |settings_params| {
            settings_params.opt_typed("theme", json_dsl::string());
            settings_params.opt_typed("notifications", json_dsl::boolean());
            settings_params.opt_typed("language", json_dsl::string());
        });
    });

    let mut data = from_str(r#"
    {
        "user": {
            "id": "123",
            "name": "山田花子",
            "profile": {
                "bio": "Rustプログラマー",
                "website": "https://example.com",
                "location": "東京"
            },
            "friend_ids": ["456", "789", "101112"]
        },
        "settings": {
            "theme": "dark",
            "notifications": true,
            "language": "ja"
        }
    }
    "#)?;

    let state = params.process(&mut data, &None);
    
    if state.is_valid() {
        println!("✓ ネストオブジェクト検証成功");
        println!("処理後のデータ: {}", serde_json::to_string_pretty(&data)?);
    } else {
        println!("✗ 検証エラー:");
        for error in state.errors {
            println!("  - {}", error);
        }
    }

    Ok(())
}

JSON Schemaを使用した検証

extern crate serde_json;
extern crate valico;

use serde_json::{from_str, Value};
use valico::json_schema;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // JSON Schemaの定義
    let schema_json = from_str::<Value>(r#"
    {
        "$schema": "https://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
            },
            "age": {
                "type": "integer",
                "minimum": 0,
                "maximum": 150
            },
            "email": {
                "type": "string",
                "format": "email"
            },
            "address": {
                "type": "object",
                "properties": {
                    "street": { "type": "string" },
                    "city": { "type": "string" },
                    "zipcode": {
                        "type": "string",
                        "pattern": "^[0-9]{3}-[0-9]{4}$"
                    }
                },
                "required": ["street", "city", "zipcode"]
            },
            "hobbies": {
                "type": "array",
                "items": { "type": "string" },
                "maxItems": 10
            }
        },
        "required": ["name", "age", "email"],
        "additionalProperties": false
    }
    "#)?;

    // スキーマのコンパイル
    let mut scope = json_schema::Scope::new();
    let schema = scope.compile_and_return(schema_json, false)?;

    // 有効なデータのテスト
    let valid_data = from_str::<Value>(r#"
    {
        "name": "佐藤次郎",
        "age": 28,
        "email": "[email protected]",
        "address": {
            "street": "新宿区西新宿1-1-1",
            "city": "東京",
            "zipcode": "160-0023"
        },
        "hobbies": ["プログラミング", "読書", "映画鑑賞"]
    }
    "#)?;

    let validation_result = schema.validate(&valid_data);
    if validation_result.is_valid() {
        println!("✓ JSON Schema検証成功");
    } else {
        println!("✗ JSON Schema検証エラー:");
        for error in validation_result.errors {
            println!("  - パス: {}, エラー: {}", 
                error.get_path(), error.get_detail());
        }
    }

    // 無効なデータのテスト
    let invalid_data = from_str::<Value>(r#"
    {
        "name": "",
        "age": -5,
        "email": "invalid-email",
        "address": {
            "street": "test street",
            "city": "test city",
            "zipcode": "invalid-zip"
        },
        "hobbies": ["hobby1", "hobby2", "hobby3", "hobby4", "hobby5", 
                   "hobby6", "hobby7", "hobby8", "hobby9", "hobby10", 
                   "hobby11", "hobby12"]
    }
    "#)?;

    let validation_result = schema.validate(&invalid_data);
    if !validation_result.is_valid() {
        println!("\n無効データの検証結果:");
        for (i, error) in validation_result.errors.iter().enumerate() {
            println!("  {}. パス: {}", i + 1, error.get_path());
            println!("     エラー: {}", error.get_detail());
        }
    }

    Ok(())
}

カスタムバリデーターと型変換

use valico::json_dsl;
use serde_json::{from_str, Value};

// カスタムバリデーション関数
fn validate_japanese_name(value: &Value) -> Result<(), String> {
    if let Some(name) = value.as_str() {
        // 日本語文字(ひらがな、カタカナ、漢字)の存在チェック
        let has_japanese = name.chars().any(|c| {
            matches!(c,
                '\u{3040}'..='\u{309F}' |  // ひらがな
                '\u{30A0}'..='\u{30FF}' |  // カタカナ
                '\u{4E00}'..='\u{9FAF}'    // 漢字
            )
        });

        if !has_japanese {
            return Err("日本語文字(ひらがな・カタカナ・漢字)を含む必要があります".to_string());
        }

        if name.trim().is_empty() {
            return Err("名前は空にできません".to_string());
        }

        if name.chars().count() > 50 {
            return Err("名前は50文字以下である必要があります".to_string());
        }

        Ok(())
    } else {
        Err("文字列である必要があります".to_string())
    }
}

fn validate_phone_number(value: &Value) -> Result<(), String> {
    if let Some(phone) = value.as_str() {
        // 日本の電話番号形式の簡単なチェック
        let cleaned = phone.replace("-", "").replace(" ", "");
        
        if cleaned.len() < 10 || cleaned.len() > 11 {
            return Err("電話番号は10-11桁である必要があります".to_string());
        }

        if !cleaned.chars().all(|c| c.is_ascii_digit()) {
            return Err("電話番号は数字のみ含む必要があります".to_string());
        }

        Ok(())
    } else {
        Err("文字列である必要があります".to_string())
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let params = json_dsl::Builder::build(|params| {
        // 日本語名前のカスタムバリデーション
        params.req("name", |name| {
            name.coerce(json_dsl::string());
            name.validate(validate_japanese_name);
        });

        // 年齢(文字列から数値に変換)
        params.req("age", |age| {
            age.coerce(json_dsl::u64());
            age.validate(|value| {
                if let Some(age_val) = value.as_u64() {
                    if age_val > 150 {
                        Err("年齢は150歳以下である必要があります".to_string())
                    } else if age_val < 1 {
                        Err("年齢は1歳以上である必要があります".to_string())
                    } else {
                        Ok(())
                    }
                } else {
                    Err("有効な年齢を入力してください".to_string())
                }
            });
        });

        // 電話番号のカスタムバリデーション
        params.opt("phone", |phone| {
            phone.coerce(json_dsl::string());
            phone.validate(validate_phone_number);
        });

        // 配列の各要素に対するバリデーション
        params.opt("skills", |skills| {
            skills.coerce(json_dsl::array());
            skills.validate(|value| {
                if let Some(array) = value.as_array() {
                    if array.len() > 20 {
                        return Err("スキルは20個まで設定可能です".to_string());
                    }
                    
                    for skill in array {
                        if let Some(skill_str) = skill.as_str() {
                            if skill_str.trim().is_empty() {
                                return Err("空のスキルは許可されていません".to_string());
                            }
                        } else {
                            return Err("スキルは文字列である必要があります".to_string());
                        }
                    }
                    Ok(())
                } else {
                    Err("配列である必要があります".to_string())
                }
            });
        });
    });

    // テストデータ
    let mut test_data = from_str(r#"
    {
        "name": "田中太郎",
        "age": "30",
        "phone": "090-1234-5678",
        "skills": ["Rust", "JavaScript", "Python", ""]
    }
    "#)?;

    let state = params.process(&mut test_data, &None);
    
    if state.is_valid() {
        println!("✓ カスタムバリデーション成功");
        println!("処理後データ: {}", serde_json::to_string_pretty(&test_data)?);
    } else {
        println!("✗ カスタムバリデーションエラー:");
        for error in state.errors {
            println!("  - {}", error);
        }
    }

    Ok(())
}

RESTful APIでの実用例

use valico::json_dsl;
use serde_json::{from_str, Value};
use std::collections::HashMap;

// API リクエストの検証とレスポンス構築
fn process_user_registration(request_json: &str) -> Result<Value, Vec<String>> {
    // ユーザー登録APIの検証ルール
    let params = json_dsl::Builder::build(|params| {
        params.req_typed("username", json_dsl::string());
        params.req_typed("email", json_dsl::string());
        params.req_typed("password", json_dsl::string());
        
        // プロフィール情報(オプション)
        params.opt_nested("profile", json_dsl::object(), |profile| {
            profile.opt_typed("first_name", json_dsl::string());
            profile.opt_typed("last_name", json_dsl::string());
            profile.opt_typed("bio", json_dsl::string());
            profile.opt_typed("birth_date", json_dsl::string());
        });

        // 設定(デフォルト値付き)
        params.opt_nested("preferences", json_dsl::object(), |prefs| {
            prefs.opt_typed("language", json_dsl::string());
            prefs.opt_typed("timezone", json_dsl::string());
            prefs.opt_typed("notifications", json_dsl::boolean());
        });
    });

    // JSONパース
    let mut data = from_str(request_json)
        .map_err(|e| vec![format!("JSON解析エラー: {}", e)])?;

    // バリデーション実行
    let state = params.process(&mut data, &None);
    
    if !state.is_valid() {
        let errors: Vec<String> = state.errors
            .into_iter()
            .map(|e| format!("{}: {}", e.get_path(), e.get_detail()))
            .collect();
        return Err(errors);
    }

    // デフォルト値の設定
    if let Some(obj) = data.as_object_mut() {
        // preferences が存在しない場合はデフォルト値を設定
        if !obj.contains_key("preferences") {
            obj.insert("preferences".to_string(), serde_json::json!({
                "language": "ja",
                "timezone": "Asia/Tokyo",
                "notifications": true
            }));
        }

        // IDの生成(実際の実装では UUID など使用)
        obj.insert("id".to_string(), Value::String(format!("user_{}", 
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs())));

        // タイムスタンプの追加
        obj.insert("created_at".to_string(), 
            Value::String(chrono::Utc::now().to_rfc3339()));
        obj.insert("updated_at".to_string(), 
            Value::String(chrono::Utc::now().to_rfc3339()));
    }

    Ok(data)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== RESTful API ユーザー登録例 ===");

    // 有効なリクエスト
    let valid_request = r#"
    {
        "username": "yamada_hanako",
        "email": "[email protected]",
        "password": "secure_password123",
        "profile": {
            "first_name": "花子",
            "last_name": "山田",
            "bio": "Rustエンジニア"
        }
    }
    "#;

    match process_user_registration(valid_request) {
        Ok(user_data) => {
            println!("✓ ユーザー登録成功:");
            println!("{}", serde_json::to_string_pretty(&user_data)?);
        }
        Err(errors) => {
            println!("✗ 登録エラー:");
            for error in errors {
                println!("  - {}", error);
            }
        }
    }

    // 無効なリクエスト
    println!("\n=== 無効リクエストのテスト ===");
    let invalid_request = r#"
    {
        "username": "",
        "email": "invalid-email",
        "profile": {
            "first_name": "田中"
        }
    }
    "#;

    match process_user_registration(invalid_request) {
        Ok(_) => println!("予期しない成功"),
        Err(errors) => {
            println!("期待される検証エラー:");
            for error in errors {
                println!("  - {}", error);
            }
        }
    }

    Ok(())
}

パフォーマンス最適化とベンチマーク

use valico::json_dsl;
use serde_json::{from_str, Value};
use std::time::{Duration, Instant};

fn create_user_validator() -> json_dsl::Builder {
    json_dsl::Builder::build(|params| {
        params.req_typed("id", json_dsl::u64());
        params.req_typed("name", json_dsl::string());
        params.req_typed("email", json_dsl::string());
        params.opt_typed("age", json_dsl::u64());
        params.opt_nested("profile", json_dsl::object(), |profile| {
            profile.opt_typed("bio", json_dsl::string());
            profile.opt_typed("location", json_dsl::string());
            profile.opt_typed("skills", json_dsl::array_of(json_dsl::string()));
        });
    })
}

fn benchmark_validation(
    validator: &json_dsl::Builder,
    test_data: &[String],
    iterations: usize
) -> Duration {
    let start = Instant::now();
    
    for _ in 0..iterations {
        for json_str in test_data {
            if let Ok(mut data) = from_str::<Value>(json_str) {
                let _ = validator.process(&mut data, &None);
            }
        }
    }
    
    start.elapsed()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== Valico パフォーマンステスト ===");

    // バリデーター作成
    let validator = create_user_validator();

    // テストデータ生成
    let test_data: Vec<String> = (0..1000)
        .map(|i| {
            format!(r#"
            {{
                "id": {},
                "name": "ユーザー{}",
                "email": "user{}@example.com",
                "age": {},
                "profile": {{
                    "bio": "プロフィール{}",
                    "location": "東京",
                    "skills": ["Rust", "JSON", "API"]
                }}
            }}
            "#, i, i, i, 20 + (i % 50), i)
        })
        .collect();

    println!("テストデータ: {} 件", test_data.len());

    // ウォームアップ
    let _ = benchmark_validation(&validator, &test_data[..10], 1);

    // ベンチマーク実行
    let iterations = 10;
    let duration = benchmark_validation(&validator, &test_data, iterations);
    
    let total_validations = test_data.len() * iterations;
    let avg_time_per_validation = duration / total_validations as u32;
    let validations_per_second = 1_000_000_000.0 / avg_time_per_validation.as_nanos() as f64;

    println!("実行時間: {:?}", duration);
    println!("総検証回数: {}", total_validations);
    println!("平均検証時間: {:?}", avg_time_per_validation);
    println!("処理速度: {:.0} validations/sec", validations_per_second);

    // メモリ使用量のテスト
    println!("\n=== メモリ効率テスト ===");
    let mut memory_test_data = from_str::<Value>(&test_data[0])?;
    
    let start = Instant::now();
    for _ in 0..10000 {
        let state = validator.process(&mut memory_test_data, &None);
        if !state.is_valid() {
            break;
        }
    }
    let memory_test_duration = start.elapsed();
    
    println!("メモリ再利用テスト時間: {:?}", memory_test_duration);
    println!("検証結果: {}", 
        serde_json::to_string_pretty(&memory_test_data)?);

    Ok(())
}