Rules Reference¶
Rules tell validatedata what to check. A rule can be a string (shorthand) or a dict (explicit form). Both work side by side in the same rule list or field map.
Types¶
Every rule — string or dict — must specify a type. The type is always the first token.
Basic types¶
Type |
Description |
|---|---|
|
Boolean |
|
Colour in any CSS format. Use |
|
Date or datetime string (parsed with |
|
Email address |
|
Even integer |
|
Floating-point number |
|
Integer |
|
IPv4 or IPv6 address |
|
Odd integer |
|
Phone number. E.164 built-in. Extended formats require |
|
Prime number |
|
Semantic version string e.g. |
|
URL-friendly string e.g. |
|
String |
|
URL with protocol e.g. |
|
UUID string |
Extended types¶
dict, list, object, regex, set, tuple
Use dict and list with fields and items for nested validation
— see Nested rules below.
Custom types¶
You can register your own type checkers for any Python class or string name.
This is especially useful for domain‑specific types (e.g. Decimal,
Point, or even custom classes).
Registering a type¶
from validatedata import register_type, validate_data
def is_decimal(v):
from decimal import Decimal
return isinstance(v, Decimal)
register_type(Decimal, is_decimal)
rule = {'price': 'decimal|min:0'} # use the class name as type
result = validate_data({'price': Decimal('19.99')}, rule)
result.ok # True
You can also register a checker under a custom name:
register_type('positive_int', lambda v: isinstance(v, int) and v > 0)
rule = {'count': 'positive_int'}
validate_data({'count': 5}, rule).ok # True
Unregistering and introspection¶
from validatedata import unregister_type, export_registered_checkers
unregister_type(Decimal) # remove by class
unregister_type('positive_int') # remove by name
checkers = export_registered_checkers() # dict of {name: checker}
Dict rule form¶
The explicit form gives you the full rule API as a Python dict:
{
'type': 'str',
'range': (3, 32),
'nullable': True,
'message': 'username must be 3 to 32 characters',
}
Valid rule keys¶
Key |
Type |
Description |
|---|---|---|
|
str |
Type name. Always required |
|
tuple |
Permitted range. Use |
|
int |
Exact expected length |
|
tuple |
Permitted values — value must equal one of these |
|
str or tuple |
Values not permitted |
|
str or tuple |
Values that must be present |
|
object |
Value the data must start with |
|
object |
Value the data must end with |
|
str |
Regular expression the data must match |
|
bool |
List or tuple must contain no duplicates |
|
bool |
Skip type casting. Default |
|
bool |
Allow |
|
str |
Format variant for |
|
str |
Region code for |
|
callable or dict |
Function applied to the value before validation runs |
|
dict |
Validate only when a sibling field meets a condition |
|
dict |
Rules for nested dict fields. See Nested rules |
|
dict |
Rule applied to each item in a list or tuple. See Nested rules |
|
type |
Python class to check against when |
|
str |
Override the default error message for type failures |
|
str |
Override the error for a specific rule e.g. |
Checking a rule dict before use¶
Use check_rule() to validate a rule dict in isolation – it raises
ValueError for unrecognised keys.
from validatedata import check_rule, VALID_RULE_KEYS
check_rule({'type': 'str', 'nullable': True}) # OK
check_rule({'type': 'str', 'nulable': True}) # ValueError
print(VALID_RULE_KEYS) # frozenset of all allowed keys
This is useful when building rules programmatically.
Shorthand rule strings¶
Rules can be written as compact strings rather than dicts. There are two syntaxes: the original colon syntax for simple cases, and the pipe syntax for anything more expressive. Both work side by side.
Colon syntax¶
'str' # string
'str:20' # string of exactly 20 characters
'int:10' # int of exactly 10 digits
'email' # email address
'email:msg:invalid email address' # with custom error message
'int:1:to:100' # int in range 1 to 100
'regex:[A-Z]{3}' # must match regex
Pipe syntax¶
Chain modifiers onto a type with |. The general shape is:
type [| transform ...] [| modifier ...] [| msg:message]
Transforms must come before validators. msg: must always be last.
Flags
'int|strict' # no type coercion
'email|nullable' # None is a valid value
'int|strict|nullable' # both
Range
'int|min:18' # >= 18
'int|max:100' # <= 100
'int|min:0|max:100' # between 0 and 100 inclusive
'int|between:0,100' # shorthand for the above
'str|min:3|max:32' # string length range
'list|min:1|max:10' # list item count range
Enums and exclusions
'str|in:admin,user,guest' # must be one of these
'str|not_in:root,superuser' # must not be any of these
String constraints
'str|starts_with:https' # required prefix
'str|ends_with:.pdf' # required suffix
'str|contains:@' # required substring
'list|unique' # no duplicate values
Format variants
'color|format:hex' # #fff or #ffffff
'color|format:rgb' # rgb(255, 0, 0)
'phone|format:national' # (415) 555-2671 — requires phonenumbers
'phone|format:e164' # +14155552671 — built-in
Transforms
Named transforms run before validation. Chain as many as needed:
'str|strip|min:3|max:32' # strip whitespace, then check length
'str|lower|in:admin,user,guest' # lowercase, then check options
'str|strip|lower|min:3' # chained
Available named transforms: strip, lstrip, rstrip, lower,
upper, title.
Regex
'str|re:[A-Z]{3}'
'str|min:8|re:(?=.*[A-Z])(?=.*\d).+'
The pattern is everything after re: up to the next recognised modifier.
Patterns can safely contain : and |.
Custom error message
msg: must be the last modifier:
'str|min:3|max:32|msg:must be 3 to 32 characters'
'int|min:18|msg:you must be 18 or older'
'str|re:[A-Z]+|msg:uppercase letters only'
Mixing syntaxes
Colon shorthand, pipe shorthand, and dict rules can coexist in the same list:
rules = [
{'type': 'str', 'expression': r'^[\w-]{3,32}$', 'expression-message': 'invalid username'},
'email|nullable|msg:invalid email',
'str|min:8|re:(?=.*[A-Z])(?=.*\d).+|msg:password too weak',
]
Pipe modifier reference¶
Modifier |
Example |
Description |
|---|---|---|
|
|
No type coercion |
|
|
Allow |
|
|
No duplicate values |
|
|
Minimum value or length |
|
|
Maximum value or length |
|
|
Range shorthand |
|
|
Allowed values |
|
|
Excluded values |
|
|
Required prefix |
|
|
Required suffix |
|
|
Required substring |
|
|
Format variant |
|
|
Remove surrounding whitespace (transform) |
|
|
Remove leading whitespace (transform) |
|
|
Remove trailing whitespace (transform) |
|
|
Lowercase (transform) |
|
|
Uppercase (transform) |
|
|
Title case (transform) |
|
|
Regex pattern |
|
|
Custom error — must be last |
Nested rules¶
Use fields to validate dict contents, and items to validate each
element of a list or tuple. Errors on nested fields are returned as
dotted-path strings: user.email: invalid email.
Nested dict¶
rules = {'keys': {
'user': {
'type': 'dict',
'fields': {
'username': {'type': 'str', 'range': (3, 32)},
'email': {'type': 'email'},
'age': {'type': 'int', 'range': (18, 'any')},
}
}
}}
result = validate_data(
data={'user': {'username': 'al', 'email': 'not-an-email', 'age': 25}},
rule=rules,
)
result.errors # ['user.username: invalid string length', 'user.email: invalid email']
Tip
For deeply nested data, the Mirror-Structure Rules shorthand lets you write
rules that match the exact shape of your data without the type/fields
boilerplate.
List of typed items¶
rules = [{'type': 'list', 'items': {'type': 'int', 'range': (1, 100)}}]
result = validate_data([[10, 50, 200, 5]], rules)
result.errors # ['[0][2]: number out of range']
List of dicts¶
rules = [{'type': 'list', 'items': {
'type': 'dict',
'fields': {
'name': {'type': 'str'},
'score': {'type': 'int', 'range': (0, 100)},
}
}}]
result = validate_data(
data=[[
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 150}, # invalid
]],
rule=rules,
)
result.errors # ['[0][1].score: number out of range']
Transforms¶
A transform is applied to a value before any validation runs. The
transformed value is what gets checked — and what is returned in
result.data when mutate=True.
Simple callable¶
rules = [{'type': 'str', 'transform': str.strip, 'length': 5}]
validate_data([' hello '], rules).ok # True — stripped then checked
Lambda¶
rules = [{'type': 'int', 'transform': lambda v: v * 2}]
result = validate_data([5], rules, mutate=True)
result.data # [10]
Accessing sibling fields¶
Pass a dict with func and pass_data=True to receive the full sibling
data dict as a second argument:
rules = {'keys': {
'role': {'type': 'str'},
'username': {
'type': 'str',
'transform': {
'func': lambda value, data: value.upper() if data.get('role') == 'admin' else value,
'pass_data': True,
}
}
}}
Conditional validation¶
Use depends_on to validate a field only when a sibling meets a condition.
Value match¶
rules = {'keys': {
'role': {'type': 'str'},
'permissions': {
'type': 'str',
'depends_on': {'field': 'role', 'value': 'admin'},
'options': ('full', 'read', 'none'),
}
}}
validate_data({'role': 'user', 'permissions': 'anything'}, rules).ok # True — skipped
validate_data({'role': 'admin', 'permissions': 'full'}, rules).ok # True
validate_data({'role': 'admin', 'permissions': 'anything'}, rules).ok # False
Callable condition¶
rules = {'keys': {
'age': {'type': 'int'},
'guardian_name': {
'type': 'str',
'depends_on': {
'field': 'age',
'condition': lambda age: age < 18,
},
'message': 'guardian name required for users under 18',
}
}}
Note
depends_on requires dict input and only works across top-level sibling
fields. Cross-nested references are not currently supported.
Custom error messages¶
Override any default error with a {rule}-message key:
rules = [
{'type': 'int', 'range': (18, 'any'), 'range-message': 'you must be at least 18'},
{'type': 'str', 'range': (3, 32), 'range-message': 'username must be 3–32 characters'},
{'type': 'email', 'message': 'please enter a valid email address'},
{'type': 'str', 'expression': r'...', 'expression-message': 'invalid format'},
]
Use message for type-level errors and <rule>-message for
constraint-level errors (range-message, expression-message,
length-message, etc.).
Validating rule dicts¶
If you write a rule dict with an unrecognised key, validatedata raises a
ValueError immediately — before any data is touched — with a suggestion
for what you might have meant:
validate_data(['hello'], [{'type': 'str', 'nulable': True}])
# ValueError: Unknown rule key 'nulable' in rule. Did you mean 'nullable'?
You can also call check_rule directly to validate a rule dict in isolation:
from validatedata import check_rule
check_rule({'type': 'str', 'nulable': True})
# ValueError: Unknown rule key 'nulable' in rule. Did you mean 'nullable'?
check_rule({'type': 'str', 'nullable': True}) # passes silently