Skip to content

Validation & Serialization

AMSDAL models inherit from Pydantic, giving you built-in data validation, type coercion, and serialization.

Type Validation

Fields are validated automatically on assignment:

from amsdal.models import Model

class Person(Model):
    name: str
    age: int

Person(name='John', age=25)       # OK
Person(name='John', age='not_a_number')  # ValidationError

Field Validators

Use @field_validator for custom validation logic:

from pydantic import field_validator
from amsdal.models import Model

class Person(Model):
    name: str
    age: int

    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0 or v > 150:
            raise ValueError('Age must be between 0 and 150')
        return v

Model Validators

Use @model_validator to validate across multiple fields:

from pydantic import model_validator
from amsdal.models import Model

class DateRange(Model):
    start_date: str
    end_date: str

    @model_validator(mode='after')
    def validate_dates(self) -> 'DateRange':
        if self.start_date > self.end_date:
            raise ValueError('start_date must be before end_date')
        return self

Default Values

from amsdal.models import Model

class Settings(Model):
    theme: str = 'light'
    notifications: bool = True
    max_items: int = 50

Optional Fields

from amsdal.models import Model

class Person(Model):
    name: str
    nickname: str | None = None
    bio: str | None = None

Serialization

Convert model instances to dictionaries or JSON:

person = Person(name='John', age=25)

# To dict
person.model_dump()
# {'name': 'John', 'age': 25}

# To JSON string
person.model_dump_json()
# '{"name": "John", "age": 25}'

Note

In async mode (async_mode=True), model_dump() and model_dump_json() automatically keep references as ref dicts (same as model_dump_refs). This is because resolving a reference requires a synchronous ReferenceLoader.load_reference() call, which is not allowed in async mode. To include inlined data in async mode, load references explicitly first (await event.person) and call model_dump() after the cache is populated.

Note

Forward-FK fields are stored under the private name _<fk>_ref with alias='<fk>' for Pydantic. Model.model_dump() and model_dump_refs() default to by_alias=True, so the public name <fk> appears in output. If you call these methods with by_alias=False or invoke the underlying Pydantic method directly, the internal _<fk>_ref key will appear instead.

Models with References

For models with FK or M2M fields, serialization methods differ in how they handle related objects:

Method Sync mode Async mode
model_dump() Resolves — loads related objects inline Keeps as Reference — sync FK load is forbidden in async mode, so it falls back to ref dicts
model_dump_json() Resolves — same as model_dump, output as JSON string Keeps as Reference — same fallback, output as JSON string
model_dump_refs() Always keeps as Reference — returns {'ref': {...}} dicts without loading Same
model_dump_json_refs() Always keeps as Reference — JSON string with ref dicts Same

The *_refs variants are useful when you want to roundtrip a model (save/load it) without forcing reference resolution — for example, exporting to a queue or audit log.

class Car(Model):
    name: str
    owner: User  # FK field

car = Car(name='Ferrari', owner=user)

# Resolves references — includes full owner data
car.model_dump()
# {'name': 'Ferrari', 'owner': {'name': 'John', 'age': 25}}

# Always keeps references as dicts (regardless of mode)
car.model_dump_refs()
# {'name': 'Ferrari', 'owner': {'ref': {'class_name': 'User', 'object_id': '...', ...}}}

# Same payload, serialized to JSON
car.model_dump_json_refs()
# '{"name":"Ferrari","owner":{"ref":{"class_name":"User","object_id":"...","object_version":"..."}}}'

Further Reading

For the full set of Pydantic features — custom types, complex validators, computed fields, JSON Schema generation, and more — see the Pydantic documentation.