Universal Data Validator
A comprehensive data validation library for Python that works across API, database, and form contexts.
Features
- ✅ Rich Validator Types: String, int, float, bool, email, URL, UUID, date, datetime, numeric, not_empty, enum, one_of, list, dict
- ✅ Schema-Based Validation: Define complex data structures
- ✅ Strict Mode: Reject unknown keys in schemas and dict validators
- ✅ Fail-Fast Mode: Stop validation at the first error
- ✅ Custom Validators: Add your own validation logic
- ✅ Nested Validation: Validate nested objects and lists
- ✅ Clear Error Messages: Detailed error reporting with field paths
- ✅ Zero Dependencies: No external dependencies required
- ✅ Production Ready: Comprehensive test coverage
Installation
pip install universal-validator
Quick Start
from universal_validator import Schema, validators
schema = Schema({
'email': validators.email(),
'age': validators.int(min_value=0, max_value=120),
'username': validators.string(min_length=3, max_length=20),
'tags': validators.list(validators.string())
})
result = schema.validate(data)
if not result.valid:
for error in result.errors:
print(f"{error.field}: {error.message}")
Package Layout
The library is packaged as universal_validator with two submodules:
universal_validator.core— validator classes,Schema,ValidationError,ValidationErrors,ValidationResult, andCode.universal_validator.validators— factory functions such asvalidators.string,validators.int,validators.uuid, etc.
All public names are re-exported from universal_validator and listed in __all__.
Usage Examples
String Validation
from universal_validator import validators
# Basic string
validator = validators.string()
# String with length constraints
validator = validators.string(min_length=3, max_length=20)
# String with pattern
validator = validators.string(pattern=r'^\d{3}-\d{4}$')
# String with choices
validator = validators.string(choices=['red', 'green', 'blue'])
# Optional string
validator = validators.string(required=False)
# Nullable string
validator = validators.string(nullable=True)
Integer and Float Validation
# Integer with range
age_validator = validators.int(min_value=0, max_value=120)
# Float with range
price_validator = validators.float(min_value=0.0, max_value=9999.99)
# Optional integer
count_validator = validators.int(required=False)
Boolean Validation
# Boolean
terms_validator = validators.bool()
# Optional boolean
newsletter_validator = validators.bool(required=False)
Email and URL Validation
# Email validation
email_validator = validators.email()
# URL validation
url_validator = validators.url()
List Validation
# Simple list
tags_validator = validators.list()
# List with item validation
numbers_validator = validators.list(validators.int())
# List with length constraints
items_validator = validators.list(
validators.string(),
min_length=1,
max_length=10
)
Dictionary Validation
# Dictionary with schema
address_validator = validators.dict({
'street': validators.string(),
'city': validators.string(),
'zip': validators.string(pattern=r'^\d{5}$')
})
# Nested dictionary
user_validator = validators.dict({
'name': validators.string(),
'email': validators.email(),
'address': validators.dict({
'street': validators.string(),
'city': validators.string()
})
})
Strict Mode
Enable strict=True on Schema or validators.dict(...) to report unknown keys as validation errors.
schema = Schema({
'name': validators.string()
}, strict=True)
result = schema.validate({'name': 'John', 'extra': 1})
# Error: extra: Unknown field
nested = validators.dict({
'a': validators.dict({'b': validators.string()}, strict=True)
})
result = nested.validate({'a': {'b': 'ok', 'c': 2}}, 'root')
# Error: root.a.c: Unknown field
Fail-Fast Mode
By default, validators collect all errors. Pass fail_fast=True to Schema or validators.dict(...) (and validators.list(...)) to stop at the first error. The result contains exactly one error; which sibling field is reported first follows the validator's iteration order and is not guaranteed across implementations.
schema = Schema({
'username': validators.string(min_length=3),
'email': validators.email(),
'age': validators.int(min_value=0)
}, fail_fast=True)
result = schema.validate({'username': 'ab', 'email': 'bad', 'age': -1})
# Only the first error encountered is reported
profile = validators.dict({
'name': validators.string(min_length=2),
'age': validators.int(min_value=0)
}, fail_fast=True)
result = profile.validate({'name': 'x', 'age': -1}, 'profile')
# Only the first error encountered is reported
Schema Validation
from universal_validator import Schema, validators
# Define schema
user_schema = Schema({
'username': validators.string(min_length=3, max_length=20),
'email': validators.email(),
'age': validators.int(min_value=13, required=False),
'bio': validators.string(max_length=500, required=False),
'tags': validators.list(validators.string()),
'settings': validators.dict({
'theme': validators.string(choices=['light', 'dark']),
'notifications': validators.bool()
})
})
# Validate data
data = {
'username': 'john_doe',
'email': 'john@example.com',
'age': 25,
'tags': ['python', 'javascript'],
'settings': {
'theme': 'dark',
'notifications': True
}
}
result = user_schema.validate(data)
if result.valid:
print("✅ Data is valid!")
else:
print("❌ Validation errors:")
for error in result.errors:
print(f" {error.field}: {error.message}")
Custom Validators
from universal_validator import validators
def is_even(value):
"""Custom validator to check if number is even."""
if value % 2 != 0:
return "Value must be even"
return None
# Add custom validator
validator = validators.int().custom(is_even)
result = validator.validate(4, 'number')
print(result.valid) # True
result = validator.validate(3, 'number')
print(result.valid) # False
print(result.errors[0].message) # "Value must be even"
Validator.custom(fn) returns a clone of the validator with the new custom function appended; the original validator is unchanged.
Multiple Custom Validators
def is_positive(value):
return "Must be positive" if value <= 0 else None
def is_even(value):
return "Must be even" if value % 2 != 0 else None
validator = validators.int().custom(is_positive).custom(is_even)
result = validator.validate(4, 'field')
print(result.valid) # True
result = validator.validate(-2, 'field')
print(result.valid) # False
Real-World Examples
User Registration
from universal_validator import Schema, validators
registration_schema = Schema({
'username': validators.string(min_length=3, max_length=20),
'email': validators.email(),
'password': validators.string(min_length=8),
'confirm_password': validators.string(min_length=8),
'age': validators.int(min_value=13, required=False),
'terms_accepted': validators.bool()
})
data = {
'username': 'john_doe',
'email': 'john@example.com',
'password': 'secure_password_123',
'confirm_password': 'secure_password_123',
'age': 25,
'terms_accepted': True
}
result = registration_schema.validate(data)
API Request Validation
api_request_schema = Schema({
'method': validators.string(choices=['GET', 'POST', 'PUT', 'DELETE']),
'url': validators.url(),
'headers': validators.dict(required=False),
'body': validators.dict(required=False),
'timeout': validators.float(min_value=0, required=False)
})
request_data = {
'method': 'POST',
'url': 'https://api.example.com/users',
'headers': {'Content-Type': 'application/json'},
'body': {'name': 'John'},
'timeout': 30.0
}
result = api_request_schema.validate(request_data)
Configuration Validation
config_schema = Schema({
'database': validators.dict({
'host': validators.string(),
'port': validators.int(min_value=1, max_value=65535),
'username': validators.string(),
'password': validators.string(),
'ssl': validators.bool()
}),
'cache': validators.dict({
'enabled': validators.bool(),
'ttl': validators.int(min_value=0),
'max_size': validators.int(min_value=1)
}),
'features': validators.list(validators.string())
})
config = {
'database': {
'host': 'localhost',
'port': 5432,
'username': 'admin',
'password': 'secret',
'ssl': True
},
'cache': {
'enabled': True,
'ttl': 3600,
'max_size': 1000
},
'features': ['feature1', 'feature2']
}
result = config_schema.validate(config)
Sensitive Fields (Redaction)
Pass sensitive=True to any validator to redact the offending value from its
errors — useful for fields that may contain secrets (passwords, tokens) so
errors can be logged safely.
password = validators.string(min_length=8, sensitive=True)
result = password.validate('hunter2', 'password')
print(result.errors[0].message) # "String length must be at least 8, got 7"
print(result.errors[0].value) # None
secret_email = validators.email(sensitive=True)
result = secret_email.validate('p@ssw0rd', 'email')
print(result.errors[0].message) # "Invalid email address: ***"
print(result.errors[0].value) # None
When sensitive=True:
error.valueis alwaysNone.- Every error message that would interpolate the input value renders
***instead (e.g.Invalid email address: ***,Value must be one of a, b, got '***'). - Messages that do not embed the input value (e.g.
Field is required,Unknown field, length bounds) are unchanged.
sensitive is per-validator and is not inherited by nested validators —
mark each sensitive field individually.
Security
- Trusted schemas only.
pattern(andvalidators.regex) compiles and runs with Python'sre, a backtracking engine. A hostile pattern can cause exponential backtracking even on short inputs, so schemas and patterns MUST come from trusted sources. - Pattern input length cap. Before running any pattern check — including the
fixed email, URL, UUID, date, datetime, and numeric patterns — inputs longer
than
MAX_PATTERN_INPUT_LENGTH(10,000 characters, exported fromuniversal_validator) are rejected with the normal pattern error without running the regex. This bounds worst-case match cost on the backtrackingreengine. - Redact secrets. Use
sensitive=Trueon validators for fields that may contain secrets soerror.valueand messages never leak the input into logs. - Email/URL are sanity checks only. The email pattern is permissive: it does
not enforce RFC 5321 length limits and cannot prove deliverability. The URL
pattern performs no IDN or port validation and accepts inputs such as
http://x.. Do not rely on them for security decisions.
Error Handling
from universal_validator import Schema, validators, ValidationErrors
schema = Schema({
'email': validators.email()
})
# Option 1: Check result
result = schema.validate({'email': 'invalid'})
if not result.valid:
for error in result.errors:
print(f"{error.field}: {error.message}")
# Option 2: Raise exception
try:
schema.validate_or_raise({'email': 'invalid'})
except ValidationErrors as e:
print(f"Validation failed: {e}")
validate_or_raise raises a ValidationErrors aggregate exception whose .errors list contains every ValidationError. Error paths follow the format parent.child for objects and field[0] for array items; a top-level type error on a Schema input uses the field name root.
API Reference
Validators
validators.string(min_length, max_length, pattern, choices, required, nullable, fail_fast, sensitive)— String validatorvalidators.int(min_value, max_value, required, nullable, fail_fast, sensitive)— Integer validatorvalidators.float(min_value, max_value, required, nullable, fail_fast, sensitive)— Number validatorvalidators.bool(required, nullable, fail_fast, sensitive)— Boolean validatorvalidators.email(required, nullable, fail_fast, sensitive)— Email validatorvalidators.url(required, nullable, fail_fast, sensitive)— URL validatorvalidators.list(item_validator, min_length, max_length, required, nullable, fail_fast, sensitive)— List validatorvalidators.dict(schema, required, nullable, strict, fail_fast, sensitive)— Dictionary validatorvalidators.uuid(required, nullable, fail_fast, sensitive)— UUID string validatorvalidators.date(required, nullable, fail_fast, sensitive)—YYYY-MM-DDstring validatorvalidators.datetime(required, nullable, fail_fast, sensitive)— ISO-8601 datetime string validatorvalidators.numeric(required, nullable, fail_fast, sensitive)— Numeric string validatorvalidators.not_empty(required, nullable, fail_fast, sensitive)— Non-empty string/array/object validatorvalidators.enum(values, required, nullable, fail_fast, sensitive)— Arbitrary value enum validatorvalidators.one_of(*validators, required, nullable, fail_fast, sensitive)— Union/disjunction validatorvalidators.regex(pattern, required, nullable, fail_fast, sensitive)— String validator anchored to a pattern
All validators accept sensitive=False by default; see "Sensitive Fields (Redaction)" above.
Schema
Schema(schema_dict, strict=False, fail_fast=False)— Create a schemaschema.validate(data)— Validate data and return ValidationResultschema.validate_or_raise(data)— Validate data and raiseValidationErrorsif invalid
ValidationResult
result.valid- Boolean indicating if validation passedresult.errors- List of ValidationError objects
ValidationError
error.field- Field name that failed validationerror.message- Error messageerror.value- Value that failed validationerror.code- Machine-readable canonical error code (e.g.required,type,min_length)
Canonical error codes are available via the Code class:
from universal_validator import Code
assert result.errors[0].code == Code.REQUIRED # or "required"
Codes include: required, unknown_field, type, min_length, max_length, min_value, max_value, pattern, choices, email, url, custom, uuid, date, datetime, numeric, not_empty, enum, one_of.
Testing
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest test_universal_validator.py -v
# Run with coverage
pytest test_universal_validator.py --cov=universal_validator --cov-report=html
Comparison with Other Libraries
| Feature | universal-validator | pydantic | marshmallow | cerberus |
|---|---|---|---|---|
| Schema-based | ✅ | ✅ | ✅ | ✅ |
| Custom validators | ✅ | ✅ | ✅ | ✅ |
| Zero dependencies | ✅ | ❌ | ❌ | ✅ |
| Nested validation | ✅ | ✅ | ✅ | ✅ |
| Clear errors | ✅ | ✅ | ✅ | ✅ |
| Simple API | ✅ | ❌ | ❌ | ✅ |
| Type hints | ✅ | ✅ | ❌ | ❌ |
License
MIT License
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Notes
- Coercion, trimming, case-folding, and parsing strings into numbers/dates are intentionally out of scope; validate inputs after normalizing them.
- The public API is listed in
universal_validator.__all__.
Changelog
1.0.0 (2024-01-15)
- Initial release
- Support for string, int, float, bool, email, URL, list, dict validators
- Schema-based validation
- Custom validators
- Nested validation
- Comprehensive test coverage
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file universal_validator-2.0.0.tar.gz.
File metadata
- Download URL: universal_validator-2.0.0.tar.gz
- Upload date:
- Size: 18.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc9e069650190d675d4b601ca43c282f0aea618db64bb72627681e8dee3ef65d
|
|
| MD5 |
62e383c3c3223f7b0deca5b82537f291
|
|
| BLAKE2b-256 |
26b851e1ca0884de6fc872058234eddac35a0d74a020ec4dc1f6673899cedc9d
|
File details
Details for the file universal_validator-2.0.0-py3-none-any.whl.
File metadata
- Download URL: universal_validator-2.0.0-py3-none-any.whl
- Upload date:
- Size: 13.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff84f5d5ca6ee5d0fd784914a20bdea1a4f39dc438d481a2cb3a8611f7d17342
|
|
| MD5 |
ee199f8eda6fb85bde062f63bff11dbd
|
|
| BLAKE2b-256 |
f29ea807849d96d8e8c1140a51e30929605f6fdcf866329d83ad0aa4c494f220
|