From 8644535fc7d9769397b02c170e6c39477974638c Mon Sep 17 00:00:00 2001 From: Ayush Saxena Date: Thu, 8 Oct 2026 22:28:08 +0530 Subject: [PATCH] fix(validation): reject a schema file that is valid JSON but not a schema --schema pointing at a JSON file whose top-level value is neither an object nor a boolean got past json.loads and then raised AttributeError from inside referencing -- "'list' object has no attribute 'get'" -- a traceback naming neither the schema nor the file it came from. main() now checks the parsed schema against the Draft 2020-12 metaschema and reports the first problem with the path to it, in the same form as a model diagnostic. That also covers an object whose keywords have the wrong shape: a string "required" used to make the validator demand one property per character, reporting four missing-property errors against the model for a mistake in the schema. true and false stay valid schemas and behave as before. --- validation/test_validate.py | 46 +++++++++++++++++++++++++++++++++++++ validation/validate.py | 14 +++++++++++ 2 files changed, 60 insertions(+) diff --git a/validation/test_validate.py b/validation/test_validate.py index 170e1bb2..f65e5f7a 100644 --- a/validation/test_validate.py +++ b/validation/test_validate.py @@ -598,6 +598,52 @@ def test_yaml_diagnostics_name_the_file_and_show_the_line(self): if has_snippet: self.assertIn("^", result.stdout) + def test_schema_that_is_not_an_object_is_reported(self): + """A schema may be an object or a boolean; any other JSON value is rejected.""" + for schema_text in ("[]", '"a string"', "3", "null", "[{}]"): + with self.subTest(schema=schema_text): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "model.yaml" + model_path.write_text(_MINIMAL_MODEL) + schema_path = Path(temp_dir) / "schema.json" + schema_path.write_text(schema_text) + result = self.run_validator_args(model_path, "--schema", schema_path) + + self.assertEqual(result.returncode, 1) + self.assertIn("Error: Invalid schema", result.stdout) + self.assertIn("is not of type 'object', 'boolean'", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_schema_with_a_malformed_keyword_is_reported(self): + """`required` taking a string used to make the validator demand its characters.""" + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "model.yaml" + model_path.write_text(_MINIMAL_MODEL) + schema_path = Path(temp_dir) / "schema.json" + schema_path.write_text('{"type": "object", "required": "name"}') + result = self.run_validator_args(model_path, "--schema", schema_path) + + self.assertEqual(result.returncode, 1) + self.assertIn("Error: Invalid schema", result.stdout) + self.assertIn("required", result.stdout) + self.assertNotIn("'a' is a required property", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_boolean_schemas_are_still_accepted(self): + """`true` and `false` are legal schemas, so the new check must let them through.""" + for schema_text, expected_code in (("true", 0), ("false", 1)): + with self.subTest(schema=schema_text): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "model.yaml" + model_path.write_text(_MINIMAL_MODEL) + schema_path = Path(temp_dir) / "schema.json" + schema_path.write_text(schema_text) + result = self.run_validator_args(model_path, "--schema", schema_path) + + self.assertEqual(result.returncode, expected_code) + self.assertNotIn("Error: Invalid schema", result.stdout) + self.assertNotIn("Traceback", result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/validation/validate.py b/validation/validate.py index 9fb63b16..3799b0eb 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -55,6 +55,7 @@ try: import yaml from jsonschema import Draft202012Validator + from jsonschema.exceptions import SchemaError from referencing import Registry from referencing.exceptions import NoSuchResource, Unresolvable from referencing.retrieval import to_cached_resource @@ -628,6 +629,19 @@ def main(): print(f"Error: Invalid JSON in schema {schema_path}: {e}") sys.exit(1) + # json.loads accepts any JSON value, but a schema has to be an object or a + # boolean. Handing the validator anything else raises AttributeError from inside + # referencing, which names neither the schema nor the file it came from. Checking + # against the metaschema also catches an object whose keywords are the wrong + # shape, such as a string "required", which otherwise validates the model against + # nonsense without complaint. + try: + Draft202012Validator.check_schema(schema) + except SchemaError as e: + location = " -> ".join(str(part) for part in e.absolute_path) if e.absolute_path else "(root)" + print(f"Error: Invalid schema {schema_path}: {location}: {e.message}") + sys.exit(1) + model_text = read_text_or_exit(yaml_path, str(yaml_path)) try: data = load_yaml_named(model_text, str(yaml_path))