Skip to content

Dynamic Schemas and Typed Serialization

Swarmauri components are designed to cross process boundaries without losing their concrete kin. A component can be initialized from JSON, YAML, or TOML, dumped back to those formats, and restored with its type discriminator intact.

That is a core platform strength for extensible systems. Factories, HTTP APIs, queues, and databases often only see serialized payloads. Without a dynamic schema layer, each boundary has to choose between untyped dictionaries or a manual switch such as if type == "ApiConnector". Both approaches drift as new component classes become available.

Swarmauri solves this with DynamicBase, ComponentBase.register_model(...), ComponentBase.register_type(...), and SubclassUnion[...]. Registered kin are available to discriminated unions, and Pydantic can emit JSON Schema for the current runtime surface.

Define a Typed Component Family

from typing import Literal

from swarmauri_base.ComponentBase import ComponentBase


@ComponentBase.register_model()
class DataSource(ComponentBase):
    type: Literal["DataSource"] = "DataSource"
    label: str


@ComponentBase.register_type(DataSource, "ApiDataSource")
class ApiDataSource(DataSource):
    type: Literal["ApiDataSource"] = "ApiDataSource"
    endpoint: str
    auth_required: bool = True


@ComponentBase.register_type(DataSource, "TableDataSource")
class TableDataSource(DataSource):
    type: Literal["TableDataSource"] = "TableDataSource"
    table: str
    primary_key: str

Carry Concrete Kin Through an Envelope

from typing import Literal

from swarmauri_base.ComponentBase import ComponentBase
from swarmauri_base.DynamicBase import SubclassUnion


@ComponentBase.register_model()
class DataSourceEnvelope(ComponentBase):
    type: Literal["DataSourceEnvelope"] = "DataSourceEnvelope"
    component: SubclassUnion[DataSource]


envelope = DataSourceEnvelope.model_validate(
    {
        "type": "DataSourceEnvelope",
        "component": {
            "type": "ApiDataSource",
            "label": "orders api",
            "endpoint": "https://api.example.test/orders",
            "auth_required": False,
        },
    }
)

assert isinstance(envelope.component, ApiDataSource)
assert "ApiDataSource" in str(DataSourceEnvelope.model_json_schema())

The envelope field is typed as the base family, but the runtime value is the registered concrete subclass. The JSON Schema includes the available type choices instead of a stale hand-written schema.

Factory Hydration

Factories can hydrate serialized component specs through the typed envelope instead of owning a switch for every possible kin class.

class ConfigHydratingFactory:
    def create_from_spec(self, spec: dict) -> DataSource:
        envelope = DataSourceEnvelope.model_validate(
            {"type": "DataSourceEnvelope", "component": spec}
        )
        return envelope.component


factory = ConfigHydratingFactory()
component = factory.create_from_spec(
    {
        "type": "ApiDataSource",
        "label": "orders api",
        "endpoint": "https://api.example.test/orders",
    }
)

assert isinstance(component, ApiDataSource)

The factory does not need to know every subclass. New registered kin can become available to the schema and hydration path without rewriting factory control flow.

API Payloads

The same model shape works for HTTP request or response bodies. An API handler can validate a base-family payload and still receive the concrete subclass.

payload = {
    "type": "DataSourceEnvelope",
    "component": {
        "type": "TableDataSource",
        "label": "warehouse events",
        "table": "events",
        "primary_key": "event_id",
    },
}

validated = DataSourceEnvelope.model_validate(payload)

assert isinstance(validated.component, TableDataSource)
assert validated.component.table == "events"

Without this pattern, the handler usually receives dict[str, Any] and must reconstruct typed objects manually.

Database Persistence

Persist component specs as JSON and restore them later through the same typed path.

import sqlite3

original = DataSourceEnvelope(
    component=ApiDataSource(
        label="orders api",
        endpoint="https://api.example.test/orders",
    )
)

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE component_specs (payload TEXT NOT NULL)")
db.execute(
    "INSERT INTO component_specs (payload) VALUES (?)",
    (original.model_dump_json(),),
)

stored_json = db.execute("SELECT payload FROM component_specs").fetchone()[0]
restored = DataSourceEnvelope.model_validate_json(stored_json)

assert isinstance(restored.component, ApiDataSource)
assert restored.component.endpoint == original.component.endpoint

The database stores portable JSON, while Swarmauri restores the concrete Python component when the payload is loaded.

JSON, YAML, and TOML Configs

ComponentBase includes YAML and TOML helpers alongside Pydantic's JSON support. The same discriminator shape can be used in operator-friendly config files.

yaml_config = """
type: DataSourceEnvelope
component:
  type: ApiDataSource
  label: yaml api
  endpoint: https://yaml.example.test
  auth_required: false
"""

toml_config = """
type = "DataSourceEnvelope"

[component]
type = "ApiDataSource"
label = "toml api"
endpoint = "https://toml.example.test"
auth_required = false
"""

from_yaml = DataSourceEnvelope.model_validate_yaml(yaml_config)
from_toml = DataSourceEnvelope.model_validate_toml(toml_config)

assert isinstance(from_yaml.component, ApiDataSource)
assert isinstance(from_toml.component, ApiDataSource)

This keeps one component contract across JSON APIs, YAML deployment files, TOML project config, and JSON database storage.