Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."
End-to-end cutover. No transitional path left on main.
Forward direction:
cores/mizan-python/src/mizan_core/ir.py
build_ir() walks mizan_core.registry, introspects Pydantic
models directly (no JSON-Schema indirection), and emits the
Mizan IR document. The KDL grammar is locked in this file's
module docstring.
Backends emit KDL:
backends/mizan-fastapi/src/mizan_fastapi/ir.py
`python -m mizan_fastapi.ir <module>` — CLI entry point.
backends/mizan-django/.../management/commands/export_mizan_ir.py
`manage.py export_mizan_ir` — Django mgmt command.
Codegen consumes KDL:
protocol/mizan-codegen/Cargo.toml: + kdl = "6"
protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
+ TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
replacing the JsonSchema sprawl. KDL parser walks the
`kdl::KdlDocument` tree into typed Rust structs.
protocol/mizan-codegen/src/fetch.rs: subprocess command switches
to the new IR-export entry points.
All emit modules (stage1 / react / python / rust / vue / svelte /
channels) port their type-walkers from JsonSchema to the new
sum types — case analysis collapses substantially.
Substrate-honesty wins beyond the moat closure:
- `int | bool` multi-arm unions land as `TypeShape::Union` (was
silently coerced to "string" before).
- `<CamelName>Output = list[T]` returns emit as named alias
types instead of struct-shaped wrappers, so consumer code
`.map()` works directly on the type.
- Pydantic field defaults flow through to `default` properties
in KDL, then back to non-optional shape in every target.
Deleted:
- backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
- backends/mizan-django/.../export_mizan_schema.py
- openapi-bearing half of mizan/export/__init__.py (edge
manifest generator preserved — separate concern).
- tests/afi/schema_normalizer.py
- tests/fixtures/{afi_schema.json, channels_schema.json}
- tests/fixtures/js_* baseline directories.
Verification:
- 20 mizan-codegen unit tests green (IR deserialization,
byte-equivalence parity across stage1/rust/python/react/vue/svelte
against fresh KDL-driven baselines, channels structural).
- tests/rust/run_wire_parity.py: 12/12 probes green driving
the binary end-to-end through KDL.
- Blazr studio-ui typechecks against the regenerated React client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mizan-fastapi
FastAPI backend adapter for the Mizan protocol. One decorator on a server function. Typed React client generated. Invalidation automatic.
Scope
mizan-fastapi targets the AFI-common subset — RPC dispatch, context bundling, JSON-body invalidation, and auth gating. Forms, Channels, Shapes, and SSR are out of scope for the FastAPI adapter — FastAPI projects use native equivalents (Pydantic, native WebSockets, ORM-of-choice, FastAPI's own SSR ecosystem).
Install
uv add mizan-fastapi
Setup
# main.py
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from mizan_fastapi import (
MizanError,
mizan_exception_handler,
mizan_validation_handler,
router as mizan_router,
)
app = FastAPI()
app.include_router(mizan_router, prefix="/api/mizan")
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
The exception handlers render every error path through the Mizan envelope
({"error": {"code", "message", "details"}}) so the kernel's MizanError
parses status + code on the frontend regardless of which failure happened.
Define server functions
from mizan_core.client.function import client
from mizan_core.registry import register
from pydantic import BaseModel
class EchoOutput(BaseModel):
message: str
@client
def echo(request, text: str) -> EchoOutput:
return EchoOutput(message=text)
register(echo, "echo")
mizan-fastapi has no auto-discovery (FastAPI doesn't have an app registry
to walk). Register every @client-decorated function explicitly. A typical
project keeps registrations in main.py (alongside the FastAPI app) or in
a dedicated clients.py imported during startup.
@client parameters
@client # plain RPC function
@client(context="global") # singleton context — fetched once, SSR-hydrated
@client(context="user") # named context — fetched per provider mount
@client(affects="user") # mutation — invalidates the user context
@client(affects=user_profile) # mutation — invalidates a specific function
@client(auth=True) # requires authentication
@client(auth="staff") # requires is_staff
@client(auth="superuser") # requires is_superuser
@client(auth=lambda req: ...) # custom predicate
@client(rev=2) # cache revision (busts on bump)
websocket=True, Forms, and Channels parameters are accepted by the
decorator (they're a mizan-core primitive) but ignored by mizan-fastapi —
those features only have effect when paired with mizan-django.
Auth integration
The executor expects request.state.user to be populated by your FastAPI
middleware or dependency tree before dispatch:
from fastapi import Request
@app.middleware("http")
async def attach_user(request: Request, call_next):
request.state.user = await resolve_user_from_token(request)
return await call_next(request)
Where resolve_user_from_token returns either a user object with
is_authenticated, is_staff, is_superuser attributes, or None for an
anonymous request. The executor branches on those for auth=True,
auth="staff", auth="superuser" requirements.
Generate the frontend
The codegen is mizan-generate (in protocol/mizan-generate/). Point a
config at your FastAPI app and run the CLI:
// frontend/fastapi.config.mjs
import path from "path"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
export default {
source: {
fastapi: {
module: "main", // module to import for @client side effects
cwd: path.join(root, "backend"), // python cwd for module resolution
command: ["uv", "run", "python"], // optional — defaults to ["python"]
},
},
output: "src/api",
}
npx mizan-generate --config fastapi.config.mjs
The codegen drives python -m mizan_fastapi.cli <module> under the hood,
then emits Stage 1 (typed callXxx/fetchXxx over the runtime kernel) +
Stage 2 (<MizanContext> provider, per-context providers, use{Hook}()
hooks) into src/api/.
// app.tsx
import { MizanContext } from "./api"
export default function App({ children }) {
return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
}
// any component
import { useEcho, useCurrentUser } from "./api"
const echo = useEcho()
echo.mutate({ text: "hi" }).then(r => console.log(r.message))
const user = useCurrentUser() // global context — auto-fetched, auto-refreshed on mutation
Running tests
uv sync --extra dev
uv run pytest
Schema export CLI
For codegen consumption (or any tooling that wants the Mizan schema):
python -m mizan_fastapi.cli <module>
Imports the named module (which must register every @client function as
import-time side effects), then prints the OpenAPI schema as JSON to stdout.
Mirrors mizan-django's manage.py export_mizan_schema so the codegen
consumes either backend the same subprocess way.
Architecture
mizan-fastapi is one of two reference backend adapters (the other is
backends/mizan-django). Both implement the same Mizan protocol on top of
the shared cores/mizan-python core (@client, registry, MWT, HMAC cache
keys). The AFI conformance suite at tests/afi/ gates that the two adapters
emit equivalent schemas for the same registered functions. See
docs/AFI_ARCHITECTURE.md.
A live e2e harness exercises this adapter end-to-end at
examples/fastapi-react-site/ (real Chromium → React with generated hooks
→ FastAPI server, 14/14 Playwright tests).