jspg union fixes
This commit is contained in:
47
src/lib.rs
47
src/lib.rs
@ -265,8 +265,51 @@ fn walk_and_validate_refs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(one_of_array) = schema.get("oneOf").and_then(|v| v.as_array()) {
|
if let Some(one_of_array) = schema.get("oneOf").and_then(|v| v.as_array()) {
|
||||||
for sub_schema in one_of_array {
|
let is_clean_ref_union = one_of_array.iter().all(|s| s.get("$ref").is_some());
|
||||||
walk_and_validate_refs(instance, sub_schema, cache, path_parts, type_validated, None, errors);
|
|
||||||
|
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::<Vec<_>>()
|
||||||
|
},
|
||||||
|
"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
src/schemas.rs
101
src/schemas.rs
@ -906,5 +906,106 @@ pub fn type_matching_schemas() -> JsonB {
|
|||||||
"required": ["root_job", "nested_or_super_job"]
|
"required": ["root_job", "nested_or_super_job"]
|
||||||
}]
|
}]
|
||||||
}]);
|
}]);
|
||||||
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn union_schemas() -> JsonB {
|
||||||
|
let enums = json!([]);
|
||||||
|
let types = json!([
|
||||||
|
{
|
||||||
|
"name": "union_a",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "union_a",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "union_a" },
|
||||||
|
"prop_a": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["type", "prop_a"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "union_b",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "union_b",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "union_b" },
|
||||||
|
"prop_b": { "type": "number" }
|
||||||
|
},
|
||||||
|
"required": ["type", "prop_b"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "union_c",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "union_c",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "union_c" },
|
||||||
|
"prop_c": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"required": ["type", "prop_c"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let puncs = json!([{
|
||||||
|
"name": "union_test",
|
||||||
|
"public": false,
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "union_test.request",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"union_prop": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "union_a" },
|
||||||
|
{ "$ref": "union_b" },
|
||||||
|
{ "$ref": "union_c" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["union_prop"]
|
||||||
|
}]
|
||||||
|
}]);
|
||||||
|
|
||||||
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nullable_union_schemas() -> JsonB {
|
||||||
|
let enums = json!([]);
|
||||||
|
let types = json!([
|
||||||
|
{
|
||||||
|
"name": "thing_a",
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "thing_a",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "thing_a" },
|
||||||
|
"prop_a": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["type", "prop_a"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let puncs = json!([{
|
||||||
|
"name": "nullable_union_test",
|
||||||
|
"public": false,
|
||||||
|
"schemas": [{
|
||||||
|
"$id": "nullable_union_test.request",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"nullable_prop": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "thing_a" },
|
||||||
|
{ "type": "null" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["nullable_prop"]
|
||||||
|
}]
|
||||||
|
}]);
|
||||||
|
|
||||||
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
cache_json_schemas(jsonb(enums), jsonb(types), jsonb(puncs))
|
||||||
}
|
}
|
||||||
104
src/tests.rs
104
src/tests.rs
@ -929,4 +929,108 @@ fn test_validate_type_matching() {
|
|||||||
let result_invalid_punc_oneof = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_oneof));
|
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
|
// 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_has_error(&result_invalid_punc_oneof, "TYPE_MISMATCH", "/nested_or_super_job/type");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pg_test]
|
||||||
|
fn test_validate_union_type_matching() {
|
||||||
|
let cache_result = union_schemas();
|
||||||
|
assert_success(&cache_result);
|
||||||
|
|
||||||
|
// 1. Test valid instance with type 'union_a'
|
||||||
|
let valid_instance_a = json!({
|
||||||
|
"union_prop": {
|
||||||
|
"type": "union_a",
|
||||||
|
"prop_a": "hello"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let result_a = validate_json_schema("union_test.request", jsonb(valid_instance_a));
|
||||||
|
assert_success(&result_a);
|
||||||
|
|
||||||
|
// 2. Test valid instance with type 'union_b'
|
||||||
|
let valid_instance_b = json!({
|
||||||
|
"union_prop": {
|
||||||
|
"type": "union_b",
|
||||||
|
"prop_b": 123
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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
|
||||||
|
let invalid_sub_schema = json!({
|
||||||
|
"union_prop": {
|
||||||
|
"type": "union_a",
|
||||||
|
"prop_a": 123 // prop_a should be a string
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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_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!({
|
||||||
|
"union_prop": {
|
||||||
|
"type": "union_d", // not a valid type in the oneOf
|
||||||
|
"prop_d": "whatever"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pg_test]
|
||||||
|
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!({
|
||||||
|
"nullable_prop": {
|
||||||
|
"type": "thing_a",
|
||||||
|
"prop_a": "hello"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let result_obj = validate_json_schema("nullable_union_test.request", jsonb(valid_object));
|
||||||
|
assert_success(&result_obj);
|
||||||
|
|
||||||
|
// 2. 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)
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user