Quickstart¶
Point castiron at a schema, get typed Pydantic models. No database connection required — the OpenAPI/PostgREST source reads your project's public API description over HTTPS, so there is no connection string, no driver, and nothing to open in a firewall.
Pre-alpha
castiron is on PyPI and the install below works, but it is young and moving fast — APIs may change between releases. Everything on this page is the CLI as it ships.
Install¶
uv add cast-iron # or: pip install cast-iron
castiron --version # the command has no hyphen
You install cast-iron; you run castiron
Install the hyphenated cast-iron, then run the unhyphenated castiron —
cast-iron --help will only ever tell you command not found, because no such
command is installed. It is the ordinary Python split between what you install and
what you get: pip install python-dateutil gives you import dateutil, and
pip install scikit-learn gives you import sklearn. Here the import package is
castiron too. PyPI does not allow castiron as a distribution name, so the hyphen
is permanent; nothing else about the name changes.
castiron supports Python 3.10–3.13 and pulls in three runtime dependencies (click,
inflection, pydantic) plus tomli on Python 3.10 only.
From a checkout¶
You do not need this to use castiron — it is how you run the code you are editing when you work on castiron itself:
git clone https://github.com/kmbhm1/castiron.git
cd castiron
uv sync
uv run castiron --version
Every castiron ... command below works as uv run castiron ... from a checkout.
One command to typed models¶
export CASTIRON_KEY='eyJhbGciOi...'
castiron gen --from https://abcdefgh.supabase.co --emit pydantic
castiron: read 6 tables, 1 enum and 4 functions from https://abcdefgh.supabase.co/rest/v1/
castiron: wrote schema.py (14.2 kB)
The counts and the size come from your schema; the two-line shape is fixed. That counts line is worth reading: PostgREST only exposes what your API key's role can see, so "read 2 tables" when you expected 20 means row-level security or a role grant is hiding things — not that castiron missed them.
A bare Supabase project URL is rewritten to its REST root
(https://<ref>.supabase.co/rest/v1/); a plain PostgREST deployment works too — pass its
API root. Put the key in CASTIRON_KEY rather than on the command line, where it lands
in your shell history. See Environment variables.
Offline: generate from a saved OpenAPI document¶
--from also takes a path. This is the reproducible path — it touches no network at all,
which makes it ideal for CI, for air-gapped builds, and for the rest of this page.
Save the document once — this is the same request castiron makes:
curl -s https://abcdefgh.supabase.co/rest/v1/ \
-H "apikey: $CASTIRON_KEY" \
-H "Authorization: Bearer $CASTIRON_KEY" \
-H "Accept: application/openapi+json" \
-H "Accept-Profile: public" \
> openapi.json
Then generate from it as often as you like:
castiron gen --from ./openapi.json --emit pydantic --output out
castiron: 4 tables have an integer primary key with no visible default (orders, products, restricted_table and 1 more) -- PostgREST does not expose nextval()/identity defaults, so castiron marks those columns required on the Insert models. Pass --infer-generated-primary-keys (or set infer-generated-primary-keys = true) if they are serial/identity columns.
castiron: read 6 tables, 1 enum and 4 functions from openapi.json
castiron: wrote out/schema.py (8.5 kB)
The first line is a fidelity warning on stderr, and it is the one rough edge you will
meet immediately. PostgREST does not publish nextval()/identity defaults, so castiron
cannot tell a bigint generated by default as identity primary key from a natural
integer key — and marks it required on the Insert model. If those columns really are
serial/identity, add --infer-generated-primary-keys. The full trade-off is on
What the OpenAPI source can and cannot see.
The summary lines go to stdout; logs, warnings and errors go to stderr. -q
silences the summary and leaves errors intact.
What you get¶
One file, schema.py. It opens with two comment lines recording the castiron version that
wrote it (the provenance header), and
the rest is organised into bands of classes:
class UsersBaseSchema(CustomModel):
"""Users Base Schema.
Application users.
"""
# Primary Keys
id: int
# Columns
bio: str | None = Field(default=None, description="A short profile blurb.")
created_at: datetime.datetime | None = Field(default=None)
email: str
field_class: str | None = Field(default=None, alias="class")
is_active: bool
login_count: int
metadata: dict | list[dict] | list[Any] | Json | None = Field(default=None)
status: PublicOrderStatusEnum
tags: list[str] | None = Field(default=None)
| Band | Classes | What it is for |
|---|---|---|
| Enums | PublicOrderStatusEnum |
One str, Enum per Postgres enum type the schema uses (member naming) |
| Base (Row) | UsersBaseSchema |
Every column, exactly as the row comes back (class naming) |
| Insert | UsersInsert |
Insert payloads: server-defaulted columns are optional |
| Update | UsersUpdate |
Update payloads: every field optional |
| Operational | Users |
The Base model plus nested foreign-key relationship fields — extend this one |
Column comments become description=, a table comment (COMMENT ON TABLE) becomes the
model docstring on every class generated for that table, a column name Python cannot use —
a reserved word like class, but also 2fast or space name — becomes a safe attribute with
the real name kept on alias= (column names),
and a foreign key becomes a real nested model:
class Orders(OrdersBaseSchema):
"""Orders Schema for Pydantic.
Customer orders.
Inherits from OrdersBaseSchema. Add any customization here.
"""
# Foreign Keys
user: Users | None = Field(default=None)
order_items: list[OrderItems] | None = Field(default=None)
Preview before you write¶
--dry-run runs the whole pipeline and reports what would land, creating no file and
no directory:
castiron gen --from ./openapi.json --output out2 --dry-run
castiron: read 6 tables, 1 enum and 4 functions from openapi.json
castiron: would write out2/schema.py (8.5 kB) [dry run, nothing written]
Regeneration overwrites by default — that is the point of a compiler. --no-overwrite
turns an existing target into a loud failure, checked for every file before any of them
is written, so you never get a half-generated tree:
castiron gen --from ./openapi.json --output out --no-overwrite
Error: out/schema.py already exists and --no-overwrite was given; nothing was written.
Put the settings in your project¶
Stop retyping flags. Every option except the API key is a [tool.castiron] key in your
pyproject.toml:
[tool.castiron]
from = "openapi.json"
emit = ["pydantic"]
output = "src/myapp/models"
castiron gen
castiron: read 6 tables, 1 enum and 4 functions from /home/you/proj/openapi.json
castiron: wrote src/myapp/models/schema.py (8.5 kB)
Relative paths in the config resolve against the config file's directory, not your
shell's, so the same command produces the same file whether you run it from the project
root or three directories down. The API key is deliberately rejected in a config file
— pyproject.toml gets committed. Full details, including the precedence chain, on
Configuration.
Determinism, and why it matters¶
The same schema, the same options and the same castiron version produce the same bytes,
every time — no timestamps, no source URL in the file, and no post-hoc ruff/black pass
whose version could change the output. The file's first two lines record which castiron
wrote it (the provenance header), and
that version is the only input that varies. That is what makes generated models safe to commit
and diff, and it is the foundation castiron check stands
on — the recorded version is what lets it tell a castiron upgrade apart from a schema change.
Running no formatter afterwards does not mean the output is untidy: it is written clean as
emitted, and castiron promises that it passes ruff's F, UP and I rules at ruff's own
default settings — so a generated module does not trip the linter of the project you just added
it to. The promise and its limits (E501 is explicitly not covered) are on
The generated code.
Where to go next¶
- What the OpenAPI source can and cannot see — the honest fidelity floor of the no-credentials path. Read this before you trust a generated constraint.
- The generated code — what castiron promises about the bytes it writes, how enum member names are derived, and the known limitations.
- CLI reference — every flag, generated from the command itself.
- Configuration — the
[tool.castiron]table and the precedence rules. - Exit codes — for scripting and CI.