crozier
An ultra-fast, memory-efficient OpenAPI-to-SDK code generator written in Rust + minijinja. crozier reproduces Fern's generated SDKs byte-for-byte (with generator-identifying comments aside), driven by nothing but an OpenAPI document and a couple of naming flags — no per-project config, no generators written in the target language.
Python is the only target today; more will follow.
Status: pre-1.0. crozier generates the whole Python SDK — the type layer (pydantic models, enums, unions/aliases), the endpoint clients, the
core/runtime, and the packaging scaffolding — and reproduces Fern's output byte-for-byte across every corpus in its golden suite. Seedocs/matching.mdfor how that is verified.
See it in action
One OpenAPI document in, a complete typed Python SDK out — crozier generate
writes the whole package in one shot, then you can read the code it produced:
The output is byte-for-byte Fern's. Here is the Pet model crozier emitted from a
handful of lines of schema — a real generated file, not a hand-written sample:
crozier generate python takes the document and a couple of naming flags and
writes the SDK — the models, enums, the per-endpoint client, and Fern's core/
runtime — into --output:
Or drive one — or several — named generators from a crozier.yml instead of
flags. crozier config shows the effective settings for every generator and the
layer each value resolved from (CLI flag > CROZIER_* env > file > default):
crozier init drops a starter crozier.yml carrying a JSON Schema modeline, so
editors complete and validate it against crozier's own config schema:
The full command surface (crozier --help and crozier generate --help)
A malformed document is rejected at the boundary with an actionable message and a non-zero exit — never a panic:
These are real captures of the CLI, rendered from its actual output by
just screenshotsand gated by screencomp — change what a command prints and the committed image (and its digest) changes with it.
Install
From PyPI (fastest — a prebuilt binary, no Rust toolchain): crozier ships as
platform wheels that wrap the compiled binary and expose it as a console script,
so any Python installer puts it on your PATH in seconds:
pip install crozier # or: pipx install crozier
uvx crozier --help # run once without installing
Platforms without a prebuilt wheel fall back to the source distribution, which builds from Rust (a toolchain is needed there).
From crates.io:
cargo install crozier --locked
From the install script (prebuilt binary from GitHub Releases, verified):
curl -fsSL https://raw.githubusercontent.com/nickderobertis/crozier/main/scripts/install.sh | sh
It detects your platform, downloads the matching archive, and verifies it against
a trust root independent of where it was downloaded — a Sigstore build-provenance
attestation when a verifier (cosign, pip install sigstore, or gh) is
present, else the canonical SHA-256 checksum. Pin a version or install location
with sh -s -- --version v0.1.0 --to ~/.local/bin.
From source (latest main):
cargo install --git https://github.com/nickderobertis/crozier --locked
From a release archive (manual): each release publishes per-platform archives,
each with a matching .sha256 and a .sigstore.json provenance bundle. Download
the archive for your platform from the
Releases page — it lists
every target built — verify the checksum (or the attestation), and put the
crozier binary on your PATH.
Usage
The fastest path needs no config file — the built-in python generator runs
straight from flags:
crozier generate python \
--spec path/to/openapi.yml \
--output ./generated \
--package-name my_api \
--project-name my-api
--spec— the OpenAPI 3.x document (.yml,.yaml, or.json).--output— directory to write the SDK into.--package-name— the Python import package (the directory undersrc/). Defaults to asnake_caseof the API title.--project-name— the distribution name recorded inversion.py. Defaults to the package name.--client-class-name— the name of the generated root client class (Fern'sclient_class_name). Defaults to{PascalCase(package_name)}Api.--audience(repeatable) /--audience-strict— prune generation tox-crozier-audiences.--fern-strict— refuse every document in a class Fern refuses (alsoCROZIER_FERN_STRICT=true, orfern-strict: trueincrozier.yml). A refusal exits1, writes nothing, and names the refusal class and the offending element. Each class Fern refuses is decided once, indocs/fern-refusals/:refuse, where crozier refuses the document whatever this setting says, orgenerate, where it writes a valid SDK by default and refuses only under--fern-strict. Every class registered today isrefuse, so the flag changes no outcome yet.
crozier exits 0 on success (with a one-line summary on stderr), 1 on an
error, printing the exact problem and a suggested fix, and 2 on a usage error
(a bad flag or argument). crozier compare adds 3 and 4 for its results
(docs/compare.md).
To check crozier against the SDK you generate today, before switching, run
crozier compare: for every generator your crozier.yml files declare, it runs a
reference command you configure, generates crozier's SDK, compares the two trees
after crozier's documented normalizations (comments, SDK-identity headers,
__init__.py import order, the generatorConfig in Fern's own
.fern/metadata.json; see
docs/matching.md), and reports the
time each side took. See
docs/compare.md. Moving from Fern, the
Fern reference recipe produces that reference.
In CI, the GitHub Action runs the same check on every pull request and reports it in the job summary:
- uses: nickderobertis/crozier@v0
See docs/github-action.md for its inputs, outputs and
versioning, and the one workflow a repository migrating from Fern adds.
Configuration
crozier runs one or more named generators. You can drive them purely from
flags (above), from a crozier.yml, or any mix — every setting resolves per
field as:
CLI flag > CROZIER_* env var > crozier.yml (generator over shared) > built-in default
Run crozier init to drop a starter crozier.yml in the working directory. It
leads with a JSON Schema modeline —
# yaml-language-server: $schema=https://raw.githubusercontent.com/nickderobertis/crozier/main/assets/crozier.schema.json
— so editors with the YAML language server give field completion and
validation against the published schema (derived
from crozier's own config types, so it never drifts). Run crozier config to
print the effective settings and the layer each value came from.
A crozier.yml in the working directory is picked up automatically. Top-level
keys are shared defaults; each entry under generators: is one SDK to emit:
# Shared across every generator
spec: ./openapi.yml
audiences: [public]
generators:
python: # overrides the built-in `python` generator
output: ./sdks/python
package-name: my_api
project-name: my-api
admin: # a second generator (also Python for now)
type: python
spec: ./admin-openapi.yml
output: ./sdks/admin
package-name: admin_api
crozierorcrozier generate— run every configured generator (or the built-inpythonwhen nothing is configured).crozier generate <name>— run one generator by name (pythonalways works, even with no config file).crozier init— write a startercrozier.yml(--output,--force).crozier config [<name>]— show the effective config and each value's source.crozier schema— print the config JSON Schema to stdout.--config <path>(repeatable, later wins) selects config files instead of auto-discovery;--no-configignores config files entirely;CROZIER_CONFIGnames a file via the environment.
Per-generation flags (--spec, --output, …) apply to a single generator, so
they are rejected when more than one would run — name one, or set the values in
crozier.yml. See docs/configuration.md for the full
reference.
Migrating from Fern
A repository can move from Fern's Python generator to crozier one generator at a
time, checking each against Fern before it switches. Before step 1, bring the
Fern generator to the version crozier certifies; that and the other steps a
migration may still need are in
docs/migrating-from-fern.md.
1. Add crozier config beside the Fern config, one generator at a time. Save
your copy of the Fern reference recipe and name it as
reference.command. Set layout to how that generator's Fern output was
written: flat for a local-file-system output path (point output where that
tree lives), packaged (the default) for fern generate --preview --output:
# crozier.yml, at the repository root beside fern/
generators:
python:
spec: ./fern/openapi/openapi.yml
output: ./sdks/python # the generator's local-file-system path
package-name: acme # fern.config.json's organization
layout: flat
reference:
command: ./scripts/fern-reference.sh
2. Compare until every generator matches, locally and in CI with the GitHub Action:
crozier compare
It checks only generators that have crozier config, so the ones still on Fern do not fail it. crozier does not track what is left; list the Python generators your Fern config still runs:
grep -rn --include=generators.yml 'fernapi/fern-python-sdk' .
docs/compare.md covers the report, exit statuses, JSON
report, timings and the reference-command contract.
3. Switch the build from fern generate to crozier:
crozier generate python
4. Remove that generator (its fernapi/fern-python-sdk entry) from
generators.yml. TypeScript and other generators stay on Fern.
Development
The command surface is a small set of just recipes:
just bootstrap # set up from a clean clone (toolchain + dev tools)
just check # full gate — must pass before any commit or PR
just test # fast tests with coverage enforced (95%)
just test-e2e # drive the compiled binary and byte-compare against fixtures
just format # rustfmt in place
just upgrade # cargo update, then re-run the gate
Bootstrap enables the repository hooks (the .githooks/pre-push visual-regression
guard). The tracked .cargo/config.toml is the build contract: every cargo
invocation inside a clone builds into that clone's own target/ (two worktrees
never share one), and dev/test builds keep line tables only (debug = 1) so full
debuginfo stays off the disk. Nothing in the tree configures sccache; to share
compiler results across worktrees, set [build] rustc-wrapper = "sccache" in
your user-level ~/.cargo/config.toml, which Cargo merges with the repository's.
See AGENTS.md for the durable contributor guide and
docs/matching.md for the byte-matching strategy.
License and attribution
crozier is licensed under Apache-2.0. It is an independent, clean-room implementation — it reproduces Fern's generated output format (the project's explicit goal) and does not copy Fern's generator source.
The test fixtures under tests/fixtures/ are Fern's own output and OpenAPI test
specs, used under Apache-2.0 with attribution and a statement of changes; see
NOTICE and licenses/fern-APACHE-2.0.txt.
Metadata
Release files for crozier 0.0.95
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| crozier-0.0.95.tar.gz | 556.9 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| crozier-0.0.95-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| crozier-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| crozier-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| crozier-0.0.95-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| crozier-0.0.95-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 12.2 MB
Release files / crozier-0.0.95.tar.gz
| Download URL | crozier-0.0.95.tar.gz |
|---|---|
| Size | 556.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a801c8a9a9ec088740a1db6bc82dab107c462d016b9e17b7a9815f9a42538d9
|
|
BLAKE2b-256 checksum How to use checksums |
c42898a2386427560d901912e7b1d009df02c2a05dcac12389738e9d9c63fabe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / crozier-0.0.95-py3-none-win_amd64.whl
| Download URL | crozier-0.0.95-py3-none-win_amd64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
52fb48db85f6a2d8d39b9e965d64c9add2860048612ddefcd59072fd213af9e9
|
|
BLAKE2b-256 checksum How to use checksums |
c25c3cf44687d1a89eed68e03eb7ca8b93b37f2347890de7248187d28626d244
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / crozier-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | crozier-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
72da8d4b958cd2c3ff8bc02805ba94048cfd9c785723abf7df1124390adc116c
|
|
BLAKE2b-256 checksum How to use checksums |
ae11bac79e0dbc3ff6c4ae47bce9acb5b0e757ec956642c828b3d7e8c64acd15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / crozier-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | crozier-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
a3a8399da43c580ba2185d7d189f06959c05a666828e69bd980f63eec856664d
|
|
BLAKE2b-256 checksum How to use checksums |
3d38c71b37e754f3439ac2a0704e09004dd51994cd494d68d5a91df24368e492
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / crozier-0.0.95-py3-none-macosx_11_0_arm64.whl
| Download URL | crozier-0.0.95-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
59e08c729b24e6b2408980d422606d812bf3a8f300b89e0082d2b3ba04b00df0
|
|
BLAKE2b-256 checksum How to use checksums |
0cfe371ec178bdea63279fbad85ae9986200b33de857df24bfee9aee01b9fdd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / crozier-0.0.95-py3-none-macosx_10_12_x86_64.whl
| Download URL | crozier-0.0.95-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
b02738a445a22c3067749f56b964257336d85b81e1fbf77dee7a0493681cad90
|
|
BLAKE2b-256 checksum How to use checksums |
c4c61836085e3d5e76766a9652d26e451c944d5939f2f2da04e9be9cb1d0519b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|