unevaluatedProperties now cascade infinitely down their leaf when strict validation mode is on
This commit is contained in:
@ -308,7 +308,7 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match cache.schemas.validate(&instance_value, schema.index, options.as_ref()) {
|
||||
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 {
|
||||
|
||||
@ -355,7 +355,16 @@ pub fn additional_properties_schemas() -> JsonB {
|
||||
|
||||
pub fn unevaluated_properties_schemas() -> JsonB {
|
||||
let enums = json!([]);
|
||||
let types = json!([]);
|
||||
let types = json!([{
|
||||
"name": "nested_for_uneval",
|
||||
"schemas": [{
|
||||
"$id": "nested_for_uneval",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deep_prop": { "type": "string" }
|
||||
}
|
||||
}]
|
||||
}]);
|
||||
let puncs = json!([
|
||||
{
|
||||
"name": "simple_unevaluated_test",
|
||||
@ -396,6 +405,29 @@ pub fn unevaluated_properties_schemas() -> JsonB {
|
||||
},
|
||||
"unevaluatedProperties": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "nested_unevaluated_test",
|
||||
"public": true, // To trigger strict mode
|
||||
"schemas": [{
|
||||
"$id": "nested_unevaluated_test.request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"non_strict_branch": {
|
||||
"type": "object",
|
||||
"unevaluatedProperties": true, // The magic switch
|
||||
"properties": {
|
||||
"some_prop": { "$ref": "nested_for_uneval" }
|
||||
}
|
||||
},
|
||||
"strict_branch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"another_prop": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
27
src/tests.rs
27
src/tests.rs
@ -452,6 +452,33 @@ fn test_validate_unevaluated_properties() {
|
||||
|
||||
let valid_result = validate_json_schema("simple_unevaluated_test.request", jsonb(valid_instance));
|
||||
assert_success(&valid_result);
|
||||
|
||||
// Test 4: Test that unevaluatedProperties: true cascades down refs
|
||||
let cascading_instance = json!({
|
||||
"strict_branch": {
|
||||
"another_prop": "is_ok"
|
||||
},
|
||||
"non_strict_branch": {
|
||||
"extra_at_toplevel": "is_ok", // Extra property at this level
|
||||
"some_prop": {
|
||||
"deep_prop": "is_ok",
|
||||
"extra_in_ref": "is_also_ok" // Extra property in the $ref'd schema
|
||||
}
|
||||
}
|
||||
});
|
||||
let cascading_result = validate_json_schema("nested_unevaluated_test.request", jsonb(cascading_instance));
|
||||
assert_success(&cascading_result);
|
||||
|
||||
// Test 5: For good measure, test that the strict branch is still strict
|
||||
let strict_fail_instance = json!({
|
||||
"strict_branch": {
|
||||
"another_prop": "is_ok",
|
||||
"extra_in_strict": "is_not_ok"
|
||||
}
|
||||
});
|
||||
let strict_fail_result = validate_json_schema("nested_unevaluated_test.request", jsonb(strict_fail_instance));
|
||||
assert_error_count(&strict_fail_result, 1);
|
||||
assert_has_error(&strict_fail_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/strict_branch/extra_in_strict");
|
||||
}
|
||||
|
||||
#[pg_test]
|
||||
|
||||
@ -194,7 +194,7 @@ impl Schemas {
|
||||
&'s self,
|
||||
v: &'v Value,
|
||||
sch_index: SchemaIndex,
|
||||
options: Option<&'s ValidationOptions>,
|
||||
options: Option<ValidationOptions>,
|
||||
) -> Result<(), ValidationError<'s, 'v>> {
|
||||
let Some(sch) = self.list.get(sch_index.0) else {
|
||||
panic!("Schemas::validate: schema index out of bounds");
|
||||
|
||||
@ -20,7 +20,7 @@ pub(crate) fn validate<'s, 'v>(
|
||||
v: &'v Value,
|
||||
schema: &'s Schema,
|
||||
schemas: &'s Schemas,
|
||||
options: Option<&'s ValidationOptions>,
|
||||
options: Option<ValidationOptions>,
|
||||
) -> Result<(), ValidationError<'s, 'v>> {
|
||||
let scope = Scope {
|
||||
sch: schema.idx,
|
||||
@ -29,7 +29,7 @@ pub(crate) fn validate<'s, 'v>(
|
||||
parent: None,
|
||||
};
|
||||
let mut vloc = Vec::with_capacity(8);
|
||||
let be_strict = options.map_or(false, |o| o.be_strict);
|
||||
let options = options.unwrap_or_default();
|
||||
let (result, _) = Validator {
|
||||
v,
|
||||
vloc: &mut vloc,
|
||||
@ -37,7 +37,7 @@ pub(crate) fn validate<'s, 'v>(
|
||||
schemas,
|
||||
scope,
|
||||
options,
|
||||
uneval: Uneval::from(v, schema, be_strict),
|
||||
uneval: Uneval::from(v, schema, options.be_strict),
|
||||
errors: vec![],
|
||||
bool_result: false,
|
||||
}
|
||||
@ -89,7 +89,7 @@ struct Validator<'v, 's, 'd, 'e> {
|
||||
schema: &'s Schema,
|
||||
schemas: &'s Schemas,
|
||||
scope: Scope<'d>,
|
||||
options: Option<&'s ValidationOptions>,
|
||||
options: ValidationOptions,
|
||||
uneval: Uneval<'v>,
|
||||
errors: Vec<ValidationError<'s, 'v>>,
|
||||
bool_result: bool, // is interested to know valid or not (but not actuall error)
|
||||
@ -296,7 +296,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
|
||||
if let Some(sch) = &s.property_names {
|
||||
for pname in obj.keys() {
|
||||
let v = Value::String(pname.to_owned());
|
||||
if let Err(mut e) = self.schemas.validate(&v, *sch, self.options) {
|
||||
if let Err(mut e) = self.schemas.validate(&v, *sch, Some(self.options)) {
|
||||
e.schema_url = &s.loc;
|
||||
e.kind = ErrorKind::PropertyName {
|
||||
prop: pname.to_owned(),
|
||||
@ -510,7 +510,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
|
||||
|
||||
// contentSchema --
|
||||
if let (Some(sch), Some(v)) = (s.content_schema, deserialized) {
|
||||
if let Err(mut e) = self.schemas.validate(&v, sch, self.options) {
|
||||
if let Err(mut e) = self.schemas.validate(&v, sch, Some(self.options)) {
|
||||
e.schema_url = &s.loc;
|
||||
e.kind = kind!(ContentSchema);
|
||||
self.errors.push(e.clone_static());
|
||||
@ -762,8 +762,6 @@ impl Validator<'_, '_, '_, '_> {
|
||||
};
|
||||
}
|
||||
|
||||
let be_strict = self.options.map_or(false, |o| o.be_strict);
|
||||
|
||||
// unevaluatedProperties --
|
||||
if let Value::Object(obj) = v {
|
||||
if let Some(sch_idx) = s.unevaluated_properties {
|
||||
@ -786,7 +784,7 @@ impl Validator<'_, '_, '_, '_> {
|
||||
}
|
||||
self.uneval.props.clear();
|
||||
}
|
||||
} else if be_strict && !self.bool_result {
|
||||
} else if self.options.be_strict && !self.bool_result {
|
||||
// 2. Runtime strictness check
|
||||
if !self.uneval.props.is_empty() {
|
||||
let props: Vec<Cow<str>> = self.uneval.props.iter().map(|p| Cow::from((*p).as_str())).collect();
|
||||
@ -824,15 +822,27 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
|
||||
}
|
||||
let scope = self.scope.child(sch, None, self.scope.vid + 1);
|
||||
let schema = &self.schemas.get(sch);
|
||||
let be_strict = self.options.map_or(false, |o| o.be_strict);
|
||||
|
||||
// Check if the new schema turns off strictness
|
||||
let allows_unevaluated = schema.boolean == Some(true) ||
|
||||
if let Some(idx) = schema.unevaluated_properties {
|
||||
self.schemas.get(idx).boolean == Some(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let mut new_options = self.options;
|
||||
if allows_unevaluated {
|
||||
new_options.be_strict = false;
|
||||
}
|
||||
|
||||
let (result, _reply) = Validator {
|
||||
v,
|
||||
vloc: self.vloc,
|
||||
schema,
|
||||
schemas: self.schemas,
|
||||
scope,
|
||||
options: self.options,
|
||||
uneval: Uneval::from(v, schema, be_strict || !self.uneval.is_empty()),
|
||||
options: new_options,
|
||||
uneval: Uneval::from(v, schema, new_options.be_strict || !self.uneval.is_empty()),
|
||||
errors: vec![],
|
||||
bool_result: self.bool_result,
|
||||
}
|
||||
@ -849,13 +859,26 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
|
||||
) -> Result<(), ValidationError<'s, 'v>> {
|
||||
let scope = self.scope.child(sch, ref_kw, self.scope.vid);
|
||||
let schema = &self.schemas.get(sch);
|
||||
|
||||
// Check if the new schema turns off strictness
|
||||
let allows_unevaluated = schema.boolean == Some(true) ||
|
||||
if let Some(idx) = schema.unevaluated_properties {
|
||||
self.schemas.get(idx).boolean == Some(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let mut new_options = self.options;
|
||||
if allows_unevaluated {
|
||||
new_options.be_strict = false;
|
||||
}
|
||||
|
||||
let (result, reply) = Validator {
|
||||
v: self.v,
|
||||
vloc: self.vloc,
|
||||
schema,
|
||||
schemas: self.schemas,
|
||||
scope,
|
||||
options: self.options,
|
||||
options: new_options,
|
||||
uneval: self.uneval.clone(),
|
||||
errors: vec![],
|
||||
bool_result: self.bool_result || bool_result,
|
||||
|
||||
Reference in New Issue
Block a user