Skip to content

JSON Data Validator

Abstract

JSON is the data format of the modern web — APIs, configs, telemetry, NoSQL stores all live in it. But JSON gives you no type safety: a misnamed field or out-of-range number sails through json.loads()json.loads() and silently breaks downstream code hours later. A schema validator catches those errors at the boundary, with precise error messages pointing at the offending field. In this project you build one from scratch — type checking, constraints, nested validation, path-tracked errors, batch validation, and sample-data generation — then compare it to the production-grade libraries jsonschemajsonschema and pydanticpydantic.

You will leave understanding:

  • The JSON Schema specification (the subset most apps actually use).
  • How to write a small recursive validator.
  • Path-tracked error messages (root.users[0].emailroot.users[0].email).
  • The difference between schema-based and class-based validation.
  • When to graduate to jsonschemajsonschema (for spec compliance) or pydanticpydantic (for type-driven validation).

Prerequisites

  • Python 3.7 or above (for dataclassesdataclasses).
  • A text editor or IDE.
  • Familiarity with JSON, dictionaries, and recursive functions.

Getting Started

Create the project

  1. Create folder json-validatorjson-validator.
  2. Inside, create jsondatavalidator.pyjsondatavalidator.py.

Write the code

JSON ValidatorSource
JSON Validator
# JSON Data Validator
 
import json
import os
import re
from typing import Any, Dict, List, Optional, Union, Tuple
from datetime import datetime
from pathlib import Path
 
class ValidationError:
    def __init__(self, path: str, message: str, expected: str = None, actual: str = None):
        self.path = path
        self.message = message
        self.expected = expected
        self.actual = actual
    
    def __str__(self):
        result = f"Path: {self.path} - {self.message}"
        if self.expected:
            result += f" (Expected: {self.expected}"
            if self.actual:
                result += f", Got: {self.actual}"
            result += ")"
        return result
 
class JSONSchema:
    def __init__(self, schema: Dict):
        self.schema = schema
        self.errors = []
    
    def validate(self, data: Any, path: str = "root") -> Tuple[bool, List[ValidationError]]:
        """Validate data against schema"""
        self.errors = []
        self._validate_recursive(data, self.schema, path)
        return len(self.errors) == 0, self.errors
    
    def _validate_recursive(self, data: Any, schema: Dict, path: str):
        """Recursively validate data against schema"""
        # Check type
        if "type" in schema:
            if not self._validate_type(data, schema["type"], path):
                return
        
        # Check required fields for objects
        if isinstance(data, dict) and "required" in schema:
            self._validate_required_fields(data, schema["required"], path)
        
        # Check properties for objects
        if isinstance(data, dict) and "properties" in schema:
            self._validate_properties(data, schema["properties"], path)
        
        # Check array items
        if isinstance(data, list) and "items" in schema:
            self._validate_array_items(data, schema["items"], path)
        
        # Check constraints
        self._validate_constraints(data, schema, path)
    
    def _validate_type(self, data: Any, expected_type: str, path: str) -> bool:
        """Validate data type"""
        type_mapping = {
            "string": str,
            "number": (int, float),
            "integer": int,
            "boolean": bool,
            "array": list,
            "object": dict,
            "null": type(None)
        }
        
        if expected_type not in type_mapping:
            self.errors.append(ValidationError(path, f"Unknown type: {expected_type}"))
            return False
        
        expected_python_type = type_mapping[expected_type]
        
        if not isinstance(data, expected_python_type):
            actual_type = type(data).__name__
            self.errors.append(ValidationError(
                path, "Type mismatch", expected_type, actual_type
            ))
            return False
        
        return True
    
    def _validate_required_fields(self, data: Dict, required_fields: List[str], path: str):
        """Validate required fields in object"""
        for field in required_fields:
            if field not in data:
                self.errors.append(ValidationError(
                    f"{path}.{field}", f"Required field '{field}' is missing"
                ))
    
    def _validate_properties(self, data: Dict, properties: Dict, path: str):
        """Validate object properties"""
        for key, value in data.items():
            if key in properties:
                self._validate_recursive(value, properties[key], f"{path}.{key}")
            # Note: Additional properties are allowed by default
    
    def _validate_array_items(self, data: List, items_schema: Dict, path: str):
        """Validate array items"""
        for i, item in enumerate(data):
            self._validate_recursive(item, items_schema, f"{path}[{i}]")
    
    def _validate_constraints(self, data: Any, schema: Dict, path: str):
        """Validate additional constraints"""
        # String constraints
        if isinstance(data, str):
            if "minLength" in schema and len(data) < schema["minLength"]:
                self.errors.append(ValidationError(
                    path, f"String too short (min: {schema['minLength']})", 
                    str(schema["minLength"]), str(len(data))
                ))
            
            if "maxLength" in schema and len(data) > schema["maxLength"]:
                self.errors.append(ValidationError(
                    path, f"String too long (max: {schema['maxLength']})",
                    str(schema["maxLength"]), str(len(data))
                ))
            
            if "pattern" in schema:
                if not re.match(schema["pattern"], data):
                    self.errors.append(ValidationError(
                        path, f"String does not match pattern: {schema['pattern']}"
                    ))
        
        # Number constraints
        if isinstance(data, (int, float)):
            if "minimum" in schema and data < schema["minimum"]:
                self.errors.append(ValidationError(
                    path, f"Number too small (min: {schema['minimum']})",
                    str(schema["minimum"]), str(data)
                ))
            
            if "maximum" in schema and data > schema["maximum"]:
                self.errors.append(ValidationError(
                    path, f"Number too large (max: {schema['maximum']})",
                    str(schema["maximum"]), str(data)
                ))
        
        # Array constraints
        if isinstance(data, list):
            if "minItems" in schema and len(data) < schema["minItems"]:
                self.errors.append(ValidationError(
                    path, f"Array too short (min items: {schema['minItems']})",
                    str(schema["minItems"]), str(len(data))
                ))
            
            if "maxItems" in schema and len(data) > schema["maxItems"]:
                self.errors.append(ValidationError(
                    path, f"Array too long (max items: {schema['maxItems']})",
                    str(schema["maxItems"]), str(len(data))
                ))
        
        # Enum constraint
        if "enum" in schema:
            if data not in schema["enum"]:
                self.errors.append(ValidationError(
                    path, f"Value not in allowed enum values: {schema['enum']}",
                    str(schema["enum"]), str(data)
                ))
 
