Skip to main content

Domain-oriented validation engine for structured Python models.

Project description

PyPI - Version PyPI - Downloads PyPI - License

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 Error class allowing to set error location, failed value, error code, code-specific error data and few more.

  • Support for unset fields via dedicated Unset sentinel.

  • 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

  1. 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)
  2. 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.

  3. 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 Loc type),
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

modelity-0.37.0.tar.gz (44.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

modelity-0.37.0-py3-none-any.whl (53.5 kB view details)

Uploaded Python 3

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

Hashes for modelity-0.37.0.tar.gz
Algorithm Hash digest
SHA256 ee6d1168ab231ed0f584af4eddac2a4a1b1433f64beb4bb608a2bcb7a0075334
MD5 987a58eb82df6a0e28a4675d1b80f73d
BLAKE2b-256 679829d0c6e47d573c7868c6994f335acbcfd96d3d5312a7d89c5e2b4bbca52d

See more details on using hashes here.

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

Hashes for modelity-0.37.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ce816828ab1d55675b809781e14cd4b7d177bc0500b6ea9f05e2051870fdad4e
MD5 162e0ff9b35f3938e9eb7ae0b6f8d52d
BLAKE2b-256 3b9783e976a02e8324e7a1507e039095e4db5d341c922fc4b29b713d166c349f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page