FastModel – Declarative Models¶
Added in version 0.6.0.
FastModel is a declarative validation model built on validatedata’s compiled core. It combines the speed of validator() with the convenience of typed, reusable models – ideal for API schemas, configuration objects, and domain entities.
Basic usage¶
Define a model by subclassing FastModel and annotating fields with types. Use Rule to add validation constraints.
from validatedata import FastModel, Rule
class User(FastModel):
id: int = Rule(type="int")
username: str = Rule(min=3, max=32, pattern=r'^[a-z0-9_]+$')
email: str = Rule("email") # pipe syntax works too
tags: list[str] = Rule(default=[], init_new=True, max=20)
# Instantiate – validates on creation
user = User(id=1, username="alice", email="alice@example.com")
# Invalid data raises ValidationError
try:
bad = User(id=1, username="a", email="not-an-email")
except ValidationError as e:
print(e.errors) # {'username': [...], 'email': [...]}
Full example — most Rule options at once¶
The example below is deliberately dense: it exercises nearly every Rule
option and shows the two syntaxes side by side. Keyword form with an
explicit type= is preferred for non-str types and for anything past
a bare type check; pipe strings remain fully supported and are shown here
both as a fallback and for cases the keyword shortcuts don’t cover.
from validatedata import FastModel, Rule, ValidationError
class Address(FastModel):
"""Nested model — used as a field type on Order below."""
# Keyword form with an explicit type=, since "str" needs no extra
# constraints here beyond a length range.
street: str = Rule(type="str", min=3, max=64)
# Pipe-string form (fallback / shortform) — identical result to
# Rule(type="str", pattern=r'^[A-Za-z ]+$'), just written as one string.
city: str = Rule("str|re:^[A-Za-z ]+$")
# pattern kwarg -> compiled to |re: internally. Also shows nullable=True
# as an explicit keyword instead of appending "|nullable" by hand.
postal_code: str = Rule(type="str", pattern=r'^[0-9]{4,10}$', nullable=True)
class Order(FastModel):
"""
Exercises keyword constraints with explicit type=, pipe-string
fallback, choices, regex patterns, transforms, custom messages,
nullable fields, mutable defaults via init_new, a default_factory,
a nested FastModel field, and cross-field validation via model_check.
"""
# --- required scalar fields, keyword form with explicit type= ---
order_id: str = Rule(type="str", pattern=r'^ORD-[0-9]{6}$')
# int with min/max and a custom failure message
priority: int = Rule(type="int", min=1, max=5, msg="priority must be 1-5")
# choices -> compiled to "in:pending,paid,shipped,cancelled"
status: str = Rule(type="str", choices=["pending", "paid", "shipped", "cancelled"])
# transforms kwarg: strip + lower BEFORE the pattern check runs
customer_email: str = Rule(type="email", transforms="strip|lower")
# Pipe-string-only fallback showing the exact same transform pipeline
# written by hand, for cases where the shortform keywords don't fit.
coupon_code: str = Rule("str|strip|upper|min:4|max:12|nullable")
# --- floats / numeric edge types ---
discount_rate: float = Rule(type="float", min=0.0, max=1.0, msg="discount_rate must be between 0 and 1")
# --- nullable without touching the pipe string directly ---
gift_message: str = Rule(type="str", max=200, nullable=True)
# --- mutable defaults ---
# init_new=True gives every Order instance its own fresh list, instead
# of sharing one mutable default across instances.
tags: list[str] = Rule("list[str]", default=[], init_new=True, max=20) # item type enforced
# default_factory is the explicit-callable equivalent of init_new for
# cases where the container needs custom construction logic.
metadata: dict = Rule(type="dict", default_factory=dict)
# --- nested FastModel field ---
# A dict passed for this field is converted into an Address instance
# (recursively validated) when built via from_dict — see below.
shipping_address: Address
# Plain list field (count constraints only — a single Rule can't
# express a per-item schema; use validate_data()'s dict-rule 'items'
# key for that instead).
items: list = Rule(type="list", min=1)
def model_check(self, data: dict):
"""Cross-field logic that can't live on a single Rule."""
if data["status"] == "cancelled" and data["discount_rate"] > 0:
raise ValidationError(
{"discount_rate": ["cancelled orders cannot carry a discount"]}
)
# Returning a dict mutates fields after validation passes.
item_count = len(data["items"])
return {"metadata": {**data["metadata"], "item_count": item_count}}
# from_dict recursively builds nested FastModel fields (a dict passed for
# shipping_address is converted into an Address instance automatically).
# validate=True runs the full __init__ path and raises ValidationError on
# bad data. Direct Order(...) construction also works, but nested fields
# must then be passed as already-built model instances (Address(...)),
# not raw dicts.
order = Order.from_dict(
{
"order_id": "ORD-004821",
"priority": 3,
"status": "pending",
"customer_email": " Alice@Example.com ", # -> stripped + lowercased
"coupon_code": "save10", # -> "SAVE10"
"discount_rate": 0.1,
"gift_message": None,
"shipping_address": {"street": "1 Main St", "city": "Springfield", "postal_code": None},
"items": [
{"sku": "ABC-1234", "quantity": 2, "unit_price": 9.99},
{"sku": "XYZ-0007", "quantity": 1, "unit_price": 24.50},
],
},
validate=True,
)
print(order.customer_email) # alice@example.com
print(order.coupon_code) # SAVE10
print(order.shipping_address) # Address(street='1 Main St', city='Springfield', postal_code=None)
print(order.metadata) # {'item_count': 2}
Note
Rule fields are field-scoped only — cross-field logic like the
cancelled/discount check above always belongs in model_check, never
on a Rule.
Tip
Every keyword shortcut compiles down to a pipe string internally. If a
constraint has no dedicated keyword, drop to the pipe string directly —
both forms can be mixed freely across fields in the same model, as shown
with coupon_code above.
Cross‑field validation¶
Implement a model_check method to run cross‑field logic. It receives a dict of all field values and can mutate them by returning a dict.
class Order(FastModel):
start: int
end: int
def model_check(self, data: dict):
if data["end"] <= data["start"]:
raise ValidationError({"end": ["end must be greater than start"]})
# Optionally mutate
return {"end": data["end"] + 1} # add one day
Serialisation / Deserialisation¶
Models can be converted to dicts and reconstructed.
# to dict
data = user.to_dict() # {'id': 1, 'username': 'alice', ...}
# recommeded if you want speed. its the same as from_dict(data, validate="check")
user2 = User.from_dict(data)
# from dict – full validation (raises on invalid)
user3 = User.from_dict(data, validate=True)
# from dict – check only, returns None on invalid
user4 = User.from_dict(data, validate="check")
# never use unless you validated the data earlier and you trust it
user5 = User.from_dict(data, validate=False)
Nested models are automatically handled:
class Address(FastModel):
street: str
city: str
class User(FastModel):
name: str
address: Address
data = {"name": "Alice", "address": {"street": "123 Main St", "city": "Springfield"}}
user = User.from_dict(data) # address is automatically converted to Address instance
Partial validation (no instantiation)¶
Use check to validate a dict without creating an instance.
ok, errors = User.check({"username": "alice", "email": "alice@example.com"})
if not ok:
print(errors)
Fast boolean checks¶
Use is_valid on an instance or is_valid_data on a class for a fast boolean check.
if user.is_valid(): # whole model
...
if user.is_valid(field="username"): # single field
...
if User.is_valid_data({"username": "alice"}): # class‑level, dict only
...
Schema introspection¶
schema() returns a lightweight description of the model fields – useful for documentation or API generation.
User.schema()
# {
# "model": "User",
# "fields": {
# "username": {"rule": "str|min:3|max:32|re:...", "required": True},
# "email": {"rule": "email", "required": True},
# "tags": {"rule": None, "required": False, "default": []},
# }
# }
Rule dict and compiled validator access¶
get_rules() returns the canonical rule dictionary used at class-creation time — each value is a pipe string, or a nested rule dict for fields whose type is a FastModel subclass.
Order.get_rules()["shipping_address"]
# {'street': 'str|min:3|max:64', 'city': 'str|re:^[A-Za-z ]+$', 'postal_code': 'str|re:^[0-9]{4,10}$|nullable'}
get_validator() returns the compiled boolean validator callable for the whole model — the same function used internally by is_valid_data and the “check” branch of from_dict.
validate = User.get_validator()
validate({"username": "alice", "email": "alice@example.com"}) # True / False