From 65971d9b933af7e7f3a25cf056ac2c91bb8cfbb3 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Thu, 12 Jun 2025 17:07:28 -0400 Subject: [PATCH] splitting up errorkind paths to produce multiple drop errors --- src/lib.rs | 89 ++++++++- src/tests.rs | 531 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 578 insertions(+), 42 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7948351..ad3116e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,16 +155,87 @@ fn collect_errors(error: &ValidationError, errors_list: &mut Vec) { ); if error.causes.is_empty() && !is_structural { - // This is a leaf error that's not structural - let original_message = format!("{}", error.kind); - let (error_code, human_message) = convert_error_kind(&error.kind); + // Handle errors with multiple fields specially + match &error.kind { + ErrorKind::Required { want } => { + // Create a separate error for each missing required field + let base_path = error.instance_location.to_string(); + for missing_field in want { + let field_path = if base_path.is_empty() { + format!("/{}", missing_field) + } else { + format!("{}/{}", base_path, missing_field) + }; + + errors_list.push(Error { + path: field_path, + code: "REQUIRED_FIELD_MISSING".to_string(), + message: format!("Required field '{}' is missing", missing_field), + cause: format!("property '{}' is required", missing_field), + }); + } + } + ErrorKind::Dependency { prop, missing } | ErrorKind::DependentRequired { prop, missing } => { + // Create a separate error for each missing field + let base_path = error.instance_location.to_string(); + for missing_field in missing { + let field_path = if base_path.is_empty() { + format!("/{}", missing_field) + } else { + format!("{}/{}", base_path, missing_field) + }; + + let (error_code, human_message) = match &error.kind { + ErrorKind::Dependency { .. } => ( + "DEPENDENCY_FAILED".to_string(), + format!("Field '{}' is required when '{}' is present", missing_field, prop), + ), + ErrorKind::DependentRequired { .. } => ( + "DEPENDENT_REQUIRED_MISSING".to_string(), + format!("Field '{}' is required when '{}' is present", missing_field, prop), + ), + _ => unreachable!(), + }; + + errors_list.push(Error { + path: field_path, + code: error_code, + message: human_message, + cause: format!("property '{}' required, if '{}' property exists", missing_field, prop), + }); + } + } + ErrorKind::AdditionalProperties { got } => { + // Create a separate error for each additional property that's not allowed + let base_path = error.instance_location.to_string(); + for extra_prop in got { + let field_path = if base_path.is_empty() { + format!("/{}", extra_prop) + } else { + format!("{}/{}", base_path, extra_prop) + }; + + errors_list.push(Error { + path: field_path, + code: "ADDITIONAL_PROPERTIES_NOT_ALLOWED".to_string(), + message: format!("Property '{}' is not allowed", extra_prop), + cause: format!("additionalProperty '{}' not allowed", extra_prop), + }); + } + } + _ => { + // Handle all other error types normally + let original_message = format!("{}", error.kind); + let (error_code, human_message) = convert_error_kind(&error.kind); - errors_list.push(Error { - path: error.instance_location.to_string(), - code: error_code, - message: human_message, - cause: original_message, - }); + errors_list.push(Error { + path: error.instance_location.to_string(), + code: error_code, + message: human_message, + cause: original_message, + }); + } + } } else { // Recurse into causes for cause in &error.causes { diff --git a/src/tests.rs b/src/tests.rs index 9751145..ad25a6d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -45,24 +45,24 @@ macro_rules! assert_failure_with_json { let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array"); - if errors_array.len() != $expected_error_count { - let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); - panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json); - } - - if $expected_error_count > 0 { - let first_error_message = errors_array[0].get("message").and_then(Value::as_str); - match first_error_message { - Some(msg) => { - if !msg.contains($expected_first_message_contains) { + if errors_array.len() != $expected_error_count { let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); - panic!("Assertion Failed (first error message mismatch): Expected contains '{}', got: '{}'. {}\nResult JSON:\n{}", $expected_first_message_contains, msg, base_msg, pretty_json); + panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json); } - } - None => { - let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); - panic!("Assertion Failed (first error in array has no 'message' string): {}\nResult JSON:\n{}", base_msg, pretty_json); - } + + if $expected_error_count > 0 { + let first_error_message = errors_array[0].get("message").and_then(Value::as_str); + match first_error_message { + Some(msg) => { + if !msg.contains($expected_first_message_contains) { + let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); + panic!("Assertion Failed (first error message mismatch): Expected contains '{}', got: '{}'. {}\nResult JSON:\n{}", $expected_first_message_contains, msg, base_msg, pretty_json); + } + } + None => { + let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); + panic!("Assertion Failed (first error in array has no 'message' string): {}\nResult JSON:\n{}", base_msg, pretty_json); + } } } }; @@ -86,9 +86,9 @@ macro_rules! assert_failure_with_json { let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array"); - if errors_array.len() != $expected_error_count { - let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); - panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json); + if errors_array.len() != $expected_error_count { + let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); + panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json); } }; // Without custom message (calls the one above with ""): @@ -112,7 +112,7 @@ macro_rules! assert_failure_with_json { let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array"); if errors_array.is_empty() { - let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); + let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result)); panic!("Assertion Failed (expected errors, but 'errors' array is empty): {}\nResult JSON:\n{}", base_msg, pretty_json); } }; @@ -160,9 +160,9 @@ fn test_cache_and_validate_json_schema() { // Missing field let invalid_result_missing = validate_json_schema(schema_id, jsonb(invalid_instance_missing)); - assert_failure_with_json!(invalid_result_missing, 1, "Required field is missing", "Validation with missing field should fail."); + assert_failure_with_json!(invalid_result_missing, 1, "Required field 'age' is missing", "Validation with missing field should fail."); let errors_missing = invalid_result_missing.0["errors"].as_array().unwrap(); - assert_eq!(errors_missing[0]["details"]["path"], ""); + assert_eq!(errors_missing[0]["details"]["path"], "/age"); assert_eq!(errors_missing[0]["details"]["schema"], "my_schema"); assert_eq!(errors_missing[0]["code"], "REQUIRED_FIELD_MISSING"); @@ -293,7 +293,7 @@ fn test_validate_json_schema_oneof_validation_errors() { e["details"]["schema"] == "oneof_schema" ), "Missing maxLength error"); assert!(errors_string.iter().any(|e| - e["details"]["path"] == "" && + e["details"]["path"] == "/number_prop" && e["code"] == "REQUIRED_FIELD_MISSING" && e["details"]["schema"] == "oneof_schema" ), "Missing number_prop required error"); @@ -311,7 +311,7 @@ fn test_validate_json_schema_oneof_validation_errors() { e["details"]["schema"] == "oneof_schema" ), "Missing minimum error"); assert!(errors_number.iter().any(|e| - e["details"]["path"] == "" && + e["details"]["path"] == "/string_prop" && e["code"] == "REQUIRED_FIELD_MISSING" && e["details"]["schema"] == "oneof_schema" ), "Missing string_prop required error"); @@ -333,16 +333,23 @@ fn test_validate_json_schema_oneof_validation_errors() { // Input: empty object, expected string_prop (branch 0) OR number_prop (branch 1) let invalid_empty_obj = json!({}); let result_empty_obj = validate_json_schema(schema_id, jsonb(invalid_empty_obj)); - // Expect only 1 leaf error after filtering, as both original errors have instance_path "" - assert_failure_with_json!(result_empty_obj, 1); - // Explicitly check that the single remaining error is one of the expected missing properties errors + // Now we expect 2 errors because required fields are split into individual errors + assert_failure_with_json!(result_empty_obj, 2); let errors_empty = result_empty_obj.0["errors"].as_array().expect("Expected error array for empty object"); - assert_eq!(errors_empty.len(), 1, "Expected exactly one error after filtering empty object"); - assert_eq!(errors_empty[0]["code"], "REQUIRED_FIELD_MISSING"); - assert_eq!(errors_empty[0]["details"]["path"], ""); - assert_eq!(errors_empty[0]["details"]["schema"], "oneof_schema"); - // The human message should be generic - assert_eq!(errors_empty[0]["message"], "Required field is missing"); + assert_eq!(errors_empty.len(), 2, "Expected two errors for missing required fields"); + + // Check that we have errors for both missing fields + assert!(errors_empty.iter().any(|e| + e["details"]["path"] == "/string_prop" && + e["code"] == "REQUIRED_FIELD_MISSING" && + e["details"]["schema"] == "oneof_schema" + ), "Missing string_prop required error"); + + assert!(errors_empty.iter().any(|e| + e["details"]["path"] == "/number_prop" && + e["code"] == "REQUIRED_FIELD_MISSING" && + e["details"]["schema"] == "oneof_schema" + ), "Missing number_prop required error"); } #[pg_test] @@ -622,3 +629,461 @@ fn test_auto_strict_validation() { let result_permissive = validate_json_schema(schema_id_permissive, jsonb(instance_with_extra)); assert_success_with_json!(result_permissive, "Instance with extra property should pass when additionalProperties is explicitly true, even with strict=true"); } + +#[pg_test] +fn test_required_fields_split_errors() { + clear_json_schemas(); + let schema_id = "required_split_test"; + + // Schema with multiple required fields + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "kind": { "type": "string" }, + "age": { "type": "number" } + }, + "required": ["name", "kind", "age"] + }); + + let cache_result = cache_json_schema(schema_id, jsonb(schema), false); + assert_success_with_json!(cache_result, "Schema caching should succeed"); + + // Test 1: Missing all required fields + let empty_instance = json!({}); + let result = validate_json_schema(schema_id, jsonb(empty_instance)); + + // Should get 3 separate errors, one for each missing field + assert_failure_with_json!(result, 3, "Required field"); + + let errors = result.0["errors"].as_array().unwrap(); + + // Check that we have errors for each missing field with correct paths + assert!(errors.iter().any(|e| + e["code"] == "REQUIRED_FIELD_MISSING" && + e["details"]["path"] == "/name" && + e["message"] == "Required field 'name' is missing" + ), "Missing error for name field"); + + assert!(errors.iter().any(|e| + e["code"] == "REQUIRED_FIELD_MISSING" && + e["details"]["path"] == "/kind" && + e["message"] == "Required field 'kind' is missing" + ), "Missing error for kind field"); + + assert!(errors.iter().any(|e| + e["code"] == "REQUIRED_FIELD_MISSING" && + e["details"]["path"] == "/age" && + e["message"] == "Required field 'age' is missing" + ), "Missing error for age field"); + + // Test 2: Missing only some required fields + let partial_instance = json!({ + "name": "Alice" + }); + let partial_result = validate_json_schema(schema_id, jsonb(partial_instance)); + + // Should get 2 errors for the missing fields + assert_failure_with_json!(partial_result, 2, "Required field"); + + let partial_errors = partial_result.0["errors"].as_array().unwrap(); + + assert!(partial_errors.iter().any(|e| + e["details"]["path"] == "/kind" + ), "Missing error for kind field"); + + assert!(partial_errors.iter().any(|e| + e["details"]["path"] == "/age" + ), "Missing error for age field"); +} + +#[pg_test] +fn test_dependency_fields_split_errors() { + clear_json_schemas(); + let schema_id = "dependency_split_test"; + + // Schema with dependencies like the tokenize_external_accounts example + let schema = json!({ + "type": "object", + "properties": { + "creating": { "type": "boolean" }, + "name": { "type": "string" }, + "kind": { "type": "string" }, + "description": { "type": "string" } + }, + "dependencies": { + "creating": ["name", "kind"] // When creating is present, name and kind are required + } + }); + + let cache_result = cache_json_schema(schema_id, jsonb(schema), false); + assert_success_with_json!(cache_result, "Schema caching should succeed"); + + // Test 1: Has creating=true but missing both dependent fields + let missing_both = json!({ + "creating": true, + "description": "Some description" + }); + let result = validate_json_schema(schema_id, jsonb(missing_both)); + + // Should get 2 separate errors, one for each missing dependent field + assert_failure_with_json!(result, 2, "Field"); + + let errors = result.0["errors"].as_array().unwrap(); + + assert!(errors.iter().any(|e| + e["code"] == "DEPENDENCY_FAILED" && + e["details"]["path"] == "/name" && + e["message"] == "Field 'name' is required when 'creating' is present" + ), "Missing error for dependent name field"); + + assert!(errors.iter().any(|e| + e["code"] == "DEPENDENCY_FAILED" && + e["details"]["path"] == "/kind" && + e["message"] == "Field 'kind' is required when 'creating' is present" + ), "Missing error for dependent kind field"); + + // Test 2: Has creating=true with only one dependent field + let missing_one = json!({ + "creating": true, + "name": "My Account" + }); + let result_one = validate_json_schema(schema_id, jsonb(missing_one)); + + // Should get 1 error for the missing kind field + assert_failure_with_json!(result_one, 1, "Field 'kind' is required when 'creating' is present"); + + let errors_one = result_one.0["errors"].as_array().unwrap(); + assert_eq!(errors_one[0]["details"]["path"], "/kind"); + + // Test 3: Has no creating field - no dependency errors + let no_creating = json!({ + "description": "No creating field" + }); + let result_no_creating = validate_json_schema(schema_id, jsonb(no_creating)); + assert_success_with_json!(result_no_creating, "Should succeed when creating field is not present"); + + // Test 4: Has creating=false - dependencies still apply because field exists! + let creating_false = json!({ + "creating": false, + "description": "Creating is false" + }); + let result_false = validate_json_schema(schema_id, jsonb(creating_false)); + // Dependencies are triggered by field existence, not value, so this should fail + assert_failure_with_json!(result_false, 2, "Field"); + + let errors_false = result_false.0["errors"].as_array().unwrap(); + assert!(errors_false.iter().any(|e| + e["details"]["path"] == "/name" + ), "Should have error for name when creating exists with false value"); + assert!(errors_false.iter().any(|e| + e["details"]["path"] == "/kind" + ), "Should have error for kind when creating exists with false value"); +} + +#[pg_test] +fn test_nested_required_dependency_errors() { + clear_json_schemas(); + let schema_id = "nested_dep_test"; + + // More complex schema with nested objects + let schema = json!({ + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "creating": { "type": "boolean" }, + "name": { "type": "string" }, + "kind": { "type": "string" } + }, + "required": ["id"], + "dependencies": { + "creating": ["name", "kind"] + } + } + } + }, + "required": ["items"] + }); + + let cache_result = cache_json_schema(schema_id, jsonb(schema), false); + assert_success_with_json!(cache_result, "Schema caching should succeed"); + + // Test with array items that have dependency violations + let instance = json!({ + "items": [ + { + "id": "item1", + "creating": true + // Missing name and kind + }, + { + "id": "item2", + "creating": true, + "name": "Item 2" + // Missing kind + } + ] + }); + + let result = validate_json_schema(schema_id, jsonb(instance)); + + // Should get 3 errors total: 2 for first item, 1 for second item + assert_failure_with_json!(result, 3, "Field"); + + let errors = result.0["errors"].as_array().unwrap(); + + // Check paths are correct for array items + assert!(errors.iter().any(|e| + e["details"]["path"] == "/items/0/name" && + e["code"] == "DEPENDENCY_FAILED" + ), "Missing error for first item's name"); + + assert!(errors.iter().any(|e| + e["details"]["path"] == "/items/0/kind" && + e["code"] == "DEPENDENCY_FAILED" + ), "Missing error for first item's kind"); + + assert!(errors.iter().any(|e| + e["details"]["path"] == "/items/1/kind" && + e["code"] == "DEPENDENCY_FAILED" + ), "Missing error for second item's kind"); +} + +#[pg_test] +fn test_additional_properties_split_errors() { + clear_json_schemas(); + let schema_id = "additional_props_split_test"; + + // Schema with additionalProperties: false + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "additionalProperties": false + }); + + let cache_result = cache_json_schema(schema_id, jsonb(schema), false); + assert_success_with_json!(cache_result, "Schema caching should succeed"); + + // Test 1: Multiple additional properties not allowed + let instance_many_extras = json!({ + "name": "Alice", + "age": 30, + "extra1": "not allowed", + "extra2": 42, + "extra3": true + }); + + let result = validate_json_schema(schema_id, jsonb(instance_many_extras)); + + // Should get 3 separate errors, one for each additional property + assert_failure_with_json!(result, 3, "Property"); + + let errors = result.0["errors"].as_array().unwrap(); + + // Check that we have errors for each additional property with correct paths + assert!(errors.iter().any(|e| + e["code"] == "ADDITIONAL_PROPERTIES_NOT_ALLOWED" && + e["details"]["path"] == "/extra1" && + e["message"] == "Property 'extra1' is not allowed" + ), "Missing error for extra1 property"); + + assert!(errors.iter().any(|e| + e["code"] == "ADDITIONAL_PROPERTIES_NOT_ALLOWED" && + e["details"]["path"] == "/extra2" && + e["message"] == "Property 'extra2' is not allowed" + ), "Missing error for extra2 property"); + + assert!(errors.iter().any(|e| + e["code"] == "ADDITIONAL_PROPERTIES_NOT_ALLOWED" && + e["details"]["path"] == "/extra3" && + e["message"] == "Property 'extra3' is not allowed" + ), "Missing error for extra3 property"); + + // Test 2: Single additional property + let instance_one_extra = json!({ + "name": "Bob", + "age": 25, + "unauthorized": "field" + }); + + let result_one = validate_json_schema(schema_id, jsonb(instance_one_extra)); + + // Should get 1 error for the additional property + assert_failure_with_json!(result_one, 1, "Property 'unauthorized' is not allowed"); + + let errors_one = result_one.0["errors"].as_array().unwrap(); + assert_eq!(errors_one[0]["details"]["path"], "/unauthorized"); + + // Test 3: Nested objects with additional properties + let nested_schema_id = "nested_additional_props_test"; + let nested_schema = json!({ + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false + } + } + }); + + let _ = cache_json_schema(nested_schema_id, jsonb(nested_schema), false); + + let nested_instance = json!({ + "user": { + "name": "Charlie", + "role": "admin", + "level": 5 + } + }); + + let nested_result = validate_json_schema(nested_schema_id, jsonb(nested_instance)); + + // Should get 2 errors for the nested additional properties + assert_failure_with_json!(nested_result, 2, "Property"); + + let nested_errors = nested_result.0["errors"].as_array().unwrap(); + + assert!(nested_errors.iter().any(|e| + e["details"]["path"] == "/user/role" && + e["code"] == "ADDITIONAL_PROPERTIES_NOT_ALLOWED" + ), "Missing error for nested role property"); + + assert!(nested_errors.iter().any(|e| + e["details"]["path"] == "/user/level" && + e["code"] == "ADDITIONAL_PROPERTIES_NOT_ALLOWED" + ), "Missing error for nested level property"); +} + +#[pg_test] +fn test_unevaluated_properties_errors() { + clear_json_schemas(); + let schema_id = "unevaluated_test"; + + // Schema with unevaluatedProperties: false + // This is more complex than additionalProperties because it considers + // properties matched by pattern properties and additional properties + let schema = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "number" } + }, + "patternProperties": { + "^attr_": { "type": "string" } // Properties starting with attr_ are allowed + }, + "unevaluatedProperties": false // No other properties allowed + }); + + let cache_result = cache_json_schema(schema_id, jsonb(schema), false); + assert_success_with_json!(cache_result, "Schema caching should succeed"); + + // Test 1: Multiple unevaluated properties + let instance_uneval = json!({ + "name": "Alice", + "age": 30, + "attr_color": "blue", // This is OK - matches pattern + "extra1": "not evaluated", // These should fail + "extra2": 42, + "extra3": true + }); + + let result = validate_json_schema(schema_id, jsonb(instance_uneval)); + + // Should get 3 separate FALSE_SCHEMA errors, one for each unevaluated property + assert_failure_with_json!(result, 3, "Schema validation always fails"); + + let errors = result.0["errors"].as_array().unwrap(); + + // Verify all errors are FALSE_SCHEMA with specific paths + for error in errors { + assert_eq!(error["code"], "FALSE_SCHEMA", "All unevaluated properties should generate FALSE_SCHEMA errors"); + } + + // Check that we have errors for each unevaluated property with correct paths + assert!(errors.iter().any(|e| + e["code"] == "FALSE_SCHEMA" && + e["details"]["path"] == "/extra1" + ), "Missing error for extra1 property"); + + assert!(errors.iter().any(|e| + e["code"] == "FALSE_SCHEMA" && + e["details"]["path"] == "/extra2" + ), "Missing error for extra2 property"); + + assert!(errors.iter().any(|e| + e["code"] == "FALSE_SCHEMA" && + e["details"]["path"] == "/extra3" + ), "Missing error for extra3 property"); + + // Test 2: Complex schema with allOf and unevaluatedProperties + let complex_schema_id = "complex_unevaluated_test"; + let complex_schema = json!({ + "type": "object", + "allOf": [ + { + "properties": { + "firstName": { "type": "string" } + } + }, + { + "properties": { + "lastName": { "type": "string" } + } + } + ], + "properties": { + "age": { "type": "number" } + }, + "unevaluatedProperties": false + }); + + let _ = cache_json_schema(complex_schema_id, jsonb(complex_schema), false); + + // firstName and lastName are evaluated by allOf schemas, age by main schema + let complex_instance = json!({ + "firstName": "John", + "lastName": "Doe", + "age": 25, + "nickname": "JD", // Not evaluated by any schema + "title": "Mr" // Not evaluated by any schema + }); + + let complex_result = validate_json_schema(complex_schema_id, jsonb(complex_instance)); + + // Should get 2 FALSE_SCHEMA errors for unevaluated properties + assert_failure_with_json!(complex_result, 2, "Schema validation always fails"); + + let complex_errors = complex_result.0["errors"].as_array().unwrap(); + + assert!(complex_errors.iter().any(|e| + e["code"] == "FALSE_SCHEMA" && + e["details"]["path"] == "/nickname" + ), "Missing error for nickname property"); + + assert!(complex_errors.iter().any(|e| + e["code"] == "FALSE_SCHEMA" && + e["details"]["path"] == "/title" + ), "Missing error for title property"); + + // Test 3: Valid instance with all properties evaluated + let valid_instance = json!({ + "name": "Bob", + "age": 40, + "attr_style": "modern", + "attr_theme": "dark" + }); + + let valid_result = validate_json_schema(schema_id, jsonb(valid_instance)); + assert_success_with_json!(valid_result, "All properties are evaluated, should pass"); +}