Examples¶
These are self-contained examples showing validatedata solving real problems. Each one reflects a workflow you might actually have — no boilerplate, no contrived data.
Fast validation with validator()¶
When you only need a boolean pass/fail result (no error messages), use validator(). It compiles a rule into a callable that returns True or False with minimal overhead. Its faster than Pydantic v2 and msgspec on invalid data dicts.
The performance advantage of the validator function on invalid data comes from early‑exit optimisations. Other libraries often have to validate everything and report errors.
from validatedata import validator
# Single value – pipe syntax
is_valid_username = validator('str|min:3|max:32')
is_valid_username('alice') # True
is_valid_username('a') # False
# Multiple fields – flat dict rule
validate_user = validator({
'username': 'str|min:3|max:32',
'email': 'email',
'age': 'int|min:18'
})
validate_user({'username': 'bob', 'email': 'bob@example.com', 'age': 25}) # True
validate_user({'username': 'bob', 'email': 'bob@example.com', 'age': 15}) # False
# Parameterized containers
is_str_list = validator('list[str]')
is_str_list(['a', 'b', 'c']) # True
is_str_list(['a', 1, 'c']) # False
is_str_or_int_list = validator('list[str,int]')
is_str_or_int_list(['a', 1, 'c']) # True
User registration¶
A typical sign-up form: username, email, password with strength requirements, and an optional phone number.
from validatedata import validate_data
rule = {
'username': 'str|strip|min:3|max:32|re:^[\\w.-]+$|msg:username must be 3–32 characters, letters, digits, dots, or hyphens only',
'email': 'email|msg:please enter a valid email address',
'password': 'str|min:8|re:(?=.*[A-Z])(?=.*\\d).+|msg:password must be at least 8 characters with one uppercase letter and one digit',
'phone': 'phone|nullable',
}
result = validate_data(
data={
'username': 'alice_99',
'email': 'alice@example.com',
'password': 'Secure123',
'phone': None,
},
rule=rule,
)
if result.ok:
print('registration accepted')
else:
# errors are grouped per field — easy to map back to form inputs
for group in result.errors:
if group:
print(group[0])
The phone field is nullable so submitting the form without it passes.
Everything else is required and validated in a single call.
Flask route with the decorator¶
Validate incoming JSON before your route body runs. On failure the decorator returns the error dict directly — you just need to check for it.
from flask import Flask, request, jsonify
from validatedata import validate, ValidationError
app = Flask(__name__)
signup_rule = {
'username': 'str|strip|min:3|max:32',
'email': 'email',
'password': 'str|min:8|re:(?=.*[A-Z])(?=.*\\d).+',
}
@app.route('/signup', methods=['POST'])
def signup():
body = request.get_json()
result = validate_data(body, signup_rule)
if not result.ok:
return jsonify({'errors': result.errors}), 422
# body is clean — proceed
user = create_user(body['username'], body['email'], body['password'])
return jsonify({'id': user.id}), 201
Or register a Flask error handler and use raise_exceptions=True to keep
the route body completely free of validation logic:
from validatedata import ValidationError
@app.errorhandler(ValidationError)
def handle_validation_error(e):
return jsonify({'errors': str(e)}), 422
@app.route('/signup', methods=['POST'])
@validate(signup_rule, raise_exceptions=True)
def signup(username, email, password):
user = create_user(username, email, password)
return jsonify({'id': user.id}), 201
Application config file¶
Validate a config dict loaded from YAML, TOML, or environment variables before your app starts. Mirror-structure rules match the shape of the config exactly — no structural boilerplate required.
import yaml
from validatedata import validate_data
with open('config.yaml') as f:
config = yaml.safe_load(f)
# config.yaml looks like:
#
# app:
# name: MyService
# version: 1.4.0
# debug: false
#
# database:
# host: 127.0.0.1
# port: 5432
# name: mydb
#
# server:
# host: 0.0.0.0
# port: 8080
rule = {
'app': {
'name': 'str|min:1',
'version': 'semver',
'debug': 'bool',
},
'database': {
'host': 'ip',
'port': 'int|between:1,65535',
'name': 'str|min:1',
},
'server': {
'host': 'ip',
'port': 'int|between:1024,65535',
},
}
result = validate_data(data=config, rule=rule, mutate=True)
if not result.ok:
for error in result.errors:
print(f'Config error: {error}')
raise SystemExit('Invalid configuration — aborting startup')
# result.data is a dict with the same shape as config —
# use it directly so any transforms (e.g. strip on string fields) are applied
app_config = result.data
Bad config fails loudly at startup with a clear field path
(e.g. database.port: invalid integer) rather than surfacing as a
cryptic runtime error later. Passing mutate=True means result.data
gives you back the validated — and optionally transformed — config in exactly
the same nested structure, ready to use without re-reading the original dict.
Bulk data import¶
Validate rows before writing them to a database. Collect all errors up front so you can report the bad rows without stopping at the first failure.
from validatedata import validate_data
row_rule = [
'str|strip|min:1|max:128', # name
'email', # email
'int|min:0', # age
'str|in:active,inactive', # status
]
rows = [
['Alice', 'alice@example.com', 30, 'active'],
['', 'bob@example.com', 25, 'active'], # blank name
['Carol', 'not-an-email', 28, 'active'], # bad email
['Dave', 'dave@example.com', -1, 'pending'], # bad age, bad status
]
bad_rows = []
for i, row in enumerate(rows):
result = validate_data(row, row_rule)
if not result.ok:
bad_rows.append({'row': i + 1, 'errors': result.errors})
if bad_rows:
for entry in bad_rows:
print(f"Row {entry['row']}: {entry['errors']}")
else:
write_to_database(rows)
Running through all rows before writing means you can return a full report to the user — not just the first bad row.
Conditional fields on a checkout form¶
Delivery method determines which fields are required. depends_on skips
validation on a field entirely when the condition isn’t met.
from collections import OrderedDict
from validatedata import validate_data
rule = {
'delivery_method': 'str|in:pickup,delivery',
'address': {
'type': 'str',
'range': (10, 'any'),
'depends_on': {'field': 'delivery_method', 'value': 'delivery'},
'message': 'a delivery address is required',
},
'promo_code': {
'type': 'str',
'length': 8,
'nullable': True,
'message': 'promo code must be exactly 8 characters',
},
}
# pickup — address is skipped, promo code is optional
result = validate_data(
data=OrderedDict([
('delivery_method', 'pickup'),
('address', None),
('promo_code', None),
]),
rule=rule,
)
result.ok # True
# delivery without address — fails
result = validate_data(
data=OrderedDict([
('delivery_method', 'delivery'),
('address', None),
('promo_code', None),
]),
rule=rule,
)
result.ok # False
result.errors # [[], ['a delivery address is required', 'a delivery address is required'], []]
Normalising data before saving¶
Use transforms with mutate=True to clean user input in the same pass as
validation. The function receives the cleaned values — no separate sanitisation
step needed.
from validatedata import validate
@validate(
rule={
'username': 'str|strip|lower|min:3|max:32',
'bio': 'str|strip|max:280|nullable',
'website': 'url|nullable',
},
mutate=True,
)
def update_profile(username, bio, website):
# username is already stripped and lowercased
# bio is stripped, website is validated
db.update(username=username, bio=bio, website=website)
return 'profile updated'
update_profile(
username=' Alice_99 ',
bio=' Building things. ',
website='https://alice.dev',
)
# saves username='alice_99', bio='Building things.' — whitespace stripped
Input arrives messy, your function receives it clean. No intermediate
variables, no separate call to .strip() or .lower().
The same thing works with validate_data() directly. When the input is a
dict, result.data is returned as a dict with the same keys — so you can
use the cleaned values straight away without tracking positional order:
from validatedata import validate_data
result = validate_data(
data={
'username': ' Alice_99 ',
'bio': ' Building things. ',
'website': 'https://alice.dev',
},
rule={
'username': 'str|strip|lower|min:3|max:32',
'bio': 'str|strip|max:280|nullable',
'website': 'url|nullable',
},
mutate=True,
)
if result.ok:
db.update(**result.data)
# result.data == {
# 'username': 'alice_99',
# 'bio': 'Building things.',
# 'website': 'https://alice.dev',
# }
result.data mirrors the shape of the input dict exactly — nested dicts are
preserved, so a config-shaped input comes back as a config-shaped output.