OpenAPI Docs a.k.a Swagger

After deploying the application, Batman got multiple queries from the users on how to use the endpoints. Robyn showed him how to generate OpenAPI specifications for his application.

Out of the box, the following endpoints are setup for you:

  • /docs The Swagger UI
  • /openapi.json The JSON Specification

To use a custom openapi configuration, you can:

  • Place the openapi.json config file in the root directory.
  • Or, pass the file path to the openapi_file_path parameter in the Robyn() constructor. (the parameter gets priority over the file).

However, if you don't want to generate the OpenAPI docs, you can disable it by passing --disable-openapi flag while starting the application.

python app.py --disable-openapi

How to use?

  • Query Params: The typing for query params can be added as def get(r: Request, query_params: GetRequestParams) where GetRequestParams is a subclass of QueryParams
  • Path Params are defaulted to string type (ref: https://en.wikipedia.org/wiki/Query_string)

Basic App

from robyn.robyn import QueryParams

from robyn import Robyn, Request

app = Robyn(
    file_object=__file__,
    openapi=OpenAPI(
        info=OpenAPIInfo(
            title="Sample App",
            description="This is a sample server application.",
            termsOfService="https://example.com/terms/",
            version="1.0.0",
            contact=Contact(
                name="API Support",
                url="https://www.example.com/support",
                email="support@example.com",
            ),
            license=License(
                name="BSD2.0",
                url="https://opensource.org/license/bsd-2-clause",
            ),
            externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"),
            components=Components(),
        ),
    ),
)


@app.get("/")
async def welcome():
    """welcome endpoint"""
    return "hi"


class GetRequestParams(QueryParams):
    appointment_id: str
    year: int


@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"])
async def get(r: Request, query_params: GetRequestParams):
    """Get Name by ID"""
    return r.query_params


@app.delete("/users/:name", openapi_tags=["Name"])
async def delete(r: Request):
    """Delete Name by ID"""
    return r.path_params


if __name__ == "__main__":
    app.start()

How does it work with subrouters?

Subrouters

from robyn.robyn import QueryParams

from robyn import Request, SubRouter

subrouter: SubRouter = SubRouter(prefix="/sub")


@subrouter.get("/")
async def subrouter_welcome():
    """welcome subrouter"""
    return "hiiiiii subrouter"


class SubRouterGetRequestParams(QueryParams):
    _id: int
    value: str


@subrouter.get("/name")
async def subrouter_get(r: Request, query_params: SubRouterGetRequestParams):
    """Get Name by ID"""
    return r.query_params


@subrouter.delete("/:name")
async def subrouter_delete(r: Request):
    """Delete Name by ID"""
    return r.path_params


app.include_router(subrouter)

Other Specification Params

We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example using request & response bodies below:

Request & Response Body

from robyn.types import JSONResponse, Body

class Initial(Body):
    is_present: bool
    letter: Optional[str]


class FullName(Body):
    first: str
    second: str
    initial: Initial


class CreateItemBody(Body):
    name: FullName
    description: str
    price: float
    tax: float


class CreateResponse(JSONResponse):
    success: bool
    items_changed: int


@app.post("/")
def create_item(request: Request, body: CreateItemBody) -> CreateResponse:
    return CreateResponse(success=True, items_changed=2)

With the reference documentation deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City.

Using Pydantic Models

If you have Pydantic installed (pip install "robyn[pydantic]" or pip install "robyn[all]"), you can use Pydantic BaseModel classes directly as handler parameter annotations. Robyn will automatically validate the request body and generate a rich OpenAPI schema — including property types, required fields, defaults, and $ref for nested models.

Pydantic + OpenAPI

from pydantic import BaseModel

class UserCreate(BaseModel):
    name: str
    email: str
    age: int
    active: bool = True

@app.post("/users", openapi_tags=["Users"])
def create_user(user: UserCreate) -> dict:
    """Create a new user"""
    return {"name": user.name}

For the full guide on Pydantic validation, nested models, error responses, and OpenAPI integration, see the dedicated Pydantic Integration page.

Documenting routes (status codes, deprecation, extra responses)

Every route decorator (@app.get, @app.post, … and the SubRouter equivalents) accepts a set of flags so you can document — and in some cases change the runtime behaviour of — a route directly from the decorator:

  • status_code — the default success status code. It is reflected in the spec (e.g. 201 instead of 200) and applied at runtime to plain returns (dict/list/str/bytes). An explicit Response(...) return always keeps its own status.
  • response_model — a type (typically a Pydantic model) used as the success response schema. When the handler returns a dict, it is validated and re-serialized through the model so the wire response matches the docs.
  • responses — additional documented responses keyed by status code. Each value may be a plain description string, a {"description": ..., "model": SomeType} dict, or a full OpenAPI response object.
  • deprecated=True — marks the operation deprecated (rendered with strikethrough in Swagger UI).
  • include_in_schema=False — hides the route from the spec entirely (useful for health checks and internal endpoints).

Route documentation flags

from robyn.types import JSONResponse


class UserResponse(JSONResponse):
    id: int
    name: str


class ErrorResponse(JSONResponse):
    message: str


@app.post(
    "/users",
    status_code=201,
    response_model=UserResponse,
    responses={
        404: "User not found",
        422: {"description": "Validation error", "model": ErrorResponse},
    },
    openapi_tags=["Users"],
)
def create_user(body: UserResponse) -> UserResponse:
    """Create a user (responds with 201)"""
    return {"id": 1, "name": "Bruce"}


@app.get("/legacy", deprecated=True)
def legacy():
    """Old endpoint kept for backwards-compat"""
    return "use /users instead"


@app.get("/healthz", include_in_schema=False)
def healthz():
    return "ok"

Authentication & the Swagger "Authorize" button

When you call app.configure_authentication(...), Robyn automatically registers a matching security scheme (BearerAuth for a BearerGetter) so Swagger UI's Authorize button works out of the box. Routes declared with auth_required=True advertise that requirement in the spec, so they render with a lock icon and send the credential when you try them out.

You can also declare schemes explicitly via Components(securitySchemes=...):

Security schemes

from robyn.openapi import OpenAPI, OpenAPIInfo, Components

app = Robyn(
    __file__,
    openapi=OpenAPI(
        info=OpenAPIInfo(
            components=Components(
                securitySchemes={
                    "BearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
                }
            )
        )
    ),
)


@app.get("/me", auth_required=True)
def me(request):
    """Requires a Bearer token — shows a lock in Swagger UI"""
    return "secret"

If no security scheme is configured, Robyn no longer emits an empty securitySchemes object, so the Authorize button stays hidden instead of opening an empty popup.

A note on auto-generated parameters

Robyn builds the OpenAPI spec by inspecting your handler's type annotations — not its body. Reading values dynamically via request.query_params, request.json(), or request.path_params produces working endpoints, but Robyn has nothing to introspect, so those parameters won't appear in /docs.

To document them, annotate the handler with typed params instead:

  • Query params: a subclass of QueryParams (e.g. query_params: MyQueryParams)
  • Request body: a subclass of Body/JsonBody, a TypedDict, or a Pydantic BaseModel
  • Path params: derived automatically from the route string (/users/:id)

Stdlib return/field types such as datetime, date, UUID, Decimal, Enum, and Literal are mapped to their proper JSON Schema type/format, and container types like list[str] and Optional[str] render correctly.

What's next?

Batman wondered about whether Robyn handlers can be dispatched to multiple processes.

Robyn showed him the way!

Multiprocess Execution