Domain-oriented validation engine for structured Python models.
Project description
Modelity
Modelity is a domain-oriented validation engine for structured Python models.
It separates construction from validation, treats models as trees, and provides location-aware structured errors.
Modelity is designed for complex domain models, not just data containers.
Installation
pip install modelity
Core idea
Modelity enforces a clear lifecycle:
raw input
↓
parsing (field-level normalization)
↓
model instance
↓
validation (domain invariants)
↓
fully validated model
A model can exist in parsed but not yet validated state.
Features
-
Declare models using type annotations just like Python dataclasses.
-
Clean separation of concerns:
- parsing is executed on raw data when model is created or modified,
- validation is executed on demand on successfully parsed model instances.
-
Error handling done via dedicated
Errorclass allowing to set error location, failed value, error code, code-specific error data and few more. -
Support for unset fields via dedicated
Unsetsentinel. -
Support for typed mutable containers (lists, sets and dicts) with type checking and parsing on modifications.
-
Easily customizable with user-defined hooks
-
Ability to access any part of the model from any user-defined validation hook to achieve complex cross-field validation logic.
-
Ability to access custom context object from any user-defined validation hook for even more complex validation strategies (like having different validators when model is created, when model is updated or when model is fetched over the API).
-
Ability to register custom types.
Lifecycle overview
-
Class build time (meta phase)
- Inheritance handling (i.e. collecting fields and hooks from base and mixin classes)
- Recursive type annotation analyzer and compiler (i.e. constructs type parser for each type and caches in the model class)
- Configuration of user-defined hooks (i.e. precomputing and assigning to fields)
-
Parsing stage (construction-time)
Triggered on:
- model instantiation,
- attribute assignment,
- mutable container modification.
Pipeline:
- raw value (i.e. input value)
- field preprocessing chain
- field type verification and parsing
- field postprocessing chain
- model value (i.e. target value)
Parsing runs independently for each model field (or typed mutable container element) and is executed when new model object is created, when existing model is modified, or when typed mutable container is modified.
Errors accumulate and raise
ParsingError. -
Validation stage (explicit)
Triggered via:
from modelity.api import validate validate(model)
Pipeline:
- unverified model instance (i.e. the input model from parsing stage)
- model prevalidation chain
- field validation chain (runs for each set field)
- location validator chain (runs for matched model locations only)
- model postvalidation chain
- verified model instance
Errors accumulate and raise
ValidationError.
Design principles
Separation of concerns
- Parsing is about structure.
- Validation is about meaning.
Deterministic execution order
- Both parsing and validation stages have a fixed and predictable order of steps.
- User-defined hooks are always executed in their declaration order.
Tree-aware architecture
- Models are treated as trees, not flat structures.
- Location of the value in the model is given by absolute path pointing to a tree leaf where the value is stored.
- Validation and serialization is implemented using visitors.
First-class location object
Modelity is using special Loc class for encoding locations in the model. This
is tuple-like type with some addons.
Structured error model
Errors are first-class objects with following properties:
- location in the model (using the
Loctype), - error code (supporting both built-in error codes and custom ones),
- error message (human-readable),
- incorrect value
- code-specific metadata (e.g. failed regex pattern, expected field length range, supported types etc.)
Minimum external dependencies
Modelity currently only depends on typing-extensions package which is needed
for some additional typing primitives and for dataclass-like UX.
Pure Python implementation
Modelity is currently implemented in pure Python by design to make it easily portable between Python versions and alternative Python interpreters.
When to use Modelity
Modelity is well suited for:
- complex domain models
- nested and repeated structures
- cross-field invariants
- structured API validation
- systems requiring deterministic validation behavior
When not to use Modelity
Modelity may be unnecessary for:
- simple DTO containers
- lightweight data coercion
- cases where parsing alone is sufficient
Example
Definition of domain models
from modelity.api import (
Model,
Gt,
UserError,
ValidationError,
fixup,
validate,
field_fixup,
model_fixup,
field_validator,
field_postprocessor
)
class OrderItem(Model):
name: str
quantity: Annotated[int, Gt(0)]
price: Annotated[float, Gt(0)]
# -- field-scoped postprocessing
@field_postprocessor("name")
def _strip(cls, value: str):
return value.strip()
@property
def total_price(self) -> float:
return self.quantity * self.price
class Order(Model):
items: list[OrderItem]
total: Optional[float] = None
modified: Optional[datetime.datetime] = None
created: Optional[datetime.datetime] = None
# -- construction or modification fixup hooks
@field_fixup("items")
def _update_total(self):
self.total = sum(x.total_price for x in self.items)
# -- construction fixup hooks
@model_fixup()
def _update_timestamps(self):
now = datetime.datetime.now()
self.modified = now
if self.created is None:
self.created = now
# -- validation hooks
@field_validator("total")
def _verify_total(self):
if self.total != sum(x.total_price for x in self.items):
raise UserError(msg="incorrect total price", code="PRICE_CHECK_ERROR")
Creating model instances
order = Order(items=[
OrderItem(name="apple", quantity=2, price=3.0),
OrderItem(name="banana", quantity="1", price=2.0), # "1" will automatically be converted to 1
])
print(order.total) # Would print: 8.0; it was automatically computed by fixup hook
Altering model instances
Modelity models are mutable by default and altering fields after model creation results in same parsing mechanics being used:
order.items.append(OrderItem(name="orange", quantity=1, price=1))
order.total = "10.0" # Will be converted to 10.0 float; but this is not the right value
Fixing up model instances
After alteration it is recommended (although not required) to run fixup
helper to ensure that all fixup hooks are called with updated data:
from modelity.api import fixup
print(order.total) # Would print: 10.0
fixup(order) # Will fix total total price
print(order.total) # Would print: 9.0;
Validating models
At this step order object is in successfully parsed (i.e. all fields have the
right types), but not yet validated state. To validate it against built-in and
user-defined constraints you have to explicitly call validate function:
from modelity.api import validate
validate(order)
print("The order is valid")
Serializing models
Modelity serialization mechanism does not produce JSON or other formats, but encodes model data into JSON-compatible dict that can later be encoded using other libraries:
from pprint import pprint
from modelity.api import dump
order_dict = dump(order)
pprint(order_dict) # Will print order object encoded to dict
Deserializing models
Serialized data can be back decoded into model instance. Deserialization involves parsing, fixup and validation stages automatically:
from modelity.api import load
order = load(Order, order_dict)
print(order.total) # Would print: 9.0
Documentation
Please visit project's ReadTheDocs site: https://modelity.readthedocs.io/en/latest/.
Disclaimer
Modelity is an independent open-source project for the Python ecosystem. It is not affiliated with, sponsored by, or endorsed by any company, organization, or product of the same or similar name. Any similarity in names is purely coincidental and does not imply any association.
License
This project is released under the terms of the MIT license.
Author
Maciej Wiatrzyk maciej.wiatrzyk@gmail.com
Project details
Release history Release notifications | RSS feed
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 modelity-0.37.0.tar.gz.
File metadata
- Download URL: modelity-0.37.0.tar.gz
- Upload date:
- Size: 44.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.3.2 CPython/3.13.12 Linux/6.17.0-1007-aws
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee6d1168ab231ed0f584af4eddac2a4a1b1433f64beb4bb608a2bcb7a0075334
|
|
| MD5 |
987a58eb82df6a0e28a4675d1b80f73d
|
|
| BLAKE2b-256 |
679829d0c6e47d573c7868c6994f335acbcfd96d3d5312a7d89c5e2b4bbca52d
|
File details
Details for the file modelity-0.37.0-py3-none-any.whl.
File metadata
- Download URL: modelity-0.37.0-py3-none-any.whl
- Upload date:
- Size: 53.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.3.2 CPython/3.13.12 Linux/6.17.0-1007-aws
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce816828ab1d55675b809781e14cd4b7d177bc0500b6ea9f05e2051870fdad4e
|
|
| MD5 |
162e0ff9b35f3938e9eb7ae0b6f8d52d
|
|
| BLAKE2b-256 |
3b9783e976a02e8324e7a1507e039095e4db5d341c922fc4b29b713d166c349f
|