use regex::Regex;
use serde_json::Value;
mod test;
pub fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> {
is_valid_schema(schema)?;
validate_instance(schema, instance)
}
pub fn is_valid_schema(schema: &Value) -> Result<(), String> {
if let Some(schema_type) = schema.get("type") {
if let Some(type_str) = schema_type.as_str() {
match type_str {
"object" => {
if schema.get("properties").is_none() {
return Err("Object schema must contain 'properties'".to_string());
}
}
"array" => {
if schema.get("items").is_none() {
return Err("Array schema must contain 'items'".to_string());
}
}
"string" | "integer" | "number" | "boolean" | "null" | "any" => {}
_ => return Err(format!("Unsupported schema type: {type_str}")),
}
} else {
return Err("'type' in schema must be a string".to_string());
}
} else {
return Err("Schema must contain a 'type'".to_string());
}
Ok(())
}
fn validate_instance(schema: &Value, instance: &Value) -> Result<(), String> {
if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
match schema_type {
"object" => validate_object(schema, instance.get("data").unwrap_or(instance))?,
"array" => validate_array(schema, instance.get("data").unwrap_or(instance))?,
"string" => validate_string(schema, instance.get("data").unwrap_or(instance))?,
"number" | "integer" => {
validate_number(schema, instance.get("data").unwrap_or(instance))?
}
"boolean" => validate_boolean(instance.get("data").unwrap_or(instance))?,
"null" => validate_null(instance.get("data").unwrap_or(instance))?,
"any" => (),
_ => return Err(format!("Unsupported type: {schema_type}")),
}
}
else if let Some(type_array) = schema.get("type").and_then(|t| t.as_array()) {
let mut any_type_matched = false;
for type_value in type_array {
if let Some(type_str) = type_value.as_str() {
let mut temp_schema = schema.clone();
temp_schema
.as_object_mut()
.ok_or_else(|| "Failed to get object mut".to_string())?
.insert("type".to_string(), Value::String(type_str.to_string()));
if validate_instance(&temp_schema, instance).is_ok() {
any_type_matched = true;
break;
}
}
}
if !any_type_matched {
let type_names: Vec<String> = type_array
.iter()
.filter_map(|t| t.as_str())
.map(|s| s.to_string())
.collect();
return Err(format!(
"Value does not match any of the allowed types: {}",
type_names.join(", ")
));
}
}
Ok(())
}
fn validate_object(schema: &Value, instance: &Value) -> Result<(), String> {
if !instance.is_object() {
return Err(format!("Expected object value: {instance}"));
}
let instance_obj = instance
.as_object()
.ok_or_else(|| "Failed to get object when validating object".to_string())?;
if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
for field in required {
if let Some(field_name) = field.as_str() {
if !instance_obj.contains_key(field_name) {
return Err(format!("Missing required field: {field_name}"));
}
}
}
}
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
let required_fields: std::collections::HashSet<&str> = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
for (key, prop_schema) in properties {
if let Some(value) = instance_obj.get(key) {
let is_required = required_fields.contains(key.as_str());
if is_required {
validate_instance(prop_schema, value)?;
} else {
validate_with_null_allowed(prop_schema, value)?;
}
}
}
}
Ok(())
}
fn validate_with_null_allowed(prop_schema: &Value, value: &Value) -> Result<(), String> {
if value.is_null() {
return Ok(());
}
if let Some(type_array) = prop_schema.get("type").and_then(|t| t.as_array()) {
if type_array.iter().any(|t| t.as_str() == Some("null")) {
return validate_instance(prop_schema, value);
}
let mut new_types = type_array.clone();
new_types.push(Value::String("null".to_string()));
let mut enhanced_schema = prop_schema.clone();
enhanced_schema
.as_object_mut()
.ok_or_else(|| "Failed to get object mut".to_string())?
.insert("type".to_string(), Value::Array(new_types));
validate_instance(&enhanced_schema, value)
} else if let Some(single_type) = prop_schema.get("type").and_then(|t| t.as_str()) {
if single_type == "null" {
validate_instance(prop_schema, value)
} else {
let mut enhanced_schema = prop_schema.clone();
enhanced_schema
.as_object_mut()
.ok_or_else(|| "Failed to get object mut".to_string())?
.insert(
"type".to_string(),
Value::Array(vec![
Value::String(single_type.to_string()),
Value::String("null".to_string()),
]),
);
validate_instance(&enhanced_schema, value)
}
} else {
validate_instance(prop_schema, value)
}
}
fn validate_array(schema: &Value, instance: &Value) -> Result<(), String> {
if !instance.is_array() {
return Err("Expected array value".to_string());
}
let array = instance
.as_array()
.ok_or_else(|| "Failed to get array when validating array".to_string())?;
if let Some(min_items) = schema.get("minItems").and_then(|m| m.as_u64()) {
if array.len() < min_items as usize {
return Err(format!(
"Array length {} is less than minItems {}",
array.len(),
min_items
));
}
}
if let Some(max_items) = schema.get("maxItems").and_then(|m| m.as_u64()) {
if array.len() > max_items as usize {
return Err(format!(
"Array length {} is greater than maxItems {}",
array.len(),
max_items
));
}
}
if let Some(unique_items) = schema.get("uniqueItems").and_then(|u| u.as_bool()) {
if unique_items {
for i in 0..array.len() {
for j in (i + 1)..array.len() {
if array[i] == array[j] {
return Err(format!(
"Array contains duplicate items at positions {i} and {j}"
));
}
}
}
}
}
if let Some(items_schema) = schema.get("items") {
for item in array {
validate_instance(items_schema, item)?;
}
}
Ok(())
}
fn validate_string(schema: &Value, instance: &Value) -> Result<(), String> {
if !instance.is_string() {
return Err(format!("Expected string value {instance:?}"));
}
let string = instance
.as_str()
.ok_or_else(|| "Failed to get string when validating string".to_string())?;
if let Some(min_length) = schema.get("minLength").and_then(|m| m.as_u64()) {
if string.len() < min_length as usize {
return Err(format!(
"String length {} is less than minLength {}",
string.len(),
min_length
));
}
}
if let Some(max_length) = schema.get("maxLength").and_then(|m| m.as_u64()) {
if string.len() > max_length as usize {
return Err(format!(
"String length {} is greater than maxLength {}",
string.len(),
max_length
));
}
}
if let Some(pattern) = schema.get("pattern").and_then(|p| p.as_str()) {
let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex pattern: {e}"))?;
if !regex.is_match(string) {
return Err(format!("String does not match pattern: {pattern}"));
}
}
Ok(())
}
fn validate_number(schema: &Value, instance: &Value) -> Result<(), String> {
if !instance.is_number() {
return Err("Expected number value".to_string());
}
let number = instance
.as_f64()
.ok_or_else(|| "Failed to get number when validating number".to_string())?;
if let Some(minimum) = schema.get("minimum").and_then(|m| m.as_f64()) {
if number < minimum {
return Err(format!("Value {number} is less than minimum {minimum}"));
}
}
if let Some(maximum) = schema.get("maximum").and_then(|m| m.as_f64()) {
if number > maximum {
return Err(format!("Value {number} is greater than maximum {maximum}"));
}
}
if let Some(multiple_of) = schema.get("multipleOf").and_then(|m| m.as_f64()) {
if (number / multiple_of).fract() != 0.0 {
return Err(format!("Value {number} is not a multiple of {multiple_of}"));
}
}
Ok(())
}
fn validate_boolean(instance: &Value) -> Result<(), String> {
if !instance.is_boolean() {
return Err("Expected boolean value".to_string());
}
Ok(())
}
fn validate_null(instance: &Value) -> Result<(), String> {
if !instance.is_null() {
return Err("Expected null value".to_string());
}
Ok(())
}
pub fn get_keys(schema: &Value) -> Result<Vec<String>, String> {
if let Some(properties) = schema.get("properties") {
if let Some(obj) = properties.as_object() {
Ok(obj.keys().cloned().collect())
} else {
Err("Properties must be an object".to_string())
}
} else {
Ok(Vec::new())
}
}
pub fn get_keys_for_consumed_inputs(schema: &Value) -> Result<Vec<String>, String> {
if let Some(properties) = schema.get("properties") {
if let Some(obj) = properties.as_object() {
if let Some(consumed_inputs) = obj.get("consumed_inputs") {
if let Some(obj) = consumed_inputs.as_object() {
if let Some(properties) = obj.get("properties") {
if let Some(obj) = properties.as_object() {
Ok(obj.keys().cloned().collect())
} else {
Err("Properties must be an object".to_string())
}
} else {
Ok(Vec::new())
}
} else {
Err("Consumed inputs must be an object".to_string())
}
} else {
Ok(Vec::new())
}
} else {
Err("Properties must be an object".to_string())
}
} else {
Ok(Vec::new())
}
}
pub fn get_keys_for_inputs(schema: &Value) -> Result<Vec<String>, String> {
if let Some(properties) = schema.get("properties") {
if let Some(obj) = properties.as_object() {
if let Some(inputs) = obj.get("inputs") {
if let Some(obj) = inputs.as_object() {
if let Some(properties) = obj.get("properties") {
if let Some(obj) = properties.as_object() {
Ok(obj.keys().cloned().collect())
} else {
Err("Properties must be an object".to_string())
}
} else {
Ok(Vec::new())
}
} else {
Err("Inputs must be an object".to_string())
}
} else {
Ok(Vec::new())
}
} else {
Err("Properties must be an object".to_string())
}
} else {
Ok(Vec::new())
}
}
pub fn check_if_type_array_for_key(schema: &Value, name: &str) -> Result<(), String> {
if let Some(properties) = schema.get("properties") {
if let Some(field_schema) = properties.get(name) {
if let Some(field_type) = field_schema.get("type") {
if let Some(type_str) = field_type.as_str() {
if type_str == "array" {
Ok(())
} else {
Err(format!("Field '{name}' is not of type array"))
}
} else {
Err(format!("Type for field '{name}' must be a string"))
}
} else {
Err(format!("Field '{name}' has no type specified"))
}
} else {
Err(format!("Field '{name}' not found in schema"))
}
} else {
Err("Schema has no properties".to_string())
}
}
pub fn get_properties(schema: &Value) -> Result<&Value, String> {
if let Some(properties) = schema.get("properties") {
Ok(properties)
} else {
Err("Schema has no properties".to_string())
}
}
pub fn check_if_type_array_for_sub_key(schema: &Value, sub_keys: Vec<&str>) -> Result<(), String> {
let mut schema = schema;
for key in sub_keys[..sub_keys.len() - 1].iter() {
if let Some(obj) = get_properties(schema)?.get(key) {
schema = obj;
} else {
return Err(format!("Field '{key}' not found in schema"));
}
}
check_if_type_array_for_key(schema, sub_keys[sub_keys.len() - 1])
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::time::Instant;
#[test]
fn test_validate_json_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
});
let valid_instance = json!({
"name": "John",
"age": 30
});
let invalid_instance = json!({
"name": "John"
});
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
assert!(validate_json_schema(&schema, &invalid_instance).is_err());
}
#[test]
fn test_is_valid_schema() {
let valid_schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let invalid_schema = json!({
"type": "invalid_type"
});
assert!(is_valid_schema(&valid_schema).is_ok());
assert!(is_valid_schema(&invalid_schema).is_err());
}
#[test]
fn test_get_keys() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
}
});
let keys = get_keys(&schema).unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"name".to_string()));
assert!(keys.contains(&"age".to_string()));
}
#[test]
fn test_check_if_type_array_for_key() {
let schema = json!({
"type": "object",
"properties": {
"items": { "type": "array" },
"name": { "type": "string" }
}
});
assert!(check_if_type_array_for_key(&schema, "items").is_ok());
assert!(check_if_type_array_for_key(&schema, "name").is_err());
assert!(check_if_type_array_for_key(&schema, "nonexistent").is_err());
}
#[test]
fn test_complex_object_validation() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 3 },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"email": { "type": "string", "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" },
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": { "type": "string", "pattern": "^\\d{5}$" }
},
"required": ["street", "city", "zip"]
},
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 5
}
},
"required": ["name", "age", "email"]
});
let valid_instance = json!({
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "Anytown",
"zip": "12345"
},
"tags": ["tag1", "tag2"]
});
let invalid_instance = json!({
"name": "Jo", "age": 200, "email": "invalid-email",
"address": {
"street": "123 Main St",
"city": "Anytown"
},
"tags": [] });
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
assert!(validate_json_schema(&schema, &invalid_instance).is_err());
}
#[test]
fn test_array_validation() {
let schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
},
"required": ["id", "name"]
},
"minItems": 2,
"maxItems": 4
});
let valid_instance = json!([
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
]);
let invalid_instance = json!([
{ "id": 1 }, { "name": "Item 2" } ]);
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
assert!(validate_json_schema(&schema, &invalid_instance).is_err());
}
#[test]
fn test_number_validation() {
let schema = json!({
"type": "number",
"minimum": 0,
"maximum": 100,
"multipleOf": 0.5
});
let valid_instance = json!(50.0);
let invalid_instance = json!(150.0);
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
assert!(validate_json_schema(&schema, &invalid_instance).is_err());
}
#[test]
fn test_string_validation() {
let schema = json!({
"type": "string",
"minLength": 5,
"maxLength": 10,
"pattern": "^[A-Za-z]+$"
});
let valid_instance = json!("Hello");
let invalid_instance = json!("Hi123");
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
assert!(validate_json_schema(&schema, &invalid_instance).is_err());
}
#[test]
fn test_performance() {
let schema = json!({
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"value": { "type": "string" }
},
"required": ["id", "value"]
}
}
}
});
let mut data = Vec::new();
for i in 0..1000 {
data.push(json!({
"id": i,
"value": format!("Value {}", i)
}));
}
let instance = json!({ "data": { "data": data } });
let start = Instant::now();
let result = validate_json_schema(&schema, &instance);
let duration = start.elapsed();
assert!(result.is_ok());
println!("Validation of 1000 items took: {duration:?}");
assert!(
duration.as_millis() < 100,
"Validation took too long: {duration:?}"
);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
fn test_wasm_compatibility() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
});
let valid_instance = json!({
"name": "John",
"age": 30
});
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
fn test_wasm_complex_validation() {
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" },
"items": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["name", "age"]
});
let valid_instance = json!({
"name": "John",
"age": 30,
"items": ["item1", "item2"]
});
assert!(validate_json_schema(&schema, &valid_instance).is_ok());
}
}