valico
ライブラリ
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(())
}