class JSONDataValidator:
    def __init__(self):
        self.schemas = {}
        self.validation_results = []
    
    def load_schema_from_file(self, schema_file: str, schema_name: str = None) -> bool:
        """Load schema from JSON file"""
        try:
            with open(schema_file, 'r', encoding='utf-8') as f:
                schema_data = json.load(f)
            
            name = schema_name or Path(schema_file).stem
            self.schemas[name] = JSONSchema(schema_data)
            return True
            
        except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
            print(f"Error loading schema from {schema_file}: {e}")
            return False
    
    def add_schema(self, schema_name: str, schema_dict: Dict) -> bool:
        """Add schema from dictionary"""
        try:
            self.schemas[schema_name] = JSONSchema(schema_dict)
            return True
        except Exception as e:
            print(f"Error adding schema {schema_name}: {e}")
            return False
    
    def validate_file(self, json_file: str, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate JSON file against schema"""
        if schema_name not in self.schemas:
            error = ValidationError("", f"Schema '{schema_name}' not found")
            return False, [error]
        
        try:
            with open(json_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            return self.validate_data(data, schema_name)
            
        except FileNotFoundError:
            error = ValidationError("", f"File '{json_file}' not found")
            return False, [error]
        except json.JSONDecodeError as e:
            error = ValidationError("", f"Invalid JSON in file '{json_file}': {e}")
            return False, [error]
        except Exception as e:
            error = ValidationError("", f"Error reading file '{json_file}': {e}")
            return False, [error]
    
    def validate_data(self, data: Any, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate data against schema"""
        if schema_name not in self.schemas:
            error = ValidationError("", f"Schema '{schema_name}' not found")
            return False, [error]
        
        schema = self.schemas[schema_name]
        is_valid, errors = schema.validate(data)
        
        # Store result
        result = {
            'timestamp': datetime.now().isoformat(),
            'schema_name': schema_name,
            'is_valid': is_valid,
            'error_count': len(errors),
            'errors': [str(error) for error in errors]
        }
        self.validation_results.append(result)
        
        return is_valid, errors
    
    def validate_json_string(self, json_string: str, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate JSON string against schema"""
        try:
            data = json.loads(json_string)
            return self.validate_data(data, schema_name)
        except json.JSONDecodeError as e:
            error = ValidationError("", f"Invalid JSON string: {e}")
            return False, [error]
    
    def batch_validate(self, file_pattern: str, schema_name: str) -> Dict[str, Tuple[bool, List[ValidationError]]]:
        """Validate multiple files matching pattern"""
        from glob import glob
        
        results = {}
        files = glob(file_pattern)
        
        if not files:
            print(f"No files found matching pattern: {file_pattern}")
            return results
        
        for file_path in files:
            print(f"Validating {file_path}...")
            is_valid, errors = self.validate_file(file_path, schema_name)
            results[file_path] = (is_valid, errors)
        
        return results
    
    def get_schema_info(self, schema_name: str) -> Optional[Dict]:
        """Get information about a schema"""
        if schema_name not in self.schemas:
            return None
        
        schema = self.schemas[schema_name].schema
        
        def analyze_schema(schema_part):
            info = {}
            if "type" in schema_part:
                info["type"] = schema_part["type"]
            if "required" in schema_part:
                info["required_fields"] = schema_part["required"]
            if "properties" in schema_part:
                info["properties"] = {
                    prop: analyze_schema(prop_schema) 
                    for prop, prop_schema in schema_part["properties"].items()
                }
            return info
        
        return analyze_schema(schema)
    
    def create_sample_data(self, schema_name: str) -> Optional[Dict]:
        """Create sample data that conforms to schema"""
        if schema_name not in self.schemas:
            return None
        
        schema = self.schemas[schema_name].schema
        
        def generate_sample(schema_part):
            if "type" not in schema_part:
                return None
            
            data_type = schema_part["type"]
            
            if data_type == "string":
                if "enum" in schema_part:
                    return schema_part["enum"][0]
                return "sample_string"
            elif data_type == "number":
                return 42.0
            elif data_type == "integer":
                return 42
            elif data_type == "boolean":
                return True
            elif data_type == "array":
                if "items" in schema_part:
                    return [generate_sample(schema_part["items"])]
                return []
            elif data_type == "object":
                obj = {}
                if "properties" in schema_part:
                    for prop, prop_schema in schema_part["properties"].items():
                        obj[prop] = generate_sample(prop_schema)
                return obj
            elif data_type == "null":
                return None
            
            return None
        
        return generate_sample(schema)
    
    def export_validation_report(self, filename: str):
        """Export validation results to file"""
        try:
            report = {
                'generated_at': datetime.now().isoformat(),
                'total_validations': len(self.validation_results),
                'successful_validations': sum(1 for r in self.validation_results if r['is_valid']),
                'failed_validations': sum(1 for r in self.validation_results if not r['is_valid']),
                'results': self.validation_results
            }
            
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(report, f, indent=2)
            
            print(f"Validation report exported to {filename}")
            
        except Exception as e:
            print(f"Error exporting report: {e}")
    
    def get_validation_statistics(self) -> Dict:
        """Get statistics about validation results"""
        if not self.validation_results:
            return {}
        
        total = len(self.validation_results)
        successful = sum(1 for r in self.validation_results if r['is_valid'])
        failed = total - successful
        
        # Schema usage
        schema_usage = {}
        for result in self.validation_results:
            schema = result['schema_name']
            schema_usage[schema] = schema_usage.get(schema, 0) + 1
        
        # Most common errors
        all_errors = []
        for result in self.validation_results:
            all_errors.extend(result['errors'])
        
        return {
            'total_validations': total,
            'successful_validations': successful,
            'failed_validations': failed,
            'success_rate': (successful / total * 100) if total > 0 else 0,
            'schema_usage': schema_usage,
            'total_errors': len(all_errors),
            'loaded_schemas': list(self.schemas.keys())
        }
 
def create_sample_schemas():
    """Create some sample schemas for demonstration"""
    schemas = {
        "user": {
            "type": "object",
            "required": ["name", "email", "age"],
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 50
                },
                "email": {
                    "type": "string",
                    "pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
                },
                "age": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 150
                },
                "phone": {
                    "type": "string",
                    "pattern": r"^\+?[\d\s\-\(\)]+$"
                },
                "status": {
                    "type": "string",
                    "enum": ["active", "inactive", "pending"]
                }
            }
        },
        "product": {
            "type": "object",
            "required": ["name", "price", "category"],
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                },
                "price": {
                    "type": "number",
                    "minimum": 0
                },
                "category": {
                    "type": "string",
                    "enum": ["electronics", "clothing", "books", "home", "sports"]
                },
                "tags": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "maxItems": 10
                },
                "in_stock": {
                    "type": "boolean"
                }
            }
        },
        "config": {
            "type": "object",
            "required": ["app_name", "version"],
            "properties": {
                "app_name": {
                    "type": "string",
                    "minLength": 1
                },
                "version": {
                    "type": "string",
                    "pattern": r"^\d+\.\d+\.\d+$"
                },
                "debug": {
                    "type": "boolean"
                },
                "features": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "database": {
                    "type": "object",
                    "required": ["host", "port"],
                    "properties": {
                        "host": {
                            "type": "string"
                        },
                        "port": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 65535
                        },
                        "name": {
                            "type": "string"
                        }
                    }
                }
            }
        }
    }
    return schemas
 
def main():
    """Main function to run the JSON data validator"""
    validator = JSONDataValidator()
    
    # Load sample schemas
    sample_schemas = create_sample_schemas()
    for name, schema in sample_schemas.items():
        validator.add_schema(name, schema)
    
    while True:
        print("\n=== JSON Data Validator ===")
        print("1. Validate JSON file")
        print("2. Validate JSON string")
        print("3. Batch validate files")
        print("4. Load schema from file")
        print("5. Add schema manually")
        print("6. View schema info")
        print("7. Generate sample data")
        print("8. View validation statistics")
        print("9. Export validation report")
        print("10. List available schemas")
        print("0. Exit")
        
        try:
            choice = input("\nEnter your choice: ").strip()
            
            if choice == '1':
                json_file = input("Enter JSON file path: ").strip()
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    is_valid, errors = validator.validate_file(json_file, schema_name)
                    
                    if is_valid:
                        print("✅ Validation successful!")
                    else:
                        print("❌ Validation failed!")
                        print(f"Found {len(errors)} errors:")
                        for error in errors:
                            print(f"  • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '2':
                print("Enter JSON string (end with empty line):")
                json_lines = []
                while True:
                    line = input()
                    if line.strip() == "":
                        break
                    json_lines.append(line)
                
                json_string = '\n'.join(json_lines)
                
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    is_valid, errors = validator.validate_json_string(json_string, schema_name)
                    
                    if is_valid:
                        print("✅ Validation successful!")
                    else:
                        print("❌ Validation failed!")
                        print(f"Found {len(errors)} errors:")
                        for error in errors:
                            print(f"  • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '3':
                file_pattern = input("Enter file pattern (e.g., *.json, data/*.json): ").strip()
                
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    results = validator.batch_validate(file_pattern, schema_name)
                    
                    print(f"\nBatch validation results:")
                    for file_path, (is_valid, errors) in results.items():
                        status = "✅" if is_valid else "❌"
                        print(f"{status} {file_path}: {len(errors)} errors")
                        if errors and len(errors) <= 3:  # Show first few errors
                            for error in errors[:3]:
                                print(f"    • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '4':
                schema_file = input("Enter schema file path: ").strip()
                schema_name = input("Enter schema name (optional): ").strip()
                
                if validator.load_schema_from_file(schema_file, schema_name or None):
                    print("Schema loaded successfully!")
                else:
                    print("Failed to load schema.")
            
            elif choice == '5':
                schema_name = input("Enter schema name: ").strip()
                print("Enter schema JSON (end with empty line):")
                schema_lines = []
                while True:
                    line = input()
                    if line.strip() == "":
                        break
                    schema_lines.append(line)
                
                schema_string = '\n'.join(schema_lines)
                
                try:
                    schema_dict = json.loads(schema_string)
                    if validator.add_schema(schema_name, schema_dict):
                        print("Schema added successfully!")
                    else:
                        print("Failed to add schema.")
                except json.JSONDecodeError as e:
                    print(f"Invalid JSON schema: {e}")
            
            elif choice == '6':
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                info = validator.get_schema_info(schema_name)
                if info:
                    print(f"\nSchema '{schema_name}' information:")
                    print(json.dumps(info, indent=2))
                else:
                    print("Schema not found!")
            
            elif choice == '7':
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                sample_data = validator.create_sample_data(schema_name)
                if sample_data is not None:
                    print(f"\nSample data for '{schema_name}' schema:")
                    print(json.dumps(sample_data, indent=2))
                else:
                    print("Schema not found or couldn't generate sample data!")
            
            elif choice == '8':
                stats = validator.get_validation_statistics()
                if stats:
                    print("\n=== Validation Statistics ===")
                    print(f"Total validations: {stats['total_validations']}")
                    print(f"Successful: {stats['successful_validations']}")
                    print(f"Failed: {stats['failed_validations']}")
                    print(f"Success rate: {stats['success_rate']:.1f}%")
                    print(f"Total errors: {stats['total_errors']}")
                    
                    if stats['schema_usage']:
                        print("\nSchema usage:")
                        for schema, count in stats['schema_usage'].items():
                            print(f"  {schema}: {count} validations")
                    
                    print(f"\nLoaded schemas: {', '.join(stats['loaded_schemas'])}")
                else:
                    print("No validation statistics available.")
            
            elif choice == '9':
                filename = input("Enter report filename (e.g., validation_report.json): ").strip()
                if not filename:
                    filename = f"validation_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
                
                validator.export_validation_report(filename)
            
            elif choice == '10':
                print("\nAvailable schemas:")
                if validator.schemas:
                    for schema_name in validator.schemas.keys():
                        schema_info = validator.get_schema_info(schema_name)
                        schema_type = schema_info.get('type', 'unknown') if schema_info else 'unknown'
                        print(f"  • {schema_name} (type: {schema_type})")
                else:
                    print("  No schemas loaded.")
            
            elif choice == '0':
                print("Thank you for using JSON Data Validator!")
                break
            
            else:
                print("Invalid choice. Please try again.")
        
        except KeyboardInterrupt:
            print("\n\nGoodbye!")
            break
        except Exception as e:
            print(f"An error occurred: {e}")
 
if __name__ == "__main__":
    main()
 
JSON Validator
# JSON Data Validator
 
import json
import os
import re
from typing import Any, Dict, List, Optional, Union, Tuple
from datetime import datetime
from pathlib import Path
 
class ValidationError:
    def __init__(self, path: str, message: str, expected: str = None, actual: str = None):
        self.path = path
        self.message = message
        self.expected = expected
        self.actual = actual
    
    def __str__(self):
        result = f"Path: {self.path} - {self.message}"
        if self.expected:
            result += f" (Expected: {self.expected}"
            if self.actual:
                result += f", Got: {self.actual}"
            result += ")"
        return result
 
class JSONSchema:
    def __init__(self, schema: Dict):
        self.schema = schema
        self.errors = []
    
    def validate(self, data: Any, path: str = "root") -> Tuple[bool, List[ValidationError]]:
        """Validate data against schema"""
        self.errors = []
        self._validate_recursive(data, self.schema, path)
        return len(self.errors) == 0, self.errors
    
    def _validate_recursive(self, data: Any, schema: Dict, path: str):
        """Recursively validate data against schema"""
        # Check type
        if "type" in schema:
            if not self._validate_type(data, schema["type"], path):
                return
        
        # Check required fields for objects
        if isinstance(data, dict) and "required" in schema:
            self._validate_required_fields(data, schema["required"], path)
        
        # Check properties for objects
        if isinstance(data, dict) and "properties" in schema:
            self._validate_properties(data, schema["properties"], path)
        
        # Check array items
        if isinstance(data, list) and "items" in schema:
            self._validate_array_items(data, schema["items"], path)
        
        # Check constraints
        self._validate_constraints(data, schema, path)
    
    def _validate_type(self, data: Any, expected_type: str, path: str) -> bool:
        """Validate data type"""
        type_mapping = {
            "string": str,
            "number": (int, float),
            "integer": int,
            "boolean": bool,
            "array": list,
            "object": dict,
            "null": type(None)
        }
        
        if expected_type not in type_mapping:
            self.errors.append(ValidationError(path, f"Unknown type: {expected_type}"))
            return False
        
        expected_python_type = type_mapping[expected_type]
        
        if not isinstance(data, expected_python_type):
            actual_type = type(data).__name__
            self.errors.append(ValidationError(
                path, "Type mismatch", expected_type, actual_type
            ))
            return False
        
        return True
    
    def _validate_required_fields(self, data: Dict, required_fields: List[str], path: str):
        """Validate required fields in object"""
        for field in required_fields:
            if field not in data:
                self.errors.append(ValidationError(
                    f"{path}.{field}", f"Required field '{field}' is missing"
                ))
    
    def _validate_properties(self, data: Dict, properties: Dict, path: str):
        """Validate object properties"""
        for key, value in data.items():
            if key in properties:
                self._validate_recursive(value, properties[key], f"{path}.{key}")
            # Note: Additional properties are allowed by default
    
    def _validate_array_items(self, data: List, items_schema: Dict, path: str):
        """Validate array items"""
        for i, item in enumerate(data):
            self._validate_recursive(item, items_schema, f"{path}[{i}]")
    
    def _validate_constraints(self, data: Any, schema: Dict, path: str):
        """Validate additional constraints"""
        # String constraints
        if isinstance(data, str):
            if "minLength" in schema and len(data) < schema["minLength"]:
                self.errors.append(ValidationError(
                    path, f"String too short (min: {schema['minLength']})", 
                    str(schema["minLength"]), str(len(data))
                ))
            
            if "maxLength" in schema and len(data) > schema["maxLength"]:
                self.errors.append(ValidationError(
                    path, f"String too long (max: {schema['maxLength']})",
                    str(schema["maxLength"]), str(len(data))
                ))
            
            if "pattern" in schema:
                if not re.match(schema["pattern"], data):
                    self.errors.append(ValidationError(
                        path, f"String does not match pattern: {schema['pattern']}"
                    ))
        
        # Number constraints
        if isinstance(data, (int, float)):
            if "minimum" in schema and data < schema["minimum"]:
                self.errors.append(ValidationError(
                    path, f"Number too small (min: {schema['minimum']})",
                    str(schema["minimum"]), str(data)
                ))
            
            if "maximum" in schema and data > schema["maximum"]:
                self.errors.append(ValidationError(
                    path, f"Number too large (max: {schema['maximum']})",
                    str(schema["maximum"]), str(data)
                ))
        
        # Array constraints
        if isinstance(data, list):
            if "minItems" in schema and len(data) < schema["minItems"]:
                self.errors.append(ValidationError(
                    path, f"Array too short (min items: {schema['minItems']})",
                    str(schema["minItems"]), str(len(data))
                ))
            
            if "maxItems" in schema and len(data) > schema["maxItems"]:
                self.errors.append(ValidationError(
                    path, f"Array too long (max items: {schema['maxItems']})",
                    str(schema["maxItems"]), str(len(data))
                ))
        
        # Enum constraint
        if "enum" in schema:
            if data not in schema["enum"]:
                self.errors.append(ValidationError(
                    path, f"Value not in allowed enum values: {schema['enum']}",
                    str(schema["enum"]), str(data)
                ))
 
class JSONDataValidator:
    def __init__(self):
        self.schemas = {}
        self.validation_results = []
    
    def load_schema_from_file(self, schema_file: str, schema_name: str = None) -> bool:
        """Load schema from JSON file"""
        try:
            with open(schema_file, 'r', encoding='utf-8') as f:
                schema_data = json.load(f)
            
            name = schema_name or Path(schema_file).stem
            self.schemas[name] = JSONSchema(schema_data)
            return True
            
        except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
            print(f"Error loading schema from {schema_file}: {e}")
            return False
    
    def add_schema(self, schema_name: str, schema_dict: Dict) -> bool:
        """Add schema from dictionary"""
        try:
            self.schemas[schema_name] = JSONSchema(schema_dict)
            return True
        except Exception as e:
            print(f"Error adding schema {schema_name}: {e}")
            return False
    
    def validate_file(self, json_file: str, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate JSON file against schema"""
        if schema_name not in self.schemas:
            error = ValidationError("", f"Schema '{schema_name}' not found")
            return False, [error]
        
        try:
            with open(json_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            return self.validate_data(data, schema_name)
            
        except FileNotFoundError:
            error = ValidationError("", f"File '{json_file}' not found")
            return False, [error]
        except json.JSONDecodeError as e:
            error = ValidationError("", f"Invalid JSON in file '{json_file}': {e}")
            return False, [error]
        except Exception as e:
            error = ValidationError("", f"Error reading file '{json_file}': {e}")
            return False, [error]
    
    def validate_data(self, data: Any, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate data against schema"""
        if schema_name not in self.schemas:
            error = ValidationError("", f"Schema '{schema_name}' not found")
            return False, [error]
        
        schema = self.schemas[schema_name]
        is_valid, errors = schema.validate(data)
        
        # Store result
        result = {
            'timestamp': datetime.now().isoformat(),
            'schema_name': schema_name,
            'is_valid': is_valid,
            'error_count': len(errors),
            'errors': [str(error) for error in errors]
        }
        self.validation_results.append(result)
        
        return is_valid, errors
    
    def validate_json_string(self, json_string: str, schema_name: str) -> Tuple[bool, List[ValidationError]]:
        """Validate JSON string against schema"""
        try:
            data = json.loads(json_string)
            return self.validate_data(data, schema_name)
        except json.JSONDecodeError as e:
            error = ValidationError("", f"Invalid JSON string: {e}")
            return False, [error]
    
    def batch_validate(self, file_pattern: str, schema_name: str) -> Dict[str, Tuple[bool, List[ValidationError]]]:
        """Validate multiple files matching pattern"""
        from glob import glob
        
        results = {}
        files = glob(file_pattern)
        
        if not files:
            print(f"No files found matching pattern: {file_pattern}")
            return results
        
        for file_path in files:
            print(f"Validating {file_path}...")
            is_valid, errors = self.validate_file(file_path, schema_name)
            results[file_path] = (is_valid, errors)
        
        return results
    
    def get_schema_info(self, schema_name: str) -> Optional[Dict]:
        """Get information about a schema"""
        if schema_name not in self.schemas:
            return None
        
        schema = self.schemas[schema_name].schema
        
        def analyze_schema(schema_part):
            info = {}
            if "type" in schema_part:
                info["type"] = schema_part["type"]
            if "required" in schema_part:
                info["required_fields"] = schema_part["required"]
            if "properties" in schema_part:
                info["properties"] = {
                    prop: analyze_schema(prop_schema) 
                    for prop, prop_schema in schema_part["properties"].items()
                }
            return info
        
        return analyze_schema(schema)
    
    def create_sample_data(self, schema_name: str) -> Optional[Dict]:
        """Create sample data that conforms to schema"""
        if schema_name not in self.schemas:
            return None
        
        schema = self.schemas[schema_name].schema
        
        def generate_sample(schema_part):
            if "type" not in schema_part:
                return None
            
            data_type = schema_part["type"]
            
            if data_type == "string":
                if "enum" in schema_part:
                    return schema_part["enum"][0]
                return "sample_string"
            elif data_type == "number":
                return 42.0
            elif data_type == "integer":
                return 42
            elif data_type == "boolean":
                return True
            elif data_type == "array":
                if "items" in schema_part:
                    return [generate_sample(schema_part["items"])]
                return []
            elif data_type == "object":
                obj = {}
                if "properties" in schema_part:
                    for prop, prop_schema in schema_part["properties"].items():
                        obj[prop] = generate_sample(prop_schema)
                return obj
            elif data_type == "null":
                return None
            
            return None
        
        return generate_sample(schema)
    
    def export_validation_report(self, filename: str):
        """Export validation results to file"""
        try:
            report = {
                'generated_at': datetime.now().isoformat(),
                'total_validations': len(self.validation_results),
                'successful_validations': sum(1 for r in self.validation_results if r['is_valid']),
                'failed_validations': sum(1 for r in self.validation_results if not r['is_valid']),
                'results': self.validation_results
            }
            
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(report, f, indent=2)
            
            print(f"Validation report exported to {filename}")
            
        except Exception as e:
            print(f"Error exporting report: {e}")
    
    def get_validation_statistics(self) -> Dict:
        """Get statistics about validation results"""
        if not self.validation_results:
            return {}
        
        total = len(self.validation_results)
        successful = sum(1 for r in self.validation_results if r['is_valid'])
        failed = total - successful
        
        # Schema usage
        schema_usage = {}
        for result in self.validation_results:
            schema = result['schema_name']
            schema_usage[schema] = schema_usage.get(schema, 0) + 1
        
        # Most common errors
        all_errors = []
        for result in self.validation_results:
            all_errors.extend(result['errors'])
        
        return {
            'total_validations': total,
            'successful_validations': successful,
            'failed_validations': failed,
            'success_rate': (successful / total * 100) if total > 0 else 0,
            'schema_usage': schema_usage,
            'total_errors': len(all_errors),
            'loaded_schemas': list(self.schemas.keys())
        }
 
def create_sample_schemas():
    """Create some sample schemas for demonstration"""
    schemas = {
        "user": {
            "type": "object",
            "required": ["name", "email", "age"],
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 50
                },
                "email": {
                    "type": "string",
                    "pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
                },
                "age": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 150
                },
                "phone": {
                    "type": "string",
                    "pattern": r"^\+?[\d\s\-\(\)]+$"
                },
                "status": {
                    "type": "string",
                    "enum": ["active", "inactive", "pending"]
                }
            }
        },
        "product": {
            "type": "object",
            "required": ["name", "price", "category"],
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                },
                "price": {
                    "type": "number",
                    "minimum": 0
                },
                "category": {
                    "type": "string",
                    "enum": ["electronics", "clothing", "books", "home", "sports"]
                },
                "tags": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "maxItems": 10
                },
                "in_stock": {
                    "type": "boolean"
                }
            }
        },
        "config": {
            "type": "object",
            "required": ["app_name", "version"],
            "properties": {
                "app_name": {
                    "type": "string",
                    "minLength": 1
                },
                "version": {
                    "type": "string",
                    "pattern": r"^\d+\.\d+\.\d+$"
                },
                "debug": {
                    "type": "boolean"
                },
                "features": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "database": {
                    "type": "object",
                    "required": ["host", "port"],
                    "properties": {
                        "host": {
                            "type": "string"
                        },
                        "port": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 65535
                        },
                        "name": {
                            "type": "string"
                        }
                    }
                }
            }
        }
    }
    return schemas
 
def main():
    """Main function to run the JSON data validator"""
    validator = JSONDataValidator()
    
    # Load sample schemas
    sample_schemas = create_sample_schemas()
    for name, schema in sample_schemas.items():
        validator.add_schema(name, schema)
    
    while True:
        print("\n=== JSON Data Validator ===")
        print("1. Validate JSON file")
        print("2. Validate JSON string")
        print("3. Batch validate files")
        print("4. Load schema from file")
        print("5. Add schema manually")
        print("6. View schema info")
        print("7. Generate sample data")
        print("8. View validation statistics")
        print("9. Export validation report")
        print("10. List available schemas")
        print("0. Exit")
        
        try:
            choice = input("\nEnter your choice: ").strip()
            
            if choice == '1':
                json_file = input("Enter JSON file path: ").strip()
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    is_valid, errors = validator.validate_file(json_file, schema_name)
                    
                    if is_valid:
                        print("✅ Validation successful!")
                    else:
                        print("❌ Validation failed!")
                        print(f"Found {len(errors)} errors:")
                        for error in errors:
                            print(f"  • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '2':
                print("Enter JSON string (end with empty line):")
                json_lines = []
                while True:
                    line = input()
                    if line.strip() == "":
                        break
                    json_lines.append(line)
                
                json_string = '\n'.join(json_lines)
                
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    is_valid, errors = validator.validate_json_string(json_string, schema_name)
                    
                    if is_valid:
                        print("✅ Validation successful!")
                    else:
                        print("❌ Validation failed!")
                        print(f"Found {len(errors)} errors:")
                        for error in errors:
                            print(f"  • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '3':
                file_pattern = input("Enter file pattern (e.g., *.json, data/*.json): ").strip()
                
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                if schema_name in validator.schemas:
                    results = validator.batch_validate(file_pattern, schema_name)
                    
                    print(f"\nBatch validation results:")
                    for file_path, (is_valid, errors) in results.items():
                        status = "✅" if is_valid else "❌"
                        print(f"{status} {file_path}: {len(errors)} errors")
                        if errors and len(errors) <= 3:  # Show first few errors
                            for error in errors[:3]:
                                print(f"    • {error}")
                else:
                    print("Schema not found!")
            
            elif choice == '4':
                schema_file = input("Enter schema file path: ").strip()
                schema_name = input("Enter schema name (optional): ").strip()
                
                if validator.load_schema_from_file(schema_file, schema_name or None):
                    print("Schema loaded successfully!")
                else:
                    print("Failed to load schema.")
            
            elif choice == '5':
                schema_name = input("Enter schema name: ").strip()
                print("Enter schema JSON (end with empty line):")
                schema_lines = []
                while True:
                    line = input()
                    if line.strip() == "":
                        break
                    schema_lines.append(line)
                
                schema_string = '\n'.join(schema_lines)
                
                try:
                    schema_dict = json.loads(schema_string)
                    if validator.add_schema(schema_name, schema_dict):
                        print("Schema added successfully!")
                    else:
                        print("Failed to add schema.")
                except json.JSONDecodeError as e:
                    print(f"Invalid JSON schema: {e}")
            
            elif choice == '6':
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                info = validator.get_schema_info(schema_name)
                if info:
                    print(f"\nSchema '{schema_name}' information:")
                    print(json.dumps(info, indent=2))
                else:
                    print("Schema not found!")
            
            elif choice == '7':
                print("\nAvailable schemas:")
                for schema_name in validator.schemas.keys():
                    print(f"  • {schema_name}")
                
                schema_name = input("Enter schema name: ").strip()
                
                sample_data = validator.create_sample_data(schema_name)
                if sample_data is not None:
                    print(f"\nSample data for '{schema_name}' schema:")
                    print(json.dumps(sample_data, indent=2))
                else:
                    print("Schema not found or couldn't generate sample data!")
            
            elif choice == '8':
                stats = validator.get_validation_statistics()
                if stats:
                    print("\n=== Validation Statistics ===")
                    print(f"Total validations: {stats['total_validations']}")
                    print(f"Successful: {stats['successful_validations']}")
                    print(f"Failed: {stats['failed_validations']}")
                    print(f"Success rate: {stats['success_rate']:.1f}%")
                    print(f"Total errors: {stats['total_errors']}")
                    
                    if stats['schema_usage']:
                        print("\nSchema usage:")
                        for schema, count in stats['schema_usage'].items():
                            print(f"  {schema}: {count} validations")
                    
                    print(f"\nLoaded schemas: {', '.join(stats['loaded_schemas'])}")
                else:
                    print("No validation statistics available.")
            
            elif choice == '9':
                filename = input("Enter report filename (e.g., validation_report.json): ").strip()
                if not filename:
                    filename = f"validation_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
                
                validator.export_validation_report(filename)
            
            elif choice == '10':
                print("\nAvailable schemas:")
                if validator.schemas:
                    for schema_name in validator.schemas.keys():
                        schema_info = validator.get_schema_info(schema_name)
                        schema_type = schema_info.get('type', 'unknown') if schema_info else 'unknown'
                        print(f"  • {schema_name} (type: {schema_type})")
                else:
                    print("  No schemas loaded.")
            
            elif choice == '0':
                print("Thank you for using JSON Data Validator!")
                break
            
            else:
                print("Invalid choice. Please try again.")
        
        except KeyboardInterrupt:
            print("\n\nGoodbye!")
            break
        except Exception as e:
            print(f"An error occurred: {e}")
 
if __name__ == "__main__":
    main()
 

Run it

run
python jsondatavalidator.py
run
python jsondatavalidator.py
text
1. Validate file
2. Validate string
3. Batch validate
4. Generate sample
5. Statistics
6. Quit
> 1
Schema: user
File: data.json
✅ Valid
text
1. Validate file
2. Validate string
3. Batch validate
4. Generate sample
5. Statistics
6. Quit
> 1
Schema: user
File: data.json
✅ Valid

A Tiny JSON Schema Example

user_schema.json
{
  "type": "object",
  "required": ["name", "email", "age"],
  "properties": {
    "name":   {"type": "string", "minLength": 2, "maxLength": 50},
    "email":  {"type": "string", "pattern": "^.+@.+\\..+$"},
    "age":    {"type": "integer", "minimum": 0, "maximum": 150},
    "status": {"type": "string", "enum": ["active", "inactive", "pending"]},
    "tags":   {"type": "array", "items": {"type": "string"}, "maxItems": 10}
  }
}
user_schema.json
{
  "type": "object",
  "required": ["name", "email", "age"],
  "properties": {
    "name":   {"type": "string", "minLength": 2, "maxLength": 50},
    "email":  {"type": "string", "pattern": "^.+@.+\\..+$"},
    "age":    {"type": "integer", "minimum": 0, "maximum": 150},
    "status": {"type": "string", "enum": ["active", "inactive", "pending"]},
    "tags":   {"type": "array", "items": {"type": "string"}, "maxItems": 10}
  }
}

Matching data:

user.json
{"name": "Madhur", "email": "m@example.com", "age": 30, "status": "active"}
user.json
{"name": "Madhur", "email": "m@example.com", "age": 30, "status": "active"}

Step-by-Step Explanation

1. The error type

error.py
from dataclasses import dataclass
 
@dataclass
class ValidationError:
    path: str            # e.g. "root.users[0].email"
    message: str         # e.g. "expected string, got int"
    expected: str = ""
    actual: str = ""
 
    def __str__(self):
        return f"{self.path}: {self.message}" + (
            f" (expected {self.expected}, got {self.actual})"
            if self.expected else "")
error.py
from dataclasses import dataclass
 
@dataclass
class ValidationError:
    path: str            # e.g. "root.users[0].email"
    message: str         # e.g. "expected string, got int"
    expected: str = ""
    actual: str = ""
 
    def __str__(self):
        return f"{self.path}: {self.message}" + (
            f" (expected {self.expected}, got {self.actual})"
            if self.expected else "")

Path-tracked errors are the single biggest UX improvement over a plain True/FalseTrue/False validator. Users learn what is wrong where.

2. The validator

validator.py
import re
 
TYPE_MAP = {
    "string": str, "integer": int, "number": (int, float),
    "boolean": bool, "array": list, "object": dict, "null": type(None),
}
 
def validate(data, schema, path="root") -> list[ValidationError]:
    errors = []
    expected_type = schema.get("type")
    if expected_type:
        py_type = TYPE_MAP.get(expected_type)
        if py_type and not isinstance(data, py_type):
            errors.append(ValidationError(path, "wrong type",
                                          expected=expected_type,
                                          actual=type(data).__name__))
            return errors                     # type mismatch — skip further checks
 
    if expected_type == "string":
        if "minLength" in schema and len(data) < schema["minLength"]:
            errors.append(ValidationError(path, f"shorter than {schema['minLength']}"))
        if "maxLength" in schema and len(data) > schema["maxLength"]:
            errors.append(ValidationError(path, f"longer than {schema['maxLength']}"))
        if "pattern" in schema and not re.match(schema["pattern"], data):
            errors.append(ValidationError(path, f"does not match pattern {schema['pattern']}"))
        if "enum" in schema and data not in schema["enum"]:
            errors.append(ValidationError(path,
                f"not in {schema['enum']}", actual=str(data)))
 
    elif expected_type in ("integer", "number"):
        if "minimum" in schema and data < schema["minimum"]:
            errors.append(ValidationError(path, f"less than {schema['minimum']}"))
        if "maximum" in schema and data > schema["maximum"]:
            errors.append(ValidationError(path, f"greater than {schema['maximum']}"))
 
    elif expected_type == "array":
        if "minItems" in schema and len(data) < schema["minItems"]:
            errors.append(ValidationError(path, f"fewer than {schema['minItems']} items"))
        if "maxItems" in schema and len(data) > schema["maxItems"]:
            errors.append(ValidationError(path, f"more than {schema['maxItems']} items"))
        if "uniqueItems" in schema and len(set(map(repr, data))) != len(data):
            errors.append(ValidationError(path, "items not unique"))
        if "items" in schema:
            for i, item in enumerate(data):
                errors += validate(item, schema["items"], f"{path}[{i}]")
 
    elif expected_type == "object":
        for key in schema.get("required", []):
            if key not in data:
                errors.append(ValidationError(path, f"missing required '{key}'"))
        for key, sub in schema.get("properties", {}).items():
            if key in data:
                errors += validate(data[key], sub, f"{path}.{key}")
        if schema.get("additionalProperties") is False:
            extras = set(data) - set(schema.get("properties", {}))
            for k in extras:
                errors.append(ValidationError(path, f"unexpected property '{k}'"))
 
    return errors
validator.py
import re
 
TYPE_MAP = {
    "string": str, "integer": int, "number": (int, float),
    "boolean": bool, "array": list, "object": dict, "null": type(None),
}
 
def validate(data, schema, path="root") -> list[ValidationError]:
    errors = []
    expected_type = schema.get("type")
    if expected_type:
        py_type = TYPE_MAP.get(expected_type)
        if py_type and not isinstance(data, py_type):
            errors.append(ValidationError(path, "wrong type",
                                          expected=expected_type,
                                          actual=type(data).__name__))
            return errors                     # type mismatch — skip further checks
 
    if expected_type == "string":
        if "minLength" in schema and len(data) < schema["minLength"]:
            errors.append(ValidationError(path, f"shorter than {schema['minLength']}"))
        if "maxLength" in schema and len(data) > schema["maxLength"]:
            errors.append(ValidationError(path, f"longer than {schema['maxLength']}"))
        if "pattern" in schema and not re.match(schema["pattern"], data):
            errors.append(ValidationError(path, f"does not match pattern {schema['pattern']}"))
        if "enum" in schema and data not in schema["enum"]:
            errors.append(ValidationError(path,
                f"not in {schema['enum']}", actual=str(data)))
 
    elif expected_type in ("integer", "number"):
        if "minimum" in schema and data < schema["minimum"]:
            errors.append(ValidationError(path, f"less than {schema['minimum']}"))
        if "maximum" in schema and data > schema["maximum"]:
            errors.append(ValidationError(path, f"greater than {schema['maximum']}"))
 
    elif expected_type == "array":
        if "minItems" in schema and len(data) < schema["minItems"]:
            errors.append(ValidationError(path, f"fewer than {schema['minItems']} items"))
        if "maxItems" in schema and len(data) > schema["maxItems"]:
            errors.append(ValidationError(path, f"more than {schema['maxItems']} items"))
        if "uniqueItems" in schema and len(set(map(repr, data))) != len(data):
            errors.append(ValidationError(path, "items not unique"))
        if "items" in schema:
            for i, item in enumerate(data):
                errors += validate(item, schema["items"], f"{path}[{i}]")
 
    elif expected_type == "object":
        for key in schema.get("required", []):
            if key not in data:
                errors.append(ValidationError(path, f"missing required '{key}'"))
        for key, sub in schema.get("properties", {}).items():
            if key in data:
                errors += validate(data[key], sub, f"{path}.{key}")
        if schema.get("additionalProperties") is False:
            extras = set(data) - set(schema.get("properties", {}))
            for k in extras:
                errors.append(ValidationError(path, f"unexpected property '{k}'"))
 
    return errors

Two key design points:

  • One recursive function handles every level. Nested schemas validate themselves naturally.
  • Errors accumulate rather than short-circuiting. Users see all problems at once.

3. Validate a file

validate_file.py
import json
from pathlib import Path
 
def validate_file(path: str, schema: dict):
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    errors = validate(data, schema)
    return (not errors), errors
 
ok, errs = validate_file("user.json", user_schema)
print("Valid!" if ok else "\n".join(str(e) for e in errs))
validate_file.py
import json
from pathlib import Path
 
def validate_file(path: str, schema: dict):
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    errors = validate(data, schema)
    return (not errors), errors
 
ok, errs = validate_file("user.json", user_schema)
print("Valid!" if ok else "\n".join(str(e) for e in errs))

4. Batch validate with glob patterns

batch.py
def batch(pattern: str, schema: dict):
    for p in Path().glob(pattern):
        ok, errs = validate_file(str(p), schema)
        print(f"{'✅' if ok else '❌'} {p}  ({len(errs)} errors)")
 
batch("data/*.json", user_schema)
batch.py
def batch(pattern: str, schema: dict):
    for p in Path().glob(pattern):
        ok, errs = validate_file(str(p), schema)
        print(f"{'✅' if ok else '❌'} {p}  ({len(errs)} errors)")
 
batch("data/*.json", user_schema)

5. Generate sample data

Useful for fixtures and tests:

sample.py
def sample(schema: dict):
    t = schema.get("type")
    if t == "string":  return schema.get("enum", ["example"])[0]
    if t == "integer": return schema.get("minimum", 0)
    if t == "number":  return float(schema.get("minimum", 0))
    if t == "boolean": return True
    if t == "array":   return [sample(schema["items"])] if "items" in schema else []
    if t == "object":
        return {k: sample(v) for k, v in schema.get("properties", {}).items()}
    return None
sample.py
def sample(schema: dict):
    t = schema.get("type")
    if t == "string":  return schema.get("enum", ["example"])[0]
    if t == "integer": return schema.get("minimum", 0)
    if t == "number":  return float(schema.get("minimum", 0))
    if t == "boolean": return True
    if t == "array":   return [sample(schema["items"])] if "items" in schema else []
    if t == "object":
        return {k: sample(v) for k, v in schema.get("properties", {}).items()}
    return None

Run on a schema → get a fully-typed valid JSON example for documentation or tests.

Common Mistakes

ProblemCauseFix
TrueTrue passes "type": "integer""type": "integer" checkPython’s boolbool is a subclass of intintCheck type(x) is inttype(x) is int, not isinstanceisinstance
Pattern always matchesUsed re.matchre.match for partial matchesUse re.fullmatchre.fullmatch for full-string match
Error message says “root” onlyDid not pass path downAlways include f"{path}.{key}"f"{path}.{key}" in recursive call
Numbers wrongly rejectedForgot int + floatint + float for "number""number"TYPE_MAP["number"] = (int, float)TYPE_MAP["number"] = (int, float)
Nested additionalPropertiesadditionalProperties not enforcedOnly checked at top levelRecurse properly
Schema author typo silently ignoredNo schema-of-schemas checkValidate the schema itself before using

Comparison to Production Libraries

jsonschemajsonschema (spec-compliant)

install
pip install jsonschema
install
pip install jsonschema
jsonschema_use.py
from jsonschema import validate, ValidationError
try:
    validate(instance=data, schema=schema)
except ValidationError as e:
    print(e.message, e.absolute_path)
jsonschema_use.py
from jsonschema import validate, ValidationError
try:
    validate(instance=data, schema=schema)
except ValidationError as e:
    print(e.message, e.absolute_path)

Handles full Draft 2020-12 spec: $ref$ref, oneOfoneOf, anyOfanyOf, allOfallOf, conditional schemas, format checks, custom keywords.

pydanticpydantic (type-driven)

install
pip install pydantic
install
pip install pydantic
pydantic_use.py
from pydantic import BaseModel, Field, EmailStr
 
class User(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    email: EmailStr
    age: int = Field(ge=0, le=150)
 
User.model_validate({"name": "M", "email": "m@x.com", "age": 30})
# raises ValidationError with errors at .errors()
pydantic_use.py
from pydantic import BaseModel, Field, EmailStr
 
class User(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    email: EmailStr
    age: int = Field(ge=0, le=150)
 
User.model_validate({"name": "M", "email": "m@x.com", "age": 30})
# raises ValidationError with errors at .errors()

Schemas are Python classes. Faster, friendlier, IDE-autocomplete-aware. Used by FastAPI, LangChain, and most modern Python web stacks.

When to use which

UseTool
Quick one-off checkBuild it yourself (this project)
Full JSON Schema compliancejsonschemajsonschema
Pythonic API definitionspydanticpydantic
OpenAPI auto-generationpydanticpydantic + FastAPI
Schema generation for non-Python consumersjsonschemajsonschema (export from pydanticpydantic)

Variations to Try

1. Custom keywords

Add "isEmail""isEmail" or "isUUID""isUUID" as schema-level shortcuts that map to specific patterns.

2. $ref$ref resolution

Lets schemas reference each other:

ref.json
{"address": {"$ref": "#/definitions/address"}}
ref.json
{"address": {"$ref": "#/definitions/address"}}

3. oneOfoneOf / anyOfanyOf / allOfallOf

Compose schemas. Email is “string AND matches email pattern AND length ≤ 254”.

4. JSON Schema → markdown docs

Walk the schema and generate human-readable documentation for an API.

5. CLI tool

cli
validator schema.json data.json
validator --watch schema.json incoming/*.json
cli
validator schema.json data.json
validator --watch schema.json incoming/*.json

6. Web service

Flask endpoint that accepts schema + data and returns {"valid": bool, "errors": [...]}{"valid": bool, "errors": [...]}. See Basic Web Server.

7. Live validation in a GUI

Tkinter with a JSON text area and a schema dropdown — errors highlight in real time.

8. Streaming validation

Validate huge JSON Lines files line-by-line with ijsonijson to keep memory bounded.

9. Schema inference

Read a sample of valid JSON, infer a permissive schema from it. Useful for legacy systems with no docs.

10. Mock data generation

sample(schema)sample(schema) already does the basic case. Add randomrandom for varied output, plus locale-aware names/emails via FakerFaker.

Real-World Applications

  • API request/response validation — at every layer of a microservices system.
  • Config-file validation — fail fast on bad YAML/JSON at startup.
  • ETL pipelines — reject malformed records before they corrupt downstream.
  • CI / data-quality — automated checks that incoming data matches contract.
  • Documentation generation — schemas become user-facing docs automatically.
  • OpenAPI / Swagger — REST APIs document themselves via JSON Schema.

Educational Value

  • Recursive algorithms — schema and data are both trees; you walk them in parallel.
  • Error reporting UX — path tracking is the difference between debug joy and debug despair.
  • Standards (JSON Schema spec) — interoperability with thousands of tools.
  • Library evaluation — knowing when “build it yourself” beats “pip install” and vice versa.
  • Code generation — from schema to sample, from schema to docs, from schema to types.

Next Steps

  • Add additionalProperties: falseadditionalProperties: false enforcement throughout.
  • Implement $ref$ref for cross-schema references.
  • Add oneOfoneOf / anyOfanyOf support for sum types.
  • Compare your output to jsonschemajsonschema on the same data.
  • Re-implement using pydanticpydantic classes and feel the speed.
  • Wrap with a CLI or web frontend.

Conclusion

You built a recursive JSON Schema validator that catches type errors, constraint violations, and missing fields with precise error messages. The same shape (data tree + schema tree + recursive walk) underlies every validator in every web framework. From here, switching to jsonschemajsonschema or pydanticpydantic is incremental — they speak the same language; they just speak it faster and more completely. Full source on GitHub. Find more data-tooling projects on Python Central Hub.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did