jsonschema

バリデーションライブラリRustJSON Schemaパフォーマンス並行処理ドラフト対応

GitHub概要

Stranger6667/jsonschema

A high-performance JSON Schema validator for Rust

スター682
ウォッチ7
フォーク112
作成日:2020年3月7日
言語:Rust
ライセンス:MIT License

トピックス

jsonschemapythonrustvalidationwebassembly

スター履歴

Stranger6667/jsonschema Star History
データ取得日時: 2025/10/22 09:50

ライブラリ

jsonschema

概要

jsonschemaは、Rust向けの高性能JSON Schemaバリデーションライブラリです。JSON Schema Draft 4、6、7、2019-09、2020-12の全メジャーバージョンをサポートし、Rustの安全性とパフォーマンスを活かした高速なバリデーション処理を提供します。2025年現在、RustエコシステムにおけるJSON Schemaバリデーションのデファクトスタンダードとして広く採用されており、並行処理への対応とゼロコスト抽象化により、大規模なデータ処理においても優れたパフォーマンスを発揮します。

詳細

jsonschema-rsは、JSON Schema仕様への準拠と実行時パフォーマンスの両立を重視して設計されたライブラリです。Rustの所有権システムとコンパイル時最適化を活用し、メモリ安全性を保ちながら高速なバリデーション処理を実現。内部的にはserde_jsonとの密な統合により、JSON解析からバリデーションまでの一連の処理を効率的に行います。1k+のGitHubスターを獲得し、async/await対応、カスタムキーワード拡張、詳細なエラーレポート機能など、現代的なRust開発における要求を満たす包括的な機能セットを提供します。

主な特徴

  • 全Draft対応: JSON Schema Draft 4/6/7/2019-09/2020-12の完全サポート
  • ゼロコスト抽象化: Rustの特性を活かした高性能バリデーション実行
  • 並行処理対応: Send + Syncトレイト実装による安全なマルチスレッド処理
  • カスタムキーワード: 独自バリデーションルールの柔軟な拡張機能
  • 詳細エラー情報: デバッグに有用な構造化エラーレポート
  • serde統合: Rustエコシステムとの優れた相互運用性

メリット・デメリット

メリット

  • Rustの安全性とパフォーマンスを両立した高速処理
  • JSON Schema標準への完全準拠で相互運用性が高い
  • ゼロコスト抽象化によるランタイムオーバーヘッドの最小化
  • 強力な型システムによるコンパイル時エラー検出
  • メモリ安全性が保証された並行処理対応
  • Cargoエコシステムとの優れた統合
  • 詳細で構造化されたエラーメッセージ

デメリット

  • Rust言語の学習コストが必要
  • JSON Schema仕様の理解が前提
  • 他言語と比較してコンパイル時間が長い
  • 動的なスキーマ変更への対応が複雑
  • デバッグ時の型情報の複雑性
  • カスタムキーワード実装の学習コスト

参考ページ

書き方の例

Cargoでの依存関係設定とセットアップ

# Cargo.toml
[dependencies]
jsonschema = "0.18"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }

# 非同期処理対応の場合
tokio = { version = "1.0", features = ["full"] }

# エラーハンドリング強化
anyhow = "1.0"
thiserror = "1.0"

基本的なスキーマ定義とバリデーション

use jsonschema::{JSONSchema, Draft};
use serde_json::{json, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 基本的なJSON Schemaの定義
    let schema_value = json!({
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
            "name": { 
                "type": "string",
                "minLength": 1,
                "maxLength": 100
            },
            "age": { 
                "type": "integer",
                "minimum": 0,
                "maximum": 150
            },
            "email": { 
                "type": "string",
                "format": "email"
            },
            "active": { 
                "type": "boolean"
            }
        },
        "required": ["name", "age"],
        "additionalProperties": false
    });

    // スキーマのコンパイル
    let schema = JSONSchema::compile(&schema_value)?;

    // バリデーション対象のデータ
    let valid_data = json!({
        "name": "田中太郎",
        "age": 30,
        "email": "[email protected]",
        "active": true
    });

    let invalid_data = json!({
        "name": "",  // 最小長違反
        "age": -5,   // 最小値違反
        "email": "invalid-email"  // フォーマット違反
    });

    // バリデーション実行
    if schema.is_valid(&valid_data) {
        println!("✓ 有効なデータです");
    } else {
        println!("✗ 無効なデータです");
    }

    // 詳細なエラー情報の取得
    if let Err(errors) = schema.validate(&invalid_data) {
        println!("バリデーションエラー:");
        for error in errors {
            println!("  - パス: {}", error.instance_path);
            println!("    メッセージ: {}", error);
            println!("    スキーマパス: {}", error.schema_path);
        }
    }

    // Draft指定でのスキーマコンパイル
    let draft7_schema = JSONSchema::options()
        .with_draft(Draft::Draft7)
        .compile(&schema_value)?;

    Ok(())
}

