Skip to content

Review API compatibility

Tenchi derives OpenAPI 3.1 from composed contracts. The document describes the same types, headers, errors, media types, examples, visibility, retry guarantees, and successful response definitions enforced by the server and client.

Generate a document

openapi_schema() is a pure function:

from tenchi.openapi import openapi_schema


document = openapi_schema(
    api_routes,
    title="Todos",
    version="1.0.0",
    description="Todo service API",
    security={
        "bearerAuth": {"type": "http", "scheme": "bearer"},
    },
)

Pass only the application route group you want documented. Public contracts are exempted from global security through the same contract.public metadata used by authentication hooks.

Discover error behavior

Every error response includes the stable Tenchi envelope and narrows code to the exact values that status may return. It also documents the required x-tenchi-error-source header as app, framework, or both. A generated client or agent can therefore distinguish declared application outcomes from framework failures without parsing response descriptions.

Every operation documents 500 with the framework-owned INTERNAL_SERVER_ERROR code. Tenchi uses that response for unexpected exceptions and undeclared application errors.

Security schemes describe how to authenticate; they cannot determine which authentication errors an application uses. Declare authentication failures on the protected route group so their statuses and codes appear on each affected operation:

private_routes = route_group(
    project_routes,
    task_routes,
    errors=(unauthorized,),
)

Teach clients and agents how to call an operation

Named request_examples= and response_examples= values flow from a contract to the corresponding OpenAPI Media Type Object. Tenchi runs them through the declared Pydantic validator and serializer, revalidates the serialized wire payload through the receiving boundary, and checks the resulting JSON against the published schema. This keeps aliases, custom validators, and custom serializers honest while giving external clients and agents concrete payloads they can adapt.

create_todo_contract = contract(
    method="POST",
    path="/todos",
    request=CreateTodo,
    response=Todo,
    status=201,
    request_examples={"create": CreateTodo(title="Buy milk")},
    response_examples={
        "created": Todo(id="todo_123", title="Buy milk", completed=False)
    },
)

Changing examples is metadata-only for compatibility. Do not put secrets, personal data, or production payloads in them because the OpenAPI document is often public.

Contracts marked idempotency_key=True or webhook=True publish Tenchi extensions that describe caller-visible guarantees. Their compatibility rules are listed under Constraints.

Serve OpenAPI

from tenchi.openapi import openapi_route, swagger_ui_route
from tenchi.routes import route_group


routes = route_group(
    api_routes,
    openapi_route(
        api_routes,
        title="Todos",
        version="1.0.0",
    ),
    swagger_ui_route(title="Todos API"),
)

The default path is /openapi.json. The serving route is public by default and does not include itself in the document. Swagger UI defaults to /docs and loads that document through a relative URL, so ASGI root_path mounting keeps both routes aligned.

The default Swagger JavaScript and stylesheet are pinned CDN assets with subresource integrity metadata. Override swagger_js_url=, swagger_css_url=, and swagger_favicon_url= to self-host them. Set public=False on both routes when the API description requires authentication.

Store a canonical snapshot

uv run tenchi openapi --write openapi.json
uv run tenchi openapi --check openapi.json

--write produces deterministic, key-sorted JSON. --check is an exact equality check suitable for a repository test. By default the command loads app.server.routes:api_routes and discovers literal OPENAPI_TITLE, OPENAPI_VERSION, OPENAPI_DESCRIPTION, and OPENAPI_SECURITY declarations from that module. Use the corresponding flags only to override this convention.

Classify changes

uv run tenchi openapi --diff openapi-baseline.json

The analyzer classifies changes as:

Breaking and unknown changes return a failing exit status. Use --diff-format json for automation.

Try a breaking and additive change

In a generated application, open app/features/todos/schemas.py and tighten the title constraint:

 class CreateTodo(BaseModel):
-    title: str = Field(min_length=1)
+    title: str = Field(min_length=3)

Compare the current contracts with the checked-in snapshot:

uv run tenchi openapi --diff openapi.json

The command fails because existing callers can send titles that the new constraint rejects. Restore min_length=1, then add an optional field:

class CreateTodo(BaseModel):
    title: str = Field(min_length=1)
    notes: str | None = None

The same diff command now reports an additive change. After reviewing it, update the snapshot and check the application:

uv run tenchi openapi --write openapi.json
uv run tenchi check

Use tenchi verify --base-ref <historical-ref> when the repository also needs one receipt for its checks, application map, verification policy, and every versioned boundary.

Compare before updating

A compatibility gate needs a snapshot from the pull-request base, previous push, or previous release. If a breaking change and its new snapshot are committed together, comparing the generated document with that same snapshot proves only equality.

CI pattern

uv run tenchi openapi --diff-ref "$BASE_SHA" --snapshot openapi.json
uv run tenchi openapi --check openapi.json

--diff-ref resolves the snapshot inside the current Git repository and reads REF:PATH without changing the working tree. Missing refs and snapshots fail the command. Generated CI uses the pull request's base SHA; push checks retain exact snapshot validation without assuming a previous commit exists.

Programmatic consumers can import analyze_openapi_compatibility() from tenchi.compatibility and inspect its structured CompatibilityReport.

Constraints