Skip to main content
Documentation

Python

Parse and serialize shared models with Pydantic.

The Python generator produces a typed package built on Pydantic 2. Records become models, wrappers become root models, and variants get case models and JSON adapters. It also emits a py.typed marker for type checkers.

Generate a Package

Generate the task bundle into a Python package directory:

sidex generate py task_models
python -m pip install "pydantic>=2.5"

Generated model modules and their vendored _sidex.py runtime support mypy --strict without module exclusions.

Use Python 3.12 or newer, as used by Sidex’s Python integration tests. Run your application from a directory where task_models is importable.

Parse and Serialize a Record

from task_models.task import Task

task = Task.model_validate_json(
    '{"title":"Write a schema","completed":false}'
)

assert task.title == "Write a schema"
assert task.description is None

encoded = task.model_dump_json(by_alias=True)
print(encoded)

Pydantic validates incoming values according to its model configuration. by_alias=True writes JSON field names. Generated models omit optional fields whose value is None by default. Required nullable fields and null inside JSON values, sequences, and maps are preserved. Passing exclude_none=True is unnecessary and also drops required fields whose value is None.

Booleans and integers require their JSON types, and integers must fit the declared range, like in the other runtimes. Only 64-bit integers also accept decimal strings. This keeps implicitly tagged variants from interpreting 1 as a boolean or 300 as a u8.

Python models use Python field identifiers. JSON aliases follow Sidex’s naming rules, which default to camelCase. Do not assume that Python’s field name is also the wire key.

Inline Fields

An #[json(inline)] field holds a record, directly or through aliases and wrappers. Its nested Python model shares its JSON object with the enclosing record:

record Payload { value: string, note?: string }
record Envelope { #[json(inline)] payload: Payload }

With these definitions in messages.sidex:

from task_models.messages import Envelope

envelope = Envelope.model_validate_json('{"value":"x"}')
assert envelope.payload.value == "x"
assert envelope.model_dump_json(by_alias=True) == '{"value":"x"}'

Nested inline records, aliases, and wrappers of records use the same representation. Inline fields must be required, and JSON names must be unique across the flattened object. Fields within an inline record may be optional; absent values are omitted. Unknown fields are ignored by default.

Variants and Wrappers

Use Pydantic’s TypeAdapter for a generated variant type:

#[json(tagged = adjacently, tag = "state", content = "value")]
variant Outcome {
    Pending,
    Complete: string,
}

After generating a package with this schema in outcomes.sidex:

from pydantic import TypeAdapter
from task_models.outcomes import Outcome

adapter = TypeAdapter(Outcome)
value = adapter.validate_json('{"state":"Complete","value":"done"}')
encoded = adapter.dump_json(value, by_alias=True)

The adapter applies the wire representation specified by the schema. For wrappers, access the wrapped value through the Pydantic root model’s root property.

Access a Case Payload

Generated cases with a payload provide a payload() method. For an internally tagged record case, the fields are inlined in the case model. payload() constructs a fresh instance of the original record. A record field named payload becomes payload_ in a case model to keep the method available:

record Point {
    x: i32,
    y: i32,
}

variant Position {
    Unknown,
    Known: Point,
}

With these definitions in outcomes.sidex:

from pydantic import TypeAdapter
from task_models.outcomes import Position, Point

position = TypeAdapter(Position).validate_json('{"tag":"Known","x":3,"y":4}')
point = position.payload()
assert isinstance(point, Point)
assert (point.x, point.y) == (3, 4)

Map Native Opaque Types

#[json(type = "string")]
#[py(typ = "uuid.UUID")]
opaque UserId

The generator imports the native module and uses its type. Your Python environment must provide that module. Without a Python mapping, opaque types fall back to their JSON shape.

Set opaque_lowering = "json" to ignore native Python mappings, for example when testing raw JSON round trips:

[backend.py]
opaque_lowering = "json"

Reuse Other Bundles

[backend.py.external]
identity = "identity_models"

Generate identity separately into an importable package. The generated bundle initializer resolves references between sibling schemas after loading them, including recursive references.

Validation Rules

Pydantic performs model parsing, but Sidex’s #[validate(...)] rules currently generate validators only for Rust and TypeScript. They are not enforced by the Python backend. Apply equivalent application checks when your Python code requires those constraints.