An implementation of JSON Schema validation for Python
—
Advanced validator creation and extension capabilities for building custom validators, extending existing ones, and registering new schema versions. These functions provide the foundation for creating specialized validation logic.
Build completely new validator classes with custom validation logic and meta-schemas.
def create(meta_schema,
validators=(),
version=None,
type_checker=None,
format_checker=None,
id_of=None,
applicable_validators=None):
"""
Create a new validator class.
Parameters:
- meta_schema: Schema that describes valid schemas for this validator
- validators: Mapping of keyword names to validation functions
- version: Version identifier for this validator
- type_checker: TypeChecker instance for type validation
- format_checker: FormatChecker instance for format validation
- id_of: Function to extract schema ID from schema
- applicable_validators: Function to filter applicable validators
Returns:
- type[Validator]: New validator class
"""Create new validator classes by extending existing ones with additional or modified validation logic.
def extend(validator,
validators=(),
version=None,
type_checker=None,
format_checker=None):
"""
Create a new validator class by extending an existing one.
Parameters:
- validator: Existing validator class to extend
- validators: Additional or replacement validation functions
- version: Version identifier for the new validator
- type_checker: TypeChecker to use (default: inherit from base)
- format_checker: FormatChecker to use (default: inherit from base)
Returns:
- type[Validator]: Extended validator class
"""Register validators for automatic selection based on schema version.
def validates(version):
"""
Decorator to register a validator for a schema version.
Parameters:
- version: Version identifier string
Returns:
- callable: Class decorator for validator registration
"""All validation functions must follow this signature:
def validation_function(validator, value, instance, schema):
"""
Custom validation function.
Parameters:
- validator: The validator instance
- value: The schema value for this keyword
- instance: The instance being validated
- schema: The complete schema being validated against
Yields:
- ValidationError: Each validation error found
"""from jsonschema import create, ValidationError, Draft202012Validator
from jsonschema._types import draft202012_type_checker
from jsonschema._format import draft202012_format_checker
def even_number(validator, value, instance, schema):
"""Validate that a number is even."""
if not validator.is_type(instance, "number"):
return
if value and instance % 2 != 0:
yield ValidationError(f"{instance} is not an even number")
def minimum_length(validator, value, instance, schema):
"""Validate minimum string length."""
if not validator.is_type(instance, "string"):
return
if len(instance) < value:
yield ValidationError(f"String too short: {len(instance)} < {value}")
# Create custom validator
CustomValidator = create(
meta_schema=Draft202012Validator.META_SCHEMA,
validators={
"evenNumber": even_number,
"minimumLength": minimum_length,
# Include standard validators
**Draft202012Validator.VALIDATORS
},
type_checker=draft202012_type_checker,
format_checker=draft202012_format_checker,
version="custom-v1"
)
# Use custom validator
schema = {
"type": "object",
"properties": {
"count": {"type": "number", "evenNumber": True},
"name": {"type": "string", "minimumLength": 3}
}
}
validator = CustomValidator(schema)
# Test validation
valid_data = {"count": 4, "name": "Alice"}
validator.validate(valid_data) # Passes
invalid_data = {"count": 3, "name": "Al"} # Odd number, short name
errors = list(validator.iter_errors(invalid_data))
for error in errors:
print(f"Custom validation error: {error.message}")from jsonschema import extend, Draft202012Validator, ValidationError
def divisible_by(validator, value, instance, schema):
"""Validate that a number is divisible by the given value."""
if not validator.is_type(instance, "number"):
return
if instance % value != 0:
yield ValidationError(f"{instance} is not divisible by {value}")
def contains_word(validator, value, instance, schema):
"""Validate that a string contains a specific word."""
if not validator.is_type(instance, "string"):
return
if value not in instance:
yield ValidationError(f"String does not contain required word: {value}")
# Extend Draft 2020-12 validator
ExtendedValidator = extend(
Draft202012Validator,
validators={
"divisibleBy": divisible_by,
"containsWord": contains_word
},
version="extended-draft2020-12"
)
# Use extended validator
schema = {
"type": "object",
"properties": {
"score": {"type": "number", "divisibleBy": 5},
"description": {"type": "string", "containsWord": "python"}
}
}
validator = ExtendedValidator(schema)
valid_data = {"score": 85, "description": "I love python programming"}
validator.validate(valid_data) # Passes
invalid_data = {"score": 87, "description": "I love javascript"}
errors = list(validator.iter_errors(invalid_data))
for error in errors:
print(f"Extended validation error: {error.message}")from jsonschema import validates, create, validator_for
from jsonschema._types import draft202012_type_checker
from jsonschema._format import draft202012_format_checker
# Custom meta-schema
CUSTOM_META_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/custom-schema",
"type": "object",
"properties": {
"type": {"type": "string"},
"customValidation": {"type": "boolean"}
}
}
def custom_validation(validator, value, instance, schema):
"""Custom validation logic."""
if value:
# Perform custom validation
if not isinstance(instance, str) or len(instance) < 5:
yield ValidationError("Custom validation failed")
@validates("custom-v1")
class CustomValidator:
META_SCHEMA = CUSTOM_META_SCHEMA
VALIDATORS = {"customValidation": custom_validation}
TYPE_CHECKER = draft202012_type_checker
FORMAT_CHECKER = draft202012_format_checker
def __init__(self, schema, **kwargs):
# Implementation similar to standard validators
pass
# Now validator_for will automatically select CustomValidator
schema_with_custom_version = {
"$schema": "https://example.com/custom-schema",
"type": "string",
"customValidation": True
}
ValidatorClass = validator_for(schema_with_custom_version)
print(ValidatorClass.__name__) # CustomValidatorfrom jsonschema import ValidationError
def unique_properties(validator, value, instance, schema):
"""
Validate that all property values in an object are unique.
"""
if not validator.is_type(instance, "object"):
return
if not value: # Skip if not enabled
return
values = list(instance.values())
seen = set()
duplicates = set()
for val in values:
# Only check hashable values
try:
if val in seen:
duplicates.add(val)
else:
seen.add(val)
except TypeError:
# Skip unhashable values
continue
if duplicates:
yield ValidationError(
f"Object has duplicate values: {duplicates}",
validator="uniqueProperties",
validator_value=value,
instance=instance,
schema=schema
)
def conditional_required(validator, value, instance, schema):
"""
Conditionally require properties based on other property values.
Example: {"ifProperty": "type", "equals": "user", "thenRequired": ["email"]}
"""
if not validator.is_type(instance, "object"):
return
if_prop = value.get("ifProperty")
equals = value.get("equals")
then_required = value.get("thenRequired", [])
if if_prop in instance and instance[if_prop] == equals:
for required_prop in then_required:
if required_prop not in instance:
yield ValidationError(
f"Property '{required_prop}' is required when '{if_prop}' equals '{equals}'",
validator="conditionalRequired",
validator_value=value,
instance=instance,
schema=schema,
path=[required_prop]
)
# Use complex validators
AdvancedValidator = extend(
Draft202012Validator,
validators={
"uniqueProperties": unique_properties,
"conditionalRequired": conditional_required
}
)
schema = {
"type": "object",
"uniqueProperties": True,
"conditionalRequired": {
"ifProperty": "type",
"equals": "user",
"thenRequired": ["email", "username"]
},
"properties": {
"type": {"type": "string"},
"email": {"type": "string"},
"username": {"type": "string"},
"name": {"type": "string"},
"age": {"type": "number"}
}
}
validator = AdvancedValidator(schema)
# Valid data
valid_data = {
"type": "user",
"email": "user@example.com",
"username": "john_doe",
"name": "John Doe",
"age": 30
}
validator.validate(valid_data) # Passes
# Invalid - duplicate values
invalid_duplicate = {
"name": "John",
"username": "John", # Duplicate value
"type": "admin"
}
# Invalid - missing required when type=user
invalid_missing = {
"type": "user",
"name": "John" # Missing email and username
}
for data in [invalid_duplicate, invalid_missing]:
errors = list(validator.iter_errors(data))
for error in errors:
print(f"Advanced validation error: {error.message}")from jsonschema import create, ValidationError, SchemaError
# Define custom meta-schema
CUSTOM_META_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/custom-meta-schema",
"type": "object",
"properties": {
"type": {"type": "string"},
"customKeyword": {"type": "string", "enum": ["strict", "loose"]}
},
"additionalProperties": False
}
def custom_keyword_validator(validator, value, instance, schema):
"""Custom keyword validation."""
if value == "strict" and isinstance(instance, str) and len(instance) < 10:
yield ValidationError("Strict mode requires strings of at least 10 characters")
CustomValidator = create(
meta_schema=CUSTOM_META_SCHEMA,
validators={"customKeyword": custom_keyword_validator}
)
# Valid schema according to custom meta-schema
valid_schema = {
"type": "string",
"customKeyword": "strict"
}
# Invalid schema - violates meta-schema
invalid_schema = {
"type": "string",
"customKeyword": "invalid_value", # Not in enum
"additionalProperty": "not_allowed" # Not allowed by meta-schema
}
try:
CustomValidator.check_schema(valid_schema)
print("Schema is valid")
except SchemaError as e:
print(f"Schema error: {e.message}")
try:
CustomValidator.check_schema(invalid_schema)
except SchemaError as e:
print(f"Schema validation failed: {e.message}")Install with Tessl CLI
npx tessl i tessl/pypi-jsonschema