カスタムキーワードとバリデーター拡張

use jsonschema::{JSONSchema, Keyword, SchemaNode, ValidationError};
use serde_json::{json, Value};
use std::collections::HashMap;

// カスタムキーワード: 偶数チェック
#[derive(Debug)]
struct EvenKeyword;

impl Keyword for EvenKeyword {
    fn validate(
        &self,
        _schema: &SchemaNode,
        instance: &Value,
        instance_path: &str,
    ) -> Result<(), ValidationError> {
        if let Some(number) = instance.as_i64() {
            if number % 2 != 0 {
                return Err(ValidationError::custom(
                    instance_path.to_string(),
                    "数値は偶数である必要があります".to_string(),
                ));
            }
        }
        Ok(())
    }
}

// カスタム日本語バリデーター
#[derive(Debug)]
struct JapaneseTextKeyword {
    max_length: usize,
}

impl Keyword for JapaneseTextKeyword {
    fn validate(
        &self,
        _schema: &SchemaNode,
        instance: &Value,
        instance_path: &str,
    ) -> Result<(), ValidationError> {
        if let Some(text) = instance.as_str() {
            // 日本語文字数カウント(バイト数ではなく文字数)
            let char_count = text.chars().count();
            
            if char_count > self.max_length {
                return Err(ValidationError::custom(
                    instance_path.to_string(),
                    format!("日本語テキストは{}文字以下である必要があります", self.max_length),
                ));
            }

            // ひらがな・カタカナ・漢字の存在チェック
            let has_japanese = text.chars().any(|c| {
                matches!(c,
                    '\u{3040}'..='\u{309F}' |  // ひらがな
                    '\u{30A0}'..='\u{30FF}' |  // カタカナ
                    '\u{4E00}'..='\u{9FAF}'    // 漢字
                )
            });

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // カスタムキーワードを使用したスキーマ
    let schema_with_custom = json!({
        "type": "object",
        "properties": {
            "even_number": {
                "type": "integer",
                "x-even": true  // カスタムキーワード
            },
            "japanese_name": {
                "type": "string",
                "x-japanese-text": { "max_length": 50 }
            }
        }
    });

    // カスタムキーワードファクトリーの定義
    let mut keywords = HashMap::new();
    keywords.insert("x-even", |_schema_value: &Value| {
        Box::new(EvenKeyword) as Box<dyn Keyword>
    });
    keywords.insert("x-japanese-text", |schema_value: &Value| {
        let max_length = schema_value["max_length"].as_u64().unwrap_or(100) as usize;
        Box::new(JapaneseTextKeyword { max_length }) as Box<dyn Keyword>
    });

    // スキーマのコンパイル(カスタムキーワード付き)
    let schema = JSONSchema::options()
        .with_keywords(keywords)
        .compile(&schema_with_custom)?;

    // テストデータ
    let test_data = json!({
        "even_number": 4,  // 偶数 - OK
        "japanese_name": "田中太郎"  // 日本語 - OK
    });

    let invalid_data = json!({
        "even_number": 3,  // 奇数 - エラー
        "japanese_name": "Tanaka Taro"  // 英語のみ - エラー
    });

    // バリデーション実行
    match schema.validate(&test_data) {
        Ok(_) => println!("✓ カスタムバリデーション成功"),
        Err(errors) => {
            for error in errors {
                println!("✗ エラー: {}", error);
            }
        }
    }

    Ok(())
}

非同期処理と大量データのバリデーション

use jsonschema::JSONSchema;
use serde_json::{json, Value};
use tokio::task;
use std::sync::Arc;
use futures::future::join_all;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // スキーマの定義
    let schema_value = json!({
        "type": "object",
        "properties": {
            "id": { "type": "string", "format": "uuid" },
            "name": { "type": "string", "minLength": 1 },
            "score": { "type": "number", "minimum": 0, "maximum": 100 }
        },
        "required": ["id", "name", "score"]
    });

    // スキーマをArcでラップ(複数タスク間で共有)
    let schema = Arc::new(JSONSchema::compile(&schema_value)?);

    // 大量のテストデータ生成
    let test_data: Vec<Value> = (0..10000)
        .map(|i| {
            json!({
                "id": format!("user-{:04}", i),
                "name": format!("ユーザー{}", i),
                "score": (i % 101) as f64
            })
        })
        .collect();

    // データをチャンクに分割して並行処理
    let chunk_size = 1000;
    let chunks: Vec<_> = test_data.chunks(chunk_size).collect();

    println!("{}件のデータを{}個のチャンクで並行バリデーション開始", 
        test_data.len(), chunks.len());

    let start_time = std::time::Instant::now();

    // 各チャンクを並行して処理
    let tasks: Vec<_> = chunks
        .into_iter()
        .enumerate()
        .map(|(chunk_idx, chunk)| {
            let schema_clone = Arc::clone(&schema);
            let chunk_data = chunk.to_vec();
            
            task::spawn(async move {
                validate_chunk(chunk_idx, chunk_data, schema_clone).await
            })
        })
        .collect();

    // 全タスクの完了を待機
    let results = join_all(tasks).await;

    let elapsed = start_time.elapsed();
    println!("バリデーション完了: {:?}", elapsed);

    // 結果の集計
    let mut total_valid = 0;
    let mut total_invalid = 0;

    for result in results {
        match result? {
            Ok((valid, invalid)) => {
                total_valid += valid;
                total_invalid += invalid;
            }
            Err(e) => eprintln!("チャンク処理エラー: {}", e),
        }
    }

    println!("結果: 有効 {} 件, 無効 {} 件", total_valid, total_invalid);

    Ok(())
}

async fn validate_chunk(
    chunk_idx: usize,
    data: Vec<Value>,
    schema: Arc<JSONSchema>,
) -> Result<(usize, usize), Box<dyn std::error::Error + Send + Sync>> {
    let mut valid_count = 0;
    let mut invalid_count = 0;

    for (item_idx, item) in data.iter().enumerate() {
        if schema.is_valid(item) {
            valid_count += 1;
        } else {
            invalid_count += 1;
            
            // エラー詳細をログ出力(最初の10件のみ)
            if invalid_count <= 10 {
                if let Err(errors) = schema.validate(item) {
                    eprintln!("チャンク {} アイテム {}: バリデーションエラー", 
                        chunk_idx, item_idx);
                    for error in errors {
                        eprintln!("  - {}: {}", error.instance_path, error);
                    }
                }
            }
        }
    }

    println!("チャンク {} 完了: 有効 {} 件, 無効 {} 件", 
        chunk_idx, valid_count, invalid_count);

    Ok((valid_count, invalid_count))
}

エラーハンドリングとカスタムエラー型

use jsonschema::{JSONSchema, ValidationError};
use serde_json::Value;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum SchemaValidationError {
    #[error("スキーマコンパイルエラー: {0}")]
    CompilationError(String),
    
    #[error("バリデーションエラー: {errors:?}")]
    ValidationErrors { errors: Vec<ValidationErrorInfo> },
    
    #[error("JSON解析エラー: {0}")]
    JsonParseError(#[from] serde_json::Error),
    
    #[error("カスタムバリデーションエラー: {message}")]
    CustomError { message: String },
}

#[derive(Debug, Clone)]
pub struct ValidationErrorInfo {
    pub field_path: String,
    pub error_message: String,
    pub schema_path: String,
    pub failed_value: Option<Value>,
}

impl From<ValidationError> for ValidationErrorInfo {
    fn from(error: ValidationError) -> Self {
        Self {
            field_path: error.instance_path.to_string(),
            error_message: error.to_string(),
            schema_path: error.schema_path.to_string(),
            failed_value: None, // 必要に応じて実装
        }
    }
}

pub struct SchemaValidator {
    schema: JSONSchema,
}

impl SchemaValidator {
    pub fn new(schema_value: &Value) -> Result<Self, SchemaValidationError> {
        let schema = JSONSchema::compile(schema_value)
            .map_err(|e| SchemaValidationError::CompilationError(e.to_string()))?;
        
        Ok(Self { schema })
    }

    pub fn validate(&self, data: &Value) -> Result<(), SchemaValidationError> {
        match self.schema.validate(data) {
            Ok(_) => Ok(()),
            Err(errors) => {
                let error_infos: Vec<ValidationErrorInfo> = 
                    errors.map(ValidationErrorInfo::from).collect();
                
                Err(SchemaValidationError::ValidationErrors { errors: error_infos })
            }
        }
    }

    pub fn validate_with_details(&self, data: &Value) -> ValidationResult {
        if self.schema.is_valid(data) {
            ValidationResult::Valid
        } else {
            let errors: Vec<ValidationErrorInfo> = self.schema
                .validate(data)
                .unwrap_err()
                .map(ValidationErrorInfo::from)
                .collect();
            
            ValidationResult::Invalid { errors }
        }
    }
}

#[derive(Debug)]
pub enum ValidationResult {
    Valid,
    Invalid { errors: Vec<ValidationErrorInfo> },
}

fn main() -> Result<(), SchemaValidationError> {
    let schema_value = serde_json::json!({
        "type": "object",
        "properties": {
            "name": { "type": "string", "minLength": 1 },
            "email": { "type": "string", "format": "email" },
            "age": { "type": "integer", "minimum": 0 }
        },
        "required": ["name", "email"]
    });

    let validator = SchemaValidator::new(&schema_value)?;

    // 無効なデータのテスト
    let invalid_data = serde_json::json!({
        "name": "",  // 最小長違反
        "email": "invalid-email",  // フォーマット違反
        "age": -1    // 最小値違反
    });

    match validator.validate_with_details(&invalid_data) {
        ValidationResult::Valid => println!("データは有効です"),
        ValidationResult::Invalid { errors } => {
            println!("バリデーションエラーが見つかりました:");
            for (i, error) in errors.iter().enumerate() {
                println!("  {}. フィールド: {}", i + 1, error.field_path);
                println!("     エラー: {}", error.error_message);
                println!("     スキーマパス: {}", error.schema_path);
                println!();
            }
        }
    }

    // Result型での一括処理
    let test_datasets = vec![
        serde_json::json!({"name": "田中太郎", "email": "[email protected]", "age": 30}),
        serde_json::json!({"name": "", "email": "invalid"}),
        serde_json::json!({"email": "[email protected]"}), // name必須フィールド欠如
    ];

    for (i, data) in test_datasets.iter().enumerate() {
        match validator.validate(data) {
            Ok(_) => println!("データセット {}: ✓ 有効", i + 1),
            Err(SchemaValidationError::ValidationErrors { errors }) => {
                println!("データセット {}: ✗ 無効 ({} 個のエラー)", i + 1, errors.len());
                for error in errors.iter().take(3) { // 最初の3個のエラーのみ表示
                    println!("  - {}: {}", error.field_path, error.error_message);
                }
            }
            Err(e) => println!("データセット {}: システムエラー: {}", i + 1, e),
        }
    }

    Ok(())
}

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

use jsonschema::JSONSchema;
use serde_json::{json, Value};
use std::time::{Duration, Instant};
use std::sync::Arc;
use rayon::prelude::*;

pub struct PerformanceValidator {
    schema: JSONSchema,
    compiled_at: Instant,
}

impl PerformanceValidator {
    pub fn new(schema_value: &Value) -> Result<Self, Box<dyn std::error::Error>> {
        let start = Instant::now();
        let schema = JSONSchema::compile(schema_value)?;
        let compiled_at = Instant::now();
        
        println!("スキーマコンパイル時間: {:?}", compiled_at - start);
        
        Ok(Self { schema, compiled_at })
    }

    pub fn benchmark_validation(&self, data: &[Value], iterations: usize) -> BenchmarkResult {
        println!("ベンチマーク開始: {} 件のデータを {} 回繰り返し", data.len(), iterations);

        // 単一スレッド実行
        let single_start = Instant::now();
        for _ in 0..iterations {
            for item in data {
                let _ = self.schema.is_valid(item);
            }
        }
        let single_duration = single_start.elapsed();

        // 並列実行(rayon使用)
        let parallel_start = Instant::now();
        for _ in 0..iterations {
            data.par_iter().for_each(|item| {
                let _ = self.schema.is_valid(item);
            });
        }
        let parallel_duration = parallel_start.elapsed();

        BenchmarkResult {
            single_thread_duration: single_duration,
            parallel_duration,
            data_count: data.len(),
            iterations,
            speedup: single_duration.as_secs_f64() / parallel_duration.as_secs_f64(),
        }
    }

    pub fn memory_efficient_validation<I>(&self, data_iter: I) -> ValidationStats
    where
        I: Iterator<Item = Value>,
    {
        let mut stats = ValidationStats::new();
        
        for (index, item) in data_iter.enumerate() {
            let start = Instant::now();
            let is_valid = self.schema.is_valid(&item);
            let duration = start.elapsed();
            
            stats.add_result(is_valid, duration);
            
            // 1000件ごとに進捗を報告
            if (index + 1) % 1000 == 0 {
                println!("処理済み: {} 件", index + 1);
            }
        }
        
        stats
    }
}

#[derive(Debug)]
pub struct BenchmarkResult {
    pub single_thread_duration: Duration,
    pub parallel_duration: Duration,
    pub data_count: usize,
    pub iterations: usize,
    pub speedup: f64,
}

impl BenchmarkResult {
    pub fn print_summary(&self) {
        println!("\n=== ベンチマーク結果 ===");
        println!("データ件数: {}", self.data_count);
        println!("繰り返し回数: {}", self.iterations);
        println!("総処理件数: {}", self.data_count * self.iterations);
        println!("単一スレッド実行時間: {:?}", self.single_thread_duration);
        println!("並列実行時間: {:?}", self.parallel_duration);
        println!("高速化倍率: {:.2}x", self.speedup);
        
        let single_ops_per_sec = (self.data_count * self.iterations) as f64 
            / self.single_thread_duration.as_secs_f64();
        let parallel_ops_per_sec = (self.data_count * self.iterations) as f64 
            / self.parallel_duration.as_secs_f64();
            
        println!("単一スレッド処理速度: {:.0} ops/sec", single_ops_per_sec);
        println!("並列処理速度: {:.0} ops/sec", parallel_ops_per_sec);
    }
}

#[derive(Debug)]
pub struct ValidationStats {
    pub valid_count: usize,
    pub invalid_count: usize,
    pub total_duration: Duration,
    pub min_duration: Option<Duration>,
    pub max_duration: Option<Duration>,
}

impl ValidationStats {
    pub fn new() -> Self {
        Self {
            valid_count: 0,
            invalid_count: 0,
            total_duration: Duration::ZERO,
            min_duration: None,
            max_duration: None,
        }
    }

    pub fn add_result(&mut self, is_valid: bool, duration: Duration) {
        if is_valid {
            self.valid_count += 1;
        } else {
            self.invalid_count += 1;
        }
        
        self.total_duration += duration;
        
        self.min_duration = Some(match self.min_duration {
            None => duration,
            Some(min) => min.min(duration),
        });
        
        self.max_duration = Some(match self.max_duration {
            None => duration,
            Some(max) => max.max(duration),
        });
    }

    pub fn print_summary(&self) {
        let total_count = self.valid_count + self.invalid_count;
        if total_count == 0 {
            println!("処理されたデータがありません");
            return;
        }

        println!("\n=== バリデーション統計 ===");
        println!("総処理件数: {}", total_count);
        println!("有効件数: {} ({:.1}%)", self.valid_count, 
            self.valid_count as f64 / total_count as f64 * 100.0);
        println!("無効件数: {} ({:.1}%)", self.invalid_count,
            self.invalid_count as f64 / total_count as f64 * 100.0);
        println!("総実行時間: {:?}", self.total_duration);
        println!("平均実行時間: {:?}", self.total_duration / total_count as u32);
        
        if let Some(min) = self.min_duration {
            println!("最短実行時間: {:?}", min);
        }
        if let Some(max) = self.max_duration {
            println!("最長実行時間: {:?}", max);
        }
        
        let throughput = total_count as f64 / self.total_duration.as_secs_f64();
        println!("処理速度: {:.0} validations/sec", throughput);
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 複雑なスキーマでのベンチマーク
    let schema = json!({
        "type": "object",
        "properties": {
            "user": {
                "type": "object",
                "properties": {
                    "id": { "type": "string", "pattern": "^[a-zA-Z0-9-]+$" },
                    "profile": {
                        "type": "object",
                        "properties": {
                            "name": { "type": "string", "minLength": 1, "maxLength": 100 },
                            "email": { "type": "string", "format": "email" },
                            "tags": {
                                "type": "array",
                                "items": { "type": "string" },
                                "maxItems": 10
                            }
                        },
                        "required": ["name", "email"]
                    }
                },
                "required": ["id", "profile"]
            },
            "metadata": {
                "type": "object",
                "additionalProperties": { "type": "string" }
            }
        },
        "required": ["user"]
    });

    let validator = PerformanceValidator::new(&schema)?;

    // テストデータ生成
    let test_data: Vec<Value> = (0..5000)
        .map(|i| {
            json!({
                "user": {
                    "id": format!("user-{}", i),
                    "profile": {
                        "name": format!("ユーザー{}", i),
                        "email": format!("user{}@example.com", i),
                        "tags": vec![format!("tag{}", i % 5)]
                    }
                },
                "metadata": {
                    "created_at": "2025-01-01T00:00:00Z",
                    "source": "api"
                }
            })
        })
        .collect();

    // ベンチマーク実行
    let benchmark_result = validator.benchmark_validation(&test_data, 10);
    benchmark_result.print_summary();

    // メモリ効率的なバリデーション(イテレーター使用)
    println!("\n=== メモリ効率的バリデーション ===");
    let stats = validator.memory_efficient_validation(test_data.into_iter());
    stats.print_summary();

    Ok(())
}