diff --git a/GEMINI.md b/GEMINI.md index 7ae4358..5fcf101 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -57,9 +57,43 @@ A DropError object provides a clear, structured explanation of a validation fail ## `boon` Crate Modifications -The version of `boon` located in the `validator/` directory has been significantly modified to support runtime-based strict validation. The original `boon` crate only supports compile-time strictness and lacks the necessary mechanisms to propagate validation context correctly for our use case. +The version of `boon` located in the `validator/` directory has been significantly modified to support application-specific validation logic that goes beyond the standard JSON Schema specification. -### 1. Recursive Runtime Strictness Control +### 1. Property-Level Overrides for Inheritance + +- **Problem:** A primary use case for this project is validating data models that use `$ref` to create inheritance chains (e.g., a `person` schema `$ref`s a `user` schema, which `$ref`s an `entity` schema). A common pattern is to use a `const` keyword on a `type` property to identify the specific model (e.g., `"type": {"const": "person"}`). However, standard JSON Schema composition with `allOf` (which is implicitly used by `$ref`) treats these as a logical AND. This creates an impossible condition where an instance's `type` property would need to be "person" AND "user" AND "entity" simultaneously. + +- **Solution:** We've implemented a custom, explicit override mechanism. A new keyword, `"override": true`, can be added to any property definition within a schema. + + ```json + // person.json + { + "$id": "person", + "$ref": "user", + "properties": { + "type": { "const": "person", "override": true } + } + } + ``` + + This signals to the validator that this definition of the `type` property should be the *only* one applied, and any definitions for `type` found in base schemas (like `user` or `entity`) should be ignored for the duration of this validation. + +#### Key Changes + +This was achieved by making the validator stateful, using a pattern already present in `boon` for handling `unevaluatedProperties`. + +1. **Meta-Schema Update**: The meta-schema for Draft 2020-12 was modified to recognize `"override": true` as a valid keyword within a schema object, preventing the compiler from rejecting our custom schemas. + +2. **Compiler Modification**: The schema compiler in `validator/src/compiler.rs` was updated. It now inspects sub-schemas within a `properties` keyword and, if it finds `"override": true`, it records the name of that property in a new `override_properties` `HashSet` on the compiled `Schema` struct. + +3. **Stateful Validator with `Override` Context**: The core `Validator` in `validator/src/validator.rs` was modified to carry an `Override` context (a `HashSet` of property names) throughout the validation process. + - **Initialization**: When validation begins, the `Override` context is created and populated with the names of any properties that the top-level schema has marked with `override`. + - **Propagation**: As the validator descends through a `$ref` or `allOf`, this `Override` context is cloned and passed down. The child schema adds its own override properties to the set, ensuring that higher-level overrides are always maintained. + - **Enforcement**: In `obj_validate`, before a property is validated, the validator first checks if the property's name exists in the `Override` context it has received. If it does, it means a parent schema has already claimed responsibility for validating this property, so the child validator **skips** it entirely. This effectively achieves the "top-level wins" inheritance model. + +This approach cleanly integrates our desired inheritance behavior directly into the validator with minimal and explicit deviation from the standard, avoiding the need for a complex, post-processing validation function like the old `walk_and_validate_refs`. + +### 2. Recursive Runtime Strictness Control - **Problem:** The `jspg` project requires that certain schemas (specifically those for public `puncs` and global `type`s) enforce a strict "no extra properties" policy. This strictness needs to be decided at runtime and must cascade through the entire validation hierarchy, including all nested objects and `$ref` chains. A compile-time flag was unsuitable because it would incorrectly apply strictness to shared, reusable schemas. diff --git a/src/lib.rs b/src/lib.rs index 334b3cb..9327558 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,147 +189,6 @@ fn compile_all_schemas( } } -fn walk_and_validate_refs( - instance: &Value, - schema: &Value, - cache: &std::sync::RwLockReadGuard, - path_parts: &mut Vec, - type_validated: bool, - top_level_id: Option<&str>, - errors: &mut Vec, -) { - if let Some(ref_url) = schema.get("$ref").and_then(|v| v.as_str()) { - if let Some(s) = cache.map.get(ref_url) { - let mut new_type_validated = type_validated; - if !type_validated && s.t == SchemaType::Type { - let id_to_use = top_level_id.unwrap_or(ref_url); - let expected_type = id_to_use.split('.').next().unwrap_or(id_to_use); - if let Some(actual_type) = instance.get("type").and_then(|v| v.as_str()) { - if actual_type == expected_type { - new_type_validated = true; - } else { - path_parts.push("type".to_string()); - let path = format!("/{}", path_parts.join("/")); - path_parts.pop(); - errors.push(json!({ - "code": "TYPE_MISMATCH", - "message": format!("Instance type '{}' does not match expected type '{}' derived from schema $ref", actual_type, expected_type), - "details": { "path": path, "context": instance, "cause": { "expected": expected_type, "actual": actual_type }, "schema": ref_url } - })); - } - } else { - if top_level_id.is_some() { - let path = if path_parts.is_empty() { "".to_string() } else { format!("/{}", path_parts.join("/")) }; - errors.push(json!({ - "code": "TYPE_MISMATCH", - "message": "Instance is missing 'type' property required for schema validation", - "details": { "path": path, "context": instance, "cause": { "expected": expected_type }, "schema": ref_url } - })); - } - } - } - walk_and_validate_refs(instance, &s.value, cache, path_parts, new_type_validated, None, errors); - } - } - - if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) { - for (prop_name, prop_schema) in properties { - if let Some(prop_value) = instance.get(prop_name) { - path_parts.push(prop_name.clone()); - walk_and_validate_refs(prop_value, prop_schema, cache, path_parts, type_validated, None, errors); - path_parts.pop(); - } - } - } - - if let Some(items_schema) = schema.get("items") { - if let Some(instance_array) = instance.as_array() { - for (i, item) in instance_array.iter().enumerate() { - path_parts.push(i.to_string()); - walk_and_validate_refs(item, items_schema, cache, path_parts, false, None, errors); - path_parts.pop(); - } - } - } - - if let Some(all_of_array) = schema.get("allOf").and_then(|v| v.as_array()) { - for sub_schema in all_of_array { - walk_and_validate_refs(instance, sub_schema, cache, path_parts, type_validated, None, errors); - } - } - - if let Some(any_of_array) = schema.get("anyOf").and_then(|v| v.as_array()) { - for sub_schema in any_of_array { - walk_and_validate_refs(instance, sub_schema, cache, path_parts, type_validated, None, errors); - } - } - - if let Some(one_of_array) = schema.get("oneOf").and_then(|v| v.as_array()) { - let is_clean_ref_union = one_of_array.iter().all(|s| s.get("$ref").is_some()); - - if is_clean_ref_union { - if let Some(actual_type) = instance.get("type").and_then(|v| v.as_str()) { - let mut match_found = false; - for sub_schema in one_of_array { - if let Some(ref_url) = sub_schema.get("$ref").and_then(|v| v.as_str()) { - if ref_url == actual_type { - walk_and_validate_refs(instance, sub_schema, cache, path_parts, type_validated, None, errors); - match_found = true; - break; - } - } - } - if !match_found { - let path = format!("/{}", path_parts.join("/")); - errors.push(json!({ - "code": "TYPE_MISMATCH_IN_UNION", - "message": format!("Instance type '{}' does not match any of the allowed types in the union", actual_type), - "details": { - "path": path, - "context": instance, - "cause": { - "actual": actual_type, - "expected": one_of_array.iter() - .filter_map(|s| s.get("$ref").and_then(|r| r.as_str())) - .collect::>() - }, - "schema": top_level_id.unwrap_or("") - } - })); - } - } else { - let path = format!("/{}", path_parts.join("/")); - errors.push(json!({ - "code": "TYPE_REQUIRED_FOR_UNION", - "message": "Instance is missing 'type' property required for union (oneOf) validation", - "details": { "path": path, "context": instance, "schema": top_level_id.unwrap_or("") } - })); - } - return; - } else { - for sub_schema in one_of_array { - walk_and_validate_refs(instance, sub_schema, cache, path_parts, type_validated, None, errors); - } - } - } - - if let Some(if_schema) = schema.get("if") { - walk_and_validate_refs(instance, if_schema, cache, path_parts, type_validated, None, errors); - } - - if let Some(then_schema) = schema.get("then") { - walk_and_validate_refs(instance, then_schema, cache, path_parts, type_validated, None, errors); - } - - if let Some(else_schema) = schema.get("else") { - walk_and_validate_refs(instance, else_schema, cache, path_parts, type_validated, None, errors); - } - - if let Some(not_schema) = schema.get("not") { - walk_and_validate_refs(instance, not_schema, cache, path_parts, type_validated, None, errors); - } -} - #[pg_extern(strict, parallel_safe)] fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { let cache = SCHEMA_CACHE.read().unwrap(); @@ -353,18 +212,7 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB { match cache.schemas.validate(&instance_value, schema.index, options) { Ok(_) => { - let mut custom_errors = Vec::new(); - if schema.t == SchemaType::Type || schema.t == SchemaType::PublicPunc || schema.t == SchemaType::PrivatePunc { - let mut path_parts = vec![]; - let top_level_id = if schema.t == SchemaType::Type { Some(schema_id) } else { None }; - walk_and_validate_refs(&instance_value, &schema.value, &cache, &mut path_parts, false, top_level_id, &mut custom_errors); - } - - if custom_errors.is_empty() { JsonB(json!({ "response": "success" })) - } else { - JsonB(json!({ "errors": custom_errors })) - } } Err(validation_error) => { let mut error_list = Vec::new(); diff --git a/src/schemas.rs b/src/schemas.rs index 75efee4..36018e0 100644 --- a/src/schemas.rs +++ b/src/schemas.rs @@ -465,7 +465,7 @@ pub fn property_merging_schemas() -> JsonB { "properties": { "id": { "type": "string" }, "name": { "type": "string" }, - "type": { "type": "string" } + "type": { "type": "string", "const": "entity" } }, "required": ["id"] }] @@ -476,6 +476,7 @@ pub fn property_merging_schemas() -> JsonB { "$id": "user", "$ref": "entity", "properties": { + "type": { "type": "string", "const": "user", "override": true }, "password": { "type": "string", "minLength": 8 } }, "required": ["password"] @@ -487,6 +488,7 @@ pub fn property_merging_schemas() -> JsonB { "$id": "person", "$ref": "user", "properties": { + "type": { "type": "string", "const": "person", "override": true }, "first_name": { "type": "string", "minLength": 1 }, "last_name": { "type": "string", "minLength": 1 } }, @@ -852,7 +854,10 @@ pub fn type_matching_schemas() -> JsonB { "schemas": [{ "$id": "entity", "type": "object", - "properties": { "type": { "type": "string" }, "name": { "type": "string" } }, + "properties": { + "type": { "type": "string", "const": "entity" }, + "name": { "type": "string" } + }, "required": ["type", "name"] }] }, @@ -861,7 +866,10 @@ pub fn type_matching_schemas() -> JsonB { "schemas": [{ "$id": "job", "$ref": "entity", - "properties": { "job_id": { "type": "string" } }, + "properties": { + "type": { "type": "string", "const": "job", "override": true }, + "job_id": { "type": "string" } + }, "required": ["job_id"] }] }, @@ -871,7 +879,10 @@ pub fn type_matching_schemas() -> JsonB { { "$id": "super_job", "$ref": "job", - "properties": { "manager_id": { "type": "string" } }, + "properties": { + "type": { "type": "string", "const": "super_job", "override": true }, + "manager_id": { "type": "string" } + }, "required": ["manager_id"] }, { @@ -912,47 +923,59 @@ pub fn type_matching_schemas() -> JsonB { pub fn union_schemas() -> JsonB { let enums = json!([]); let types = json!([ + { + "name": "union_base", + "schemas": [{ + "$id": "union_base", + "type": "object", + "properties": { + "type": { "type": "string", "const": "union_base" }, + "id": { "type": "string" } + }, + "required": ["type", "id"] + }] + }, { "name": "union_a", "schemas": [{ "$id": "union_a", - "type": "object", + "$ref": "union_base", "properties": { - "type": { "const": "union_a" }, + "type": { "type": "string", "const": "union_a", "override": true }, "prop_a": { "type": "string" } }, - "required": ["type", "prop_a"] + "required": ["prop_a"] }] }, { "name": "union_b", "schemas": [{ "$id": "union_b", - "type": "object", + "$ref": "union_base", "properties": { - "type": { "const": "union_b" }, + "type": { "type": "string", "const": "union_b", "override": true }, "prop_b": { "type": "number" } }, - "required": ["type", "prop_b"] + "required": ["prop_b"] }] }, { "name": "union_c", "schemas": [{ "$id": "union_c", - "type": "object", + "$ref": "union_base", "properties": { - "type": { "const": "union_c" }, + "type": { "type": "string", "const": "union_c", "override": true }, "prop_c": { "type": "boolean" } }, - "required": ["type", "prop_c"] + "required": ["prop_c"] }] } ]); let puncs = json!([{ "name": "union_test", - "public": false, + "public": true, "schemas": [{ "$id": "union_test.request", "type": "object", @@ -975,23 +998,47 @@ pub fn union_schemas() -> JsonB { pub fn nullable_union_schemas() -> JsonB { let enums = json!([]); let types = json!([ + { + "name": "thing_base", + "schemas": [{ + "$id": "thing_base", + "type": "object", + "properties": { + "type": { "type": "string", "const": "thing_base" }, + "id": { "type": "string" } + }, + "required": ["type", "id"] + }] + }, { "name": "thing_a", "schemas": [{ "$id": "thing_a", - "type": "object", + "$ref": "thing_base", "properties": { - "type": { "const": "thing_a" }, + "type": { "type": "string", "const": "thing_a", "override": true }, "prop_a": { "type": "string" } }, - "required": ["type", "prop_a"] + "required": ["prop_a"] + }] + }, + { + "name": "thing_b", + "schemas": [{ + "$id": "thing_b", + "$ref": "thing_base", + "properties": { + "type": { "type": "string", "const": "thing_b", "override": true }, + "prop_b": { "type": "string" } + }, + "required": ["prop_b"] }] } ]); let puncs = json!([{ "name": "nullable_union_test", - "public": false, + "public": true, "schemas": [{ "$id": "nullable_union_test.request", "type": "object", @@ -999,6 +1046,7 @@ pub fn nullable_union_schemas() -> JsonB { "nullable_prop": { "oneOf": [ { "$ref": "thing_a" }, + { "$ref": "thing_b" }, { "type": "null" } ] } diff --git a/src/tests.rs b/src/tests.rs index 7a62925..ea5ef10 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -824,8 +824,8 @@ fn test_validate_type_matching() { "job_id": "job123" }); let result_invalid_job = validate_json_schema("job", jsonb(invalid_job)); - assert_error_count(&result_invalid_job, 1); - assert_has_error(&result_invalid_job, "TYPE_MISMATCH", "/type"); + assert_failure(&result_invalid_job); + assert_has_error(&result_invalid_job, "CONST_VIOLATED", "/type"); // 2. Test 'super_job' which extends 'job' let valid_super_job = json!({ @@ -854,9 +854,8 @@ fn test_validate_type_matching() { "manager_id": "mgr1" }); let result_invalid_short = validate_json_schema("super_job.short", jsonb(invalid_short_super_job)); - assert_error_count(&result_invalid_short, 1); - let error = find_error_with_code_and_path(&result_invalid_short, "TYPE_MISMATCH", "/type"); - assert_error_message_contains(error, "Instance type 'job' does not match expected type 'super_job'"); + assert_failure(&result_invalid_short); + assert_has_error(&result_invalid_short, "CONST_VIOLATED", "/type"); // 4. Test punc with root, nested, and oneOf type refs let valid_punc_instance = json!({ @@ -890,8 +889,8 @@ fn test_validate_type_matching() { } }); let result_invalid_punc_root = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_root)); - assert_error_count(&result_invalid_punc_root, 1); - assert_has_error(&result_invalid_punc_root, "TYPE_MISMATCH", "/root_job/type"); + assert_failure(&result_invalid_punc_root); + assert_has_error(&result_invalid_punc_root, "CONST_VIOLATED", "/root_job/type"); // 6. Test invalid type at punc nested ref let invalid_punc_nested = json!({ @@ -909,8 +908,8 @@ fn test_validate_type_matching() { } }); let result_invalid_punc_nested = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_nested)); - assert_error_count(&result_invalid_punc_nested, 1); - assert_has_error(&result_invalid_punc_nested, "TYPE_MISMATCH", "/nested_or_super_job/my_job/type"); + assert_failure(&result_invalid_punc_nested); + assert_has_error(&result_invalid_punc_nested, "CONST_VIOLATED", "/nested_or_super_job/my_job/type"); // 7. Test invalid type at punc oneOf ref let invalid_punc_oneof = json!({ @@ -927,8 +926,8 @@ fn test_validate_type_matching() { } }); let result_invalid_punc_oneof = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_oneof)); - // This will have multiple errors because the invalid oneOf branch will also fail the other branch's validation - assert_has_error(&result_invalid_punc_oneof, "TYPE_MISMATCH", "/nested_or_super_job/type"); + assert_failure(&result_invalid_punc_oneof); + assert_has_error(&result_invalid_punc_oneof, "CONST_VIOLATED", "/nested_or_super_job/type"); } #[pg_test] @@ -939,6 +938,7 @@ fn test_validate_union_type_matching() { // 1. Test valid instance with type 'union_a' let valid_instance_a = json!({ "union_prop": { + "id": "123", "type": "union_a", "prop_a": "hello" } @@ -949,6 +949,7 @@ fn test_validate_union_type_matching() { // 2. Test valid instance with type 'union_b' let valid_instance_b = json!({ "union_prop": { + "id": "456", "type": "union_b", "prop_b": 123 } @@ -956,50 +957,30 @@ fn test_validate_union_type_matching() { let result_b = validate_json_schema("union_test.request", jsonb(valid_instance_b)); assert_success(&result_b); - // 3. Test invalid instance - correct type, but fails sub-schema validation + // 3. Test invalid instance - wrong type const in a valid oneOf branch let invalid_sub_schema = json!({ "union_prop": { - "type": "union_a", - "prop_a": 123 // prop_a should be a string + "id": "789", + "type": "union_b", // Should be union_a + "prop_a": "hello" } }); let result_invalid_sub = validate_json_schema("union_test.request", jsonb(invalid_sub_schema)); - // Expect 4 errors because the instance fails validation against all 3 sub-schemas for different reasons, - // and the error collector flattens all unique-path errors. - assert_error_count(&result_invalid_sub, 4); - // The "correct" error from the matched branch 'union_a' - assert_has_error(&result_invalid_sub, "TYPE_MISMATCH", "/union_prop/prop_a"); - // Noise from failing the 'union_b' schema + assert_failure(&result_invalid_sub); + // This should fail because the `type` override in `union_a` is `const: "union_a"` assert_has_error(&result_invalid_sub, "CONST_VIOLATED", "/union_prop/type"); - assert_has_error(&result_invalid_sub, "REQUIRED_FIELD_MISSING", "/union_prop/prop_b"); - // Noise from failing the 'union_c' schema - assert_has_error(&result_invalid_sub, "REQUIRED_FIELD_MISSING", "/union_prop/prop_c"); - // 4. Test invalid instance - type does not match any union member - let invalid_type = json!({ + // 4. Test invalid instance - base type, should fail due to override + let invalid_base_type = json!({ "union_prop": { - "type": "union_d", // not a valid type in the oneOf - "prop_d": "whatever" + "id": "101", + "type": "union_base", // This is the base type, but the override should be enforced + "prop_a": "world" } }); - let result_invalid_type = validate_json_schema("union_test.request", jsonb(invalid_type)); - assert_error_count(&result_invalid_type, 4); - assert_has_error(&result_invalid_type, "CONST_VIOLATED", "/union_prop/type"); - assert_has_error(&result_invalid_type, "REQUIRED_FIELD_MISSING", "/union_prop/prop_a"); - assert_has_error(&result_invalid_type, "REQUIRED_FIELD_MISSING", "/union_prop/prop_b"); - assert_has_error(&result_invalid_type, "REQUIRED_FIELD_MISSING", "/union_prop/prop_c"); - - // 5. Test invalid instance - missing 'type' property for union - let missing_type = json!({ - "union_prop": { - "prop_a": "hello" // no 'type' field - } - }); - let result_missing_type = validate_json_schema("union_test.request", jsonb(missing_type)); - assert_error_count(&result_missing_type, 3); - assert_has_error(&result_missing_type, "REQUIRED_FIELD_MISSING", "/union_prop/type"); - assert_has_error(&result_missing_type, "REQUIRED_FIELD_MISSING", "/union_prop/prop_b"); - assert_has_error(&result_missing_type, "REQUIRED_FIELD_MISSING", "/union_prop/prop_c"); + let result_invalid_base = validate_json_schema("union_test.request", jsonb(invalid_base_type)); + assert_failure(&result_invalid_base); + assert_has_error(&result_invalid_base, "CONST_VIOLATED", "/union_prop/type"); } #[pg_test] @@ -1007,30 +988,52 @@ fn test_validate_nullable_union() { let cache_result = nullable_union_schemas(); assert_success(&cache_result); - // 1. Test valid instance with the object type - let valid_object = json!({ + // 1. Test valid instance with object type 'thing_a' + let valid_object_a = json!({ "nullable_prop": { + "id": "123", "type": "thing_a", "prop_a": "hello" } }); - let result_obj = validate_json_schema("nullable_union_test.request", jsonb(valid_object)); - assert_success(&result_obj); + let result_obj_a = validate_json_schema("nullable_union_test.request", jsonb(valid_object_a)); + assert_success(&result_obj_a); - // 2. Test valid instance with null + // 2. Test valid instance with object type 'thing_b' + let valid_object_b = json!({ + "nullable_prop": { + "id": "456", + "type": "thing_b", + "prop_b": "goodbye" + } + }); + let result_obj_b = validate_json_schema("nullable_union_test.request", jsonb(valid_object_b)); + assert_success(&result_obj_b); + + // 3. Test valid instance with null let valid_null = json!({ "nullable_prop": null }); let result_null = validate_json_schema("nullable_union_test.request", jsonb(valid_null)); assert_success(&result_null); - // 3. Test invalid instance (e.g., a string) + // 4. Test invalid instance - base type, should fail due to override + let invalid_base_type = json!({ + "nullable_prop": { + "id": "789", + "type": "thing_base", + "prop_a": "should fail" + } + }); + let result_invalid_base = validate_json_schema("nullable_union_test.request", jsonb(invalid_base_type)); + assert_failure(&result_invalid_base); + assert_has_error(&result_invalid_base, "CONST_VIOLATED", "/nullable_prop/type"); + + // 5. Test invalid instance (e.g., a string) let invalid_string = json!({ "nullable_prop": "not_an_object_or_null" }); let result_invalid = validate_json_schema("nullable_union_test.request", jsonb(invalid_string)); assert_failure(&result_invalid); - // The boon validator will report that the string doesn't match either schema in the oneOf. - // We expect at least one TYPE_MISMATCH error at the path of the property. assert_has_error(&result_invalid, "TYPE_MISMATCH", "/nullable_prop"); } \ No newline at end of file diff --git a/validator/src/compiler.rs b/validator/src/compiler.rs index 9bc3ac9..416dc96 100644 --- a/validator/src/compiler.rs +++ b/validator/src/compiler.rs @@ -370,7 +370,21 @@ impl ObjCompiler<'_, '_, '_, '_, '_, '_> { } } - s.properties = self.enqueue_map("properties"); + if let Some(Value::Object(props_obj)) = self.value("properties") { + let mut properties = AHashMap::with_capacity(props_obj.len()); + for (pname, pvalue) in props_obj { + let ptr = self.up.ptr.append2("properties", pname); + let sch_idx = self.enqueue_schema(ptr); + properties.insert(pname.clone(), sch_idx); + + if let Some(prop_schema_obj) = pvalue.as_object() { + if let Some(Value::Bool(true)) = prop_schema_obj.get("override") { + s.override_properties.insert(pname.clone()); + } + } + } + s.properties = properties; + } s.pattern_properties = { let mut v = vec![]; if let Some(Value::Object(obj)) = self.value("patternProperties") { diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 1dcaa7f..2589c7d 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -129,7 +129,7 @@ pub use { use std::{borrow::Cow, collections::HashMap, error::Error, fmt::Display}; -use ahash::AHashMap; +use ahash::{AHashMap, AHashSet}; use regex::Regex; use serde_json::{Number, Value}; use util::*; @@ -238,6 +238,7 @@ struct Schema { max_properties: Option, required: Vec, properties: AHashMap, + override_properties: AHashSet, pattern_properties: Vec<(Regex, SchemaIndex)>, property_names: Option, additional_properties: Option, diff --git a/validator/src/metaschemas/draft/2020-12/schema b/validator/src/metaschemas/draft/2020-12/schema index 364f8ad..1cce56a 100644 --- a/validator/src/metaschemas/draft/2020-12/schema +++ b/validator/src/metaschemas/draft/2020-12/schema @@ -24,6 +24,9 @@ "type": ["object", "boolean"], "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", "properties": { + "override": { + "type": "boolean" + }, "definitions": { "$comment": "\"definitions\" has been replaced by \"$defs\".", "type": "object", diff --git a/validator/src/validator.rs b/validator/src/validator.rs index 7ee04d9..e8fb75e 100644 --- a/validator/src/validator.rs +++ b/validator/src/validator.rs @@ -1,9 +1,13 @@ use std::{borrow::Cow, cmp::min, collections::HashSet, fmt::Write}; +use ahash::AHashSet; use serde_json::{Map, Value}; use crate::{util::*, *}; +#[derive(Default, Clone)] +struct Override<'s>(AHashSet<&'s str>); + macro_rules! prop { ($prop:expr) => { InstanceToken::Prop(Cow::Borrowed($prop)) @@ -37,6 +41,7 @@ pub(crate) fn validate<'s, 'v>( schemas, scope, options, + overrides: Override::default(), // Start with an empty override context uneval: Uneval::from(v, schema, options.be_strict), errors: vec![], bool_result: false, @@ -90,6 +95,7 @@ struct Validator<'v, 's, 'd, 'e> { schemas: &'s Schemas, scope: Scope<'d>, options: ValidationOptions, + overrides: Override<'s>, uneval: Uneval<'v>, errors: Vec>, bool_result: bool, // is interested to know valid or not (but not actuall error) @@ -190,7 +196,7 @@ impl<'v, 's> Validator<'v, 's, '_, '_> { } // type specific validations -impl<'v> Validator<'v, '_, '_, '_> { +impl<'v> Validator<'v, '_, '_,'_> { fn obj_validate(&mut self, obj: &'v Map) { let s = self.schema; macro_rules! add_err { @@ -244,6 +250,11 @@ impl<'v> Validator<'v, '_, '_, '_> { let mut additional_props = vec![]; for (pname, pvalue) in obj { + if self.overrides.0.contains(pname.as_str()) { + self.uneval.props.remove(pname); + continue; + } + if self.bool_result && !self.errors.is_empty() { return; } @@ -835,6 +846,11 @@ impl<'v, 's> Validator<'v, 's, '_, '_> { new_options.be_strict = false; } + let mut overrides = Override::default(); + for pname in &schema.override_properties { + overrides.0.insert(pname.as_str()); + } + let (result, _reply) = Validator { v, vloc: self.vloc, @@ -842,6 +858,7 @@ impl<'v, 's> Validator<'v, 's, '_, '_> { schemas: self.schemas, scope, options: new_options, + overrides, uneval: Uneval::from(v, schema, new_options.be_strict || !self.uneval.is_empty()), errors: vec![], bool_result: self.bool_result, @@ -872,6 +889,11 @@ impl<'v, 's> Validator<'v, 's, '_, '_> { new_options.be_strict = false; } + let mut overrides = self.overrides.clone(); + for pname in &self.schema.override_properties { + overrides.0.insert(pname.as_str()); + } + let (result, reply) = Validator { v: self.v, vloc: self.vloc, @@ -879,6 +901,7 @@ impl<'v, 's> Validator<'v, 's, '_, '_> { schemas: self.schemas, scope, options: new_options, + overrides, uneval: self.uneval.clone(), errors: vec![], bool_result: self.bool_result || bool_result,