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.
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. |
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