#!/usr/bin/env python3 """ Simple JSON Schema Validator - validates JSON files against basic schema constraints. Created: 2024-10-21 """ import json import sys from pathlib import Path from typing import Any, Dict, List, Tuple def validate_type(value: Any, type_name: str) -> bool: """Check if value matches the expected JSON type.""" type_validators = { 'string': lambda v: isinstance(v, str), 'number': lambda v: isinstance(v, (int, float)) and not isinstance(v, bool), 'integer': lambda v: isinstance(v, int) and not isinstance(v, bool), 'boolean': lambda v: isinstance(v, bool), 'array': lambda v: isinstance(v, list), 'object': lambda v: isinstance(v, dict), 'null': lambda v: v is None, } validator = type_validators.get(type_name) return validator(value) if validator else False def validate_against_schema(data: Any, schema: Dict, path: str = "") -> Tuple[bool, List[str]]: """Recursively validate data against a JSON schema. Returns (valid, errors).""" errors = [] # Type checking if 'type' in schema: if not validate_type(data, schema['type']): errors.append(f"{path or '(root)'}: Expected type '{schema['type']}', got '{type(data).__name__}'") return False, errors # Object validation if isinstance(data, dict) and 'properties' in schema: props = schema['properties'] required = schema.get('required', []) for prop in required: if prop not in data: errors.append(f"{path}.{prop}: Required property missing") for prop, prop_schema in props.items(): if prop in data: sub_path = f"{path}.{prop}" if path else prop valid, sub_errors = validate_against_schema(data[prop], prop_schema, sub_path) errors.extend(sub_errors) # Array validation if isinstance(data, list) and 'items' in schema: for i, item in enumerate(data): sub_path = f"{path}[{i}]" valid, sub_errors = validate_against_schema(item, schema['items'], sub_path) errors.extend(sub_errors) # Enum validation if 'enum' in schema: if data not in schema['enum']: errors.append(f"{path or '(root)'}: Value '{data}' not in enum {schema['enum']}") # String constraints if isinstance(data, str): if 'minLength' in schema and len(data) < schema['minLength']: errors.append(f"{path or '(root)'}: String length {len(data)} < minLength {schema['minLength']}") if 'maxLength' in schema and len(data) > schema['maxLength']: errors.append(f"{path or '(root)'}: String length {len(data)} > maxLength {schema['maxLength']}") if 'pattern' in schema: import re if not re.match(schema['pattern'], data): errors.append(f"{path or '(root)'}: String '{data}' doesn't match pattern '{schema['pattern']}'") # Numeric constraints if isinstance(data, (int, float)) and not isinstance(data, bool): if 'minimum' in schema and data < schema['minimum']: errors.append(f"{path or '(root)'}: Value {data} < minimum {schema['minimum']}") if 'maximum' in schema and data > schema['maximum']: errors.append(f"{path or '(root)'}: Value {data} > maximum {schema['maximum']}") return len(errors) == 0, errors def validate_json_file(json_path: str, schema_path: str) -> bool: """Validate a JSON file against a schema file.""" try: with open(json_path) as f: data = json.load(f) except FileNotFoundError: print(f"Error: JSON file not found: {json_path}") return False except json.JSONDecodeError as e: print(f"Error: Invalid JSON in {json_path}: {e}") return False try: with open(schema_path) as f: schema = json.load(f) except FileNotFoundError: print(f"Error: Schema file not found: {schema_path}") return False except json.JSONDecodeError as e: print(f"Error: Invalid JSON in {schema_path}: {e}") return False valid, errors = validate_against_schema(data, schema) if valid: print(f"VALID: {json_path} conforms to schema") return True else: print(f"INVALID: {json_path} does not conform to schema") for err in errors: print(f" {err}") return False def run_self_test() -> bool: """Run self-validation tests against known test files.""" import tempfile import os test_passed = 0 test_failed = 0 # Test 1: Valid JSON against valid schema schema1 = { "type": "object", "properties": { "name": {"type": "string"}, "value": {"type": "integer", "minimum": 0} }, "required": ["name", "value"] } data1 = {"name": "test", "value": 42} valid, _ = validate_against_schema(data1, schema1) if valid: print("Test 1 PASSED: Valid object with required properties") test_passed += 1 else: print("Test 1 FAILED: Valid object should pass") test_failed += 1 # Test 2: Missing required property data2 = {"name": "test"} valid, errors = validate_against_schema(data2, schema1) if not valid and any("value" in e for e in errors): print("Test 2 PASSED: Missing required property detected") test_passed += 1 else: print("Test 2 FAILED: Should detect missing 'value'") test_failed += 1 # Test 3: Wrong type data3 = {"name": "test", "value": "not a number"} valid, errors = validate_against_schema(data3, schema1) if not valid and any("integer" in e for e in errors): print("Test 3 PASSED: Wrong type detected") test_passed += 1 else: print("Test 3 FAILED: Should detect wrong type") test_failed += 1 # Test 4: Value below minimum data4 = {"name": "test", "value": -5} valid, errors = validate_against_schema(data4, schema1) if not valid and any("minimum" in e for e in errors): print("Test 4 PASSED: Value below minimum detected") test_passed += 1 else: print("Test 4 FAILED: Should detect value below minimum") test_failed += 1 # Test 5: Array validation schema5 = { "type": "object", "properties": { "items": { "type": "array", "items": {"type": "string"} } } } data5 = {"items": ["a", "b", "c"]} valid, _ = validate_against_schema(data5, schema5) if valid: print("Test 5 PASSED: Valid array of strings") test_passed += 1 else: print("Test 5 FAILED: Valid array should pass") test_failed += 1 # Test 6: Array with wrong item type data6 = {"items": ["a", 123, "c"]} valid, errors = validate_against_schema(data6, schema5) if not valid and any("[1]" in e for e in errors): print("Test 6 PASSED: Array item type mismatch detected") test_passed += 1 else: print("Test 7 FAILED: Should detect array item type mismatch") test_failed += 1 # Test 7: Enum validation schema7 = { "type": "object", "properties": { "status": {"enum": ["active", "inactive", "pending"]} } } data7 = {"status": "active"} valid, _ = validate_against_schema(data7, schema7) if valid: print("Test 7 PASSED: Valid enum value") test_passed += 1 else: print("Test 7 FAILED: Valid enum should pass") test_failed += 1 # Test 8: Invalid enum value data8 = {"status": "unknown"} valid, errors = validate_against_schema(data8, schema7) if not valid and any("enum" in e for e in errors): print("Test 8 PASSED: Invalid enum value detected") test_passed += 1 else: print("Test 8 FAILED: Should detect invalid enum value") test_failed += 1 print(f"\nSelf-test results: {test_passed} passed, {test_failed} failed") return test_failed == 0 def main(): if len(sys.argv) >= 2 and sys.argv[1] == '--self-test': success = run_self_test() sys.exit(0 if success else 1) if len(sys.argv) < 3: print("Usage: json_schema_validator.py ") print(" json_schema_validator.py --self-test") sys.exit(1) json_path = sys.argv[1] schema_path = sys.argv[2] success = validate_json_file(json_path, schema_path) sys.exit(0 if success else 1) if __name__ == '__main__': main()