jsonschema
GitHub概要
Stranger6667/jsonschema
A high-performance JSON Schema validator for Rust
スター682
ウォッチ7
フォーク112
作成日:2020年3月7日
言語:Rust
ライセンス:MIT License
トピックス
jsonschemapythonrustvalidationwebassembly
スター履歴
データ取得日時: 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(())
}