Source files:
core/sillo/openapi/config.py,OpenAPIConfigcore/sillo/openapi/models.py: Pydantic OpenAPI 3.0 models (OpenAPI,Info,PathItem,Operation,Schema,Components, etc.)core/sillo/openapi/_builder.py,APIDocumentationcore/sillo/openapi/ui.py:DocsUI,Atlas,Swagger,ReDoc,Scalar,default_docscore/sillo/openapi/utils.py,get_openapi(route flattening utility)core/sillo/openapi/__init__.py. Public re-exports
14.1 Architecture Overview
Section titled “14.1 Architecture Overview”Sillo’s OpenAPI system generates a complete OpenAPI 3.0 specification from route declarations and validated parameter models. The document is built once after all routes are registered and served as a static JSON file. Documentation UIs (Atlas, Swagger, ReDoc, Scalar) render it client-side.
flowchart LR
subgraph Registration["Route Registration"]
R["Route objects<br/>(handlers + markers + models)"]
end
subgraph Build["Document Build (once)"]
AD["APIDocumentation.get_openapi()"]
AD --> CR["_collect_routes_with_paths()"]
CR --> AR["_add_route_to_openapi_spec()"]
AR --> PR["_build_parameters_spec()"]
AR --> RB["_build_request_body_spec()"]
AR --> RS["_build_responses_spec()"]
AR --> SEC["_route_security()"]
PR & RB & RS --> OD["OpenAPI spec dict"]
end
subgraph Serve["Serving"]
OD --> JSON["/openapi.json"]
JSON --> Atlas["Atlas /docs"]
JSON --> Swagger["Swagger /docs"]
JSON --> ReDoc["ReDoc /redoc"]
JSON --> Scalar["Scalar /reference"]
end
R --> AD
14.2 OpenAPIConfig
Section titled “14.2 OpenAPIConfig”# core/sillo/openapi/config.py:21-163class OpenAPIConfig: def __init__( self, title: str = "API Documentation", version: str = "1.0.0", description: str = "", servers: list[Server] | None = [], contact: Contact | None = None, license: License | None = None, termsOfService: str | None = None, openapi_version: str = "3.0.0", ):OpenAPIConfig holds the OpenAPI document and provides methods to modify
its components section. It is the single source of truth for the spec.
14.2.1 Constructor Fields
Section titled “14.2.1 Constructor Fields”| Field | Type | Default | Description |
|---|---|---|---|
title | str | "API Documentation" | API title in the info block. |
version | str | "1.0.0" | API version. |
description | str | "" | API description. |
servers | list[Server] | [] | Server entries. |
contact | Contact | None | Contact info. |
license | License | None | License info. |
termsOfService | str | None | Terms of service URL. |
openapi_version | str | "3.0.0" | OpenAPI spec version. |
14.2.2 Internal State
Section titled “14.2.2 Internal State”The config creates an OpenAPI Pydantic model in __init__:
# config.py:36-49self.openapi_spec = OpenAPI( openapi=openapi_version, info=Info( title=title, version=version, description=description, contact=contact, license=license, termsOfService=termsOfService, ), paths={}, servers=servers, components=Components(),)14.2.3 Component Registration Methods
Section titled “14.2.3 Component Registration Methods”add_security_scheme(name, scheme)
Section titled “add_security_scheme(name, scheme)”# config.py:69-77def add_security_scheme(self, name: str, scheme: SecurityScheme): if not self.openapi_spec.components: self.openapi_spec.components = Components() if not self.openapi_spec.components.securitySchemes: self.openapi_spec.components.securitySchemes = {} self.openapi_spec.components.securitySchemes[name] = schemeRegisters a security scheme (API key, HTTP bearer, OAuth2, OpenID Connect).
add_schema(name, schema)
Section titled “add_schema(name, schema)”# config.py:79-92def add_schema(self, name: str, schema: type[BaseModel] | Schema): if isinstance(schema, type) and issubclass(schema, BaseModel): self.openapi_spec.components.schemas[name] = Schema(**schema.model_json_schema()) else: self.openapi_spec.components.schemas[name] = schemaAccepts either a Pydantic BaseModel subclass (auto-converted via
model_json_schema()) or a raw Schema object.
add_parameter(name, parameter)
Section titled “add_parameter(name, parameter)”# config.py:94-102def add_parameter(self, name: str, parameter: Parameter): ... self.openapi_spec.components.parameters[name] = parameterRegisters a reusable parameter component.
add_response(name, response)
Section titled “add_response(name, response)”# config.py:104-112def add_response(self, name: str, response: OpenAPIResponse): ... self.openapi_spec.components.responses[name] = responseRegisters a reusable response component.
add_example(name, example)
Section titled “add_example(name, example)”# config.py:114-122def add_example(self, name: str, example: Example): ... self.openapi_spec.components.examples[name] = exampleRegisters an example component.
add_tag(tag)
Section titled “add_tag(tag)”# config.py:124-132def add_tag(self, tag: Tag): if not self.openapi_spec.tags: self.openapi_spec.tags = [] existing_tags = [t.name for t in self.openapi_spec.tags] if tag.name not in existing_tags: self.openapi_spec.tags.append(tag)Adds a tag, deduplicating by name.
14.2.4 Additional Methods
Section titled “14.2.4 Additional Methods”| Method | Description |
|---|---|
add_server(server) | Add a server entry. |
set_external_docs(docs) | Set external documentation link. |
set_global_security(sec) | Set global security requirements. |
get_schema_ref(name) | Returns #/components/schemas/{name}. |
get_parameter_ref(name) | Returns #/components/parameters/{name}. |
get_response_ref(name) | Returns #/components/responses/{name}. |
get_example_ref(name) | Returns #/components/examples/{name}. |
14.2.5 Security Schemes Property
Section titled “14.2.5 Security Schemes Property”# config.py:51-67@propertydef security_schemes(self) -> dict[str, SecurityScheme | Reference]: components = self.openapi_spec.components if components is None or not components.securitySchemes: return {} return components.securitySchemesReads directly from the document. This used to be a separate dict that was never written to, causing the application to report no security schemes.
14.3 Pydantic OpenAPI 3.0 Models
Section titled “14.3 Pydantic OpenAPI 3.0 Models”All OpenAPI structures are modeled as Pydantic BaseModel subclasses in
core/sillo/openapi/models.py (494 lines). They provide validation, JSON
serialization, and $ref support.
14.3.1 Document Root
Section titled “14.3.1 Document Root”# models.py:478-489class OpenAPI(BaseModel): openapi: str info: Info paths: Annotated[dict[str, PathItem | Extension], Field(default_factory=dict)] servers: list[Server] | None = None components: Components = Components() security: list[dict[str, list[str]]] | None = None tags: list[Tag] | None = None externalDocs: ExternalDocumentation | None = None14.3.2 Info Block
Section titled “14.3.2 Info Block”# models.py:44-54class Info(BaseModel): title: str version: str description: str | None = None termsOfService: str | None = None contact: Contact | None = None license: License | None = None model_config = ConfigDict(extra="allow")extra="allow" permits vendor extensions (x-* fields).
14.3.3 Path and Operation
Section titled “14.3.3 Path and Operation”# models.py:339-356class PathItem(BaseModel): ref: Annotated[str | None, Field(alias="$ref")] = None summary: str | None = None description: str | None = None get: Operation | None = None put: Operation | None = None post: Operation | None = None delete: Operation | None = None options: Operation | None = None head: Operation | None = None patch: Operation | None = None trace: Operation | None = None servers: list[Server] | None = None parameters: list[Parameter | Reference] | None = None model_config = ConfigDict(extra="allow")# models.py:316-333class Operation(BaseModel): responses: dict[str, Response | Reference] tags: list[str] | None = None summary: str | None = None description: str | None = None externalDocs: ExternalDocumentation | None = None operationId: str | None = None parameters: list[ConcreteParameter | Reference] | None = None requestBody: RequestBody | Reference | None = None callbacks: dict[str, dict[str, PathItem] | Reference] | None = None deprecated: bool | None = None security: list[dict[str, list[str]]] | None = None servers: list[Server] | None = None model_config = ConfigDict(extra="allow")14.3.4 Schema
Section titled “14.3.4 Schema”# models.py:106-171class Schema(BaseModel): ref: Annotated[str | None, Field(alias="$ref")] = None title: str | None = None multipleOf: float | None = None maximum: float | None = None exclusiveMaximum: float | None = None minimum: float | None = None exclusiveMinimum: float | None = None maxLength: Annotated[int | None, Field(ge=0)] = None minLength: Annotated[int | None, Field(ge=0)] = None pattern: str | None = None # ... (full JSON Schema vocabulary) type: str | None = None allOf: list[Schema] | None = None oneOf: list[Schema] | None = None anyOf: list[Schema] | None = None properties: dict[str, Schema] | None = None # ...The validate_type field validator handles composition keywords. When
anyOf/oneOf/allOf are present, type defaults to None instead of
"object":
# models.py:155-171@field_validator("type", mode="before")@classmethoddef validate_type(cls, v, info): if v is not None: return v data = info.data if hasattr(info, "data") else {} has_composition = any( data.get(key) is not None for key in ["anyOf", "oneOf", "allOf"] ) if not has_composition: return "object" return None14.3.5 Parameter Models
Section titled “14.3.5 Parameter Models”# models.py:219-270class ConcreteParameter(ParameterBase): name: str in_: ParameterLocations = Field(alias="in")
class Header(ConcreteParameter): in_: Literal["header"] = Field(default="header", serialization_alias="in") style: HeaderParamStyles = "simple" explode: bool = False spec: ... = Schema(type="string")
class Query(ConcreteParameter): in_: Literal["query"] = Field(default="query", serialization_alias="in") style: QueryParamStyles = "form" explode: bool = True spec: ... = Schema(type="string")
class Path(ConcreteParameter): in_: Literal["path"] = Field(default="path", alias="in") style: PathParamStyles = "simple" explode: bool = False required: Literal[True] = True
class Cookie(ConcreteParameter): in_: Literal["cookie"] = "cookie" style: CookieParamStyles = "form" explode: bool = True
Parameter = Union[Query, Header, Cookie, Path]Each parameter type has appropriate defaults for its location (style, explode, required).
14.3.6 Security Scheme Models
Section titled “14.3.6 Security Scheme Models”# models.py:362-453SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]| Model | Type | Key Fields |
|---|---|---|
APIKey | "apiKey" | name, in_ (query/header/cookie) |
HTTPBase | "http" | scheme |
HTTPBearer | "http" | scheme="bearer", bearerFormat |
OAuth2 | "oauth2" | flows (implicit/password/clientCredentials/authorizationCode) |
OpenIdConnect | "openIdConnect" | openIdConnectUrl |
14.3.7 Components
Section titled “14.3.7 Components”# models.py:456-467class Components(BaseModel): schemas: dict[str, Schema | Reference] | None = None responses: dict[str, Response | Reference] | None = None parameters: dict[str, Parameter | Reference] | None = None examples: Examples | None = None requestBodies: dict[str, RequestBody | Reference] | None = None headers: dict[str, Header | Reference] | None = None securitySchemes: dict[str, SecurityScheme | Reference] | None = None links: dict[str, Link | Reference] | None = None callbacks: dict[str, dict[str, PathItem] | Reference] | None = None14.3.8 Model Rebuilds
Section titled “14.3.8 Model Rebuilds”# models.py:492-494Schema.model_rebuild()Operation.model_rebuild()Encoding.model_rebuild()These calls resolve forward references after all models are defined. Without them, self-referential models (Schema → Schema, Operation → PathItem) would fail validation.
14.4 APIDocumentation
Section titled “14.4 APIDocumentation”# core/sillo/openapi/_builder.py:39-798class APIDocumentation: def __init__( self, config: OpenAPIConfig | None = None, swagger_url: str = "/docs", redoc_url: str = "/redoc", openapi_url: str = "/openapi.json", ):APIDocumentation is the document builder. It walks all registered routes,
generates OpenAPI operations, and produces the final spec dictionary.
14.4.1 get_openapi()
Section titled “14.4.1 get_openapi()”# _builder.py:92-125def get_openapi( self, route: Route | Router | Group | Any, current_prefix: str = "") -> dict[str, Any]: self._validator_memo = {} routes_with_paths = self._collect_routes_with_paths(route, current_prefix) for full_path, route_obj in routes_with_paths: if isinstance(route_obj, Route) and not getattr( route_obj, "exclude_from_schema", False ): self._add_route_to_openapi_spec(full_path, route_obj) spec = self.config.openapi_spec.model_dump( by_alias=True, exclude_none=True, mode="json" ) self._validator_memo = {} return specFlow:
flowchart TD
A["get_openapi(root_route)"] --> B["_collect_routes_with_paths(root, '')"]
B --> C["List of (full_path, Route)"]
C --> D{"For each route:"}
D -->|"exclude_from_schema"| E["Skip"]
D -->|"Normal"| F["_add_route_to_openapi_spec(path, route)"]
F --> G["Build Operation"]
G --> H["Add to paths dict"]
H --> I["model_dump(by_alias, exclude_none, mode='json')"]
I --> J["Return spec dict"]
The mode="json" parameter is critical: without it, Pydantic’s rich types
(AnyUrl, datetime) would appear as Python objects that json.dumps refuses
to serialize.
14.4.2 _collect_routes_with_paths()
Section titled “14.4.2 _collect_routes_with_paths()”# _builder.py:127-183def _collect_routes_with_paths( self, route: Route | Router | Group | Any, current_prefix: str = "") -> list[tuple[str, Route]]:Recursively flattens the route hierarchy into (full_path, Route) pairs.
Handles three container types:
flowchart TD
A["_collect_routes_with_paths(route, prefix)"] --> B{"Route type?"}
B -->|"Route"| C["Return [(prefix + raw_path, route)]"]
B -->|"Router"| D["Add router prefix<br/>Recurse into sub-routes"]
B -->|"Group"| E["Add group path<br/>Recurse into _base_app or routes"]
B -->|"Other"| F["Recurse into .routes"]
Prefix handling: The method avoids double-counting prefixes when a router
is mounted via a Group:
# _builder.py:146-148if router_prefix and current_prefix.endswith(router_prefix): new_prefix = current_prefix # Don't add prefix againelse: new_prefix = self._normalize_path(current_prefix + router_prefix)14.4.3 _add_route_to_openapi_spec()
Section titled “14.4.3 _add_route_to_openapi_spec()”# _builder.py:235-274def _add_route_to_openapi_spec(self, full_path: str, route: Route) -> None: openapi_path = self._convert_path_to_openapi_format(full_path) for method in sorted(route.methods): request_body_spec = self._build_request_body_spec(route, method) responses_spec = self._build_responses_spec(route) parameters = self._build_parameters_spec(route)
operation = Operation( summary=route.summary or f"{method.upper()} {openapi_path}", description=route.description, responses=responses_spec, tags=route.tags or [], parameters=parameters, requestBody=request_body_spec, security=self._route_security(route), operationId=route.operation_id or ..., deprecated=route.deprecated, externalDocs=getattr(route, "external_docs", None), )
if openapi_path not in self.config.openapi_spec.paths: self.config.openapi_spec.paths[openapi_path] = PathItem() setattr( self.config.openapi_spec.paths[openapi_path], method.lower(), operation )For each HTTP method on the route, it builds:
- Parameters: path, query, header, cookie
- Request body: JSON or form
- Responses: success model + error responses
- Security: from route declaration or auth gate
14.4.4 _build_request_body_spec()
Section titled “14.4.4 _build_request_body_spec()”# _builder.py:283-341def _build_request_body_spec(self, route: Route, method: str) -> RequestBody | None:Request bodies come from two sources:
flowchart TD
A["_build_request_body_spec(route, method)"] --> B["_build_marker_body_spec(route)"]
B -->|"Found form/file markers"| C["Return form RequestBody"]
B -->|"None"| D{"route.request_model?"}
D -->|"BaseModel"| E["model_json_schema() → RequestBody"]
D -->|"dict"| F["Extract first model from dict"]
D -->|"None"| G{"Method has body?"}
G -->|"POST/PUT/PATCH"| H["Default JSON body"]
G -->|"GET/DELETE/HEAD"| I["None"]
_build_marker_body_spec()
Section titled “_build_marker_body_spec()”# _builder.py:343-384def _build_marker_body_spec(self, route: Route) -> RequestBody | None: for validator in self._collect_validators(route): if validator.form_spec is None: continue spec = validator.form_spec raw = spec.model.model_json_schema(by_alias=True, ref_template="#/$defs/{model}") schema_dict = self._extract_and_add_nested_schemas(raw) properties = dict(schema_dict.get("properties") or {}) for alias in spec.passthrough.values(): properties[alias] = {"type": "string", "format": "binary"} schema_dict["properties"] = properties content_type = ( "multipart/form-data" if spec.passthrough else "application/x-www-form-urlencoded" ) return RequestBody( required=True, content={content_type: MediaType(spec=Schema(**schema_dict))}, ) return NoneFile markers (passthrough) get {"type": "string", "format": "binary"} in
the schema. The content type is multipart/form-data when files are present,
application/x-www-form-urlencoded otherwise.
14.4.5 _extract_and_add_nested_schemas()
Section titled “14.4.5 _extract_and_add_nested_schemas()”# _builder.py:439-463def _extract_and_add_nested_schemas(self, schema: dict[str, Any]) -> dict[str, Any]: cleaned_schema = copy.deepcopy(schema) nested = cleaned_schema.pop("$defs", None) if nested: for def_name, def_schema in nested.items(): processed_schema = self._extract_and_add_nested_schemas(def_schema) self.config.add_schema(def_name, Schema(**processed_schema)) self._update_schema_references(cleaned_schema) return cleaned_schemaPydantic puts nested model definitions under $defs. This method:
- Deep copies the schema (the caller’s copy is not mutated)
- Extracts
$defsand registers each as acomponents.schemasentry - Recursively processes nested
$defs(models referencing models) - Rewrites
$refpointers from#/$defs/Xto#/components/schemas/X
14.4.6 _update_schema_references()
Section titled “14.4.6 _update_schema_references()”# _builder.py:465-497def _update_schema_references(self, schema: Any) -> None: if isinstance(schema, dict): # Handle discriminator mappings discriminator = schema.get("discriminator") if isinstance(discriminator, dict): mapping = discriminator.get("mapping") if isinstance(mapping, dict): for name, target in mapping.items(): if isinstance(target, str) and target.startswith("#/$defs/"): mapping[name] = target.replace("#/$defs/", "#/components/schemas/")
for key, value in schema.items(): if key == "$ref" and isinstance(value, str) and value.startswith("#/$defs/"): schema[key] = value.replace("#/$defs/", "#/components/schemas/") else: self._update_schema_references(value) elif isinstance(schema, list): for item in schema: self._update_schema_references(item)This recursive rewriter handles:
$refvalues in any position- Discriminator
mappingvalues (plain strings, not$refobjects) - Nested dicts and lists at any depth
14.4.7 _build_parameters_spec()
Section titled “14.4.7 _build_parameters_spec()”# _builder.py:609-660def _build_parameters_spec(self, route: Route) -> list[Parameter]: parameters = [] documented: set = set()
# Path parameters from compiled validators path_schemas: dict[str, Schema] = {} for validator in self._collect_validators(route): for spec in validator.specs: if spec.location is not ParameterLocation.PATH: continue for name, schema in self._schemas_for_spec(spec).items(): path_schemas[name] = schema
# Generic path parameters (from route pattern) for param_name in route.param_names: parameters.append(OpenAPIPath( name=param_name, required=True, spec=path_schemas.get(param_name, Schema(type="string")), )) documented.add(("path", param_name))
# Legacy parameters if hasattr(route, "resolved_params") and route.resolved_params: for param_dep in route.resolved_params: openapi_param = self._convert_param_dependency(param_dep) if openapi_param: parameters.append(openapi_param) documented.add((openapi_param.in_, openapi_param.name))
# Validated parameters from compiled models for validator in self._collect_validators(route): for spec in validator.specs: if spec.location is ParameterLocation.PATH: continue parameters.extend(self._convert_location_spec(spec, documented))
return parametersParameters come from three sources, merged with deduplication:
- Compiled validators. Pydantic models with real schemas (constraints, types)
- Legacy extractors: schema inferred from default values
- Route pattern: path parameters from the URL pattern itself
14.4.8 _route_security()
Section titled “14.4.8 _route_security()”# _builder.py:185-214def _route_security(self, route: Any) -> Any: if route.security is not None: return route.security
gate = getattr(route, "auth", None) derive = getattr(gate, "security_requirements", None) if not callable(derive): return None
return derive(available=list(self.config.security_schemes))Security requirements come from:
- Explicit
security=on the route - Auth gate’s
security_requirements(): gates likeuseAuth()that reject anonymous callers without naming a scheme get filled in with all registered schemes
14.4.9 _collect_validators()
Section titled “14.4.9 _collect_validators()”# _builder.py:572-607def _collect_validators(self, route: Route) -> list[Any]: key = id(route) cached = self._validator_memo.get(key) if cached is not None: return cached
validators = [] dependants = [getattr(route, "dependant", None)] dependants.extend(getattr(route, "_router_dependants", []) or [])
for dependant in dependants: if dependant is None: continue for _, validator in getattr(dependant, "_validator_plan", ()): validators.append(validator)
self._validator_memo[key] = validators return validatorsCollects every CompiledValidator reachable from a route: including those on
nested dependencies. Results are memoized per build since the parameter,
request-body, and response sections all need the same list.
14.4.10 Response Building: _build_responses_spec()
Section titled “14.4.10 Response Building: _build_responses_spec()”# _builder.py:386-437def _build_responses_spec(self, route: Route) -> dict[str, OpenAPIResponse | Reference]:Response specs are built from (in priority order):
response_model: takes the 200 slot;response_model_manywraps it inlist[]responsesdict: explicit status-code-to-model mapping- Default: generic 200 with example object
# For BaseModel response modelsschema_dict = model.model_json_schema()processed_schema = self._extract_and_add_nested_schemas(schema_dict)example = model.model_validate({}).model_dump(exclude_none=True)14.4.11 Schema Inference from Defaults
Section titled “14.4.11 Schema Inference from Defaults”# _builder.py:764-798def _infer_schema_from_default(self, default: Any) -> Schema: if default is ... or default is None: return Schema(type="string")
type_map = {int: "integer", float: "number", bool: "boolean", str: "string"} type_default = type(default) if type_default in type_map: schema = Schema(type=type_map[type_default]) if default is not None: schema.default = default if type_default is float: schema.format = "float" return schema
if isinstance(default, list): return Schema(type="array", items=Schema(type="string"))
return Schema(type="string")Legacy parameters (no type=, no constraints) get their schema inferred from
the default value’s runtime type. This is the same heuristic used by
_convert() for coercion.
14.5 Documentation UIs
Section titled “14.5 Documentation UIs”14.5.1 DocsUI Base Class
Section titled “14.5.1 DocsUI Base Class”# core/sillo/openapi/ui.py:75-151class DocsUI: path: str = "/docs" name: str = "docs"
def __init__( self, *, path: str | None = None, title: str | None = None, favicon_url: str | None = None, ) -> None:| Attribute | Type | Description |
|---|---|---|
path | str | Where the page is served. Must begin with /. |
name | str | Short identifier for error messages and lookup. |
title | str | None | Browser tab title override. |
favicon_url | str | None | Icon URL. |
render(ctx) Method
Section titled “render(ctx) Method”def render(self, ctx: DocsContext) -> str: raise NotImplementedErrorSubclasses return a complete HTML document as a string. The DocsContext
provides openapi_url (already mount-aware), title, version,
description, and the full OpenAPIConfig.
14.5.2 DocsContext
Section titled “14.5.2 DocsContext”# ui.py:53-72@dataclass(frozen=True)class DocsContext: openapi_url: str title: str version: str description: str config: OpenAPIConfigA frozen dataclass passed to render(). The openapi_url is already prefixed
with the request’s root_path, so pages work correctly when the application
is mounted under a prefix.
14.5.3 Atlas
Section titled “14.5.3 Atlas”# ui.py:170-242class Atlas(DocsUI): path = "/docs" name = "atlas"
def __init__( self, *, path: str | None = None, title: str | None = None, favicon_url: str | None = DEFAULT_FAVICON, js_url: str = ATLAS_JS, theme: str = "auto", ui_config: dict[str, Any] | None = None, ) -> None:Atlas is sillo’s own OpenAPI reference viewer: a three-pane reference with a request builder, ranked search, and snippets in nine languages.
Key features:
- Zero dependencies: one script tag
- Carries its own styles
- Pinned CDN version (
v0.8.0) for reproducibility ui_configmerged intocreateApiReference()call
Render output: Single HTML page with <div id="app"> and
Atlas.createApiReference('#app', options).
14.5.4 Swagger
Section titled “14.5.4 Swagger”# ui.py:245-317class Swagger(DocsUI): path = "/docs" name = "swagger"
def __init__( self, *, path: str | None = None, js_url: str = SWAGGER_JS, css_url: str = SWAGGER_CSS, ui_config: dict[str, Any] | None = None, ... ) -> None:Standard Swagger UI. ui_config is passed to SwaggerUIBundle.
Render output: HTML with swagger-ui-bundle.js, swagger-ui.css, and
SwaggerUIBundle(options) on window.onload.
14.5.5 ReDoc
Section titled “14.5.5 ReDoc”# ui.py:320-376class ReDoc(DocsUI): path = "/redoc" name = "redoc"
def __init__( self, *, js_url: str = REDOC_JS, ui_config: dict[str, Any] | None = None, ... ) -> None:ReDoc. ui_config is passed to Redoc.init().
Render output: HTML with redoc.standalone.js and
Redoc.init(url, options, element).
14.5.6 Scalar
Section titled “14.5.6 Scalar”# ui.py:379-446class Scalar(DocsUI): path = "/reference" name = "scalar"
def __init__( self, *, js_url: str = SCALAR_JS, theme: str = "default", ui_config: dict[str, Any] | None = None, ... ) -> None:Scalar API Reference. Uses Scalar.createApiReference() (the current API;
older <script id="api-reference"> forms are not supported).
14.5.7 default_docs()
Section titled “14.5.7 default_docs()”# ui.py:449-467def default_docs(swagger_url: str = "/docs", redoc_url: str = "/redoc") -> list[DocsUI]: return [Atlas(path=swagger_url), ReDoc(path=redoc_url)]The presenters mounted when docs is not given. Returns a fresh list (callers
mutate their own copy).
14.6 CDNs and Asset URLs
Section titled “14.6 CDNs and Asset URLs”# ui.py:153-167DEFAULT_FAVICON = "https://docs.sillo.build/favicon.svg"ATLAS_VERSION = "v0.8.0"ATLAS_JS = f"https://cdn.jsdelivr.net/gh/sillohq/atlas@{ATLAS_VERSION}/dist/atlas.standalone.js"SWAGGER_JS = "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"SWAGGER_CSS = "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"REDOC_JS = "https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"SCALAR_JS = "https://cdn.jsdelivr.net/npm/@scalar/api-reference"Atlas is pinned to a specific tag. Other viewers use major-version pins or
latest. Override js_url / css_url to self-host for environments with
no outbound network or strict CSP.
14.7 Document Serialization
Section titled “14.7 Document Serialization”# _builder.py:121-123spec = self.config.openapi_spec.model_dump( by_alias=True, exclude_none=True, mode="json")Three flags matter:
| Flag | Purpose |
|---|---|
by_alias=True | Serialize $ref as $ref (not ref), in as in (not in_). |
exclude_none=True | Omit optional fields not set: keeps the spec clean. |
mode="json" | Convert rich types (AnyUrl, datetime) to JSON-native values. Without this, json.dumps fails on AnyUrl objects. |
14.8 Path Normalization
Section titled “14.8 Path Normalization”# _builder.py:216-233def _normalize_path(self, path: str) -> str: if not path: return "/" if not path.startswith("/"): path = "/" + path path = re.sub(r"/+", "/", path) if len(path) > 1 and path.endswith("/"): path = path.rstrip("/") return path14.8.1 Path Format Conversion
Section titled “14.8.1 Path Format Conversion”# _builder.py:276-281def _convert_path_to_openapi_format(self, path: str) -> str: return re.sub(r"\{(\w+):[^}]+\}", r"{\1}", path)Sillo’s path format (/users/{id:int}) is converted to OpenAPI format
(/users/{id}). The type constraint in the URL pattern is stripped; validation
is handled by Path markers instead.
14.9 get_openapi() Utility
Section titled “14.9 get_openapi() Utility”# core/sillo/openapi/utils.py:7-47def get_openapi(route: Route | Router | Group | Any) -> list[Route]: routes_list: list[Route] = [] if isinstance(route, Route): return [route] if isinstance(route, Router): for sub_route in route.routes: routes_list.extend(get_openapi(sub_route)) return routes_list if isinstance(route, Group): if hasattr(route, "_base_app") and isinstance(route._base_app, Router): routes_list.extend(get_openapi(route._base_app)) elif hasattr(route, "routes"): for sub_route in route.routes: routes_list.extend(get_openapi(sub_route)) return routes_list if hasattr(route, "routes"): for sub_route in route.routes: routes_list.extend(get_openapi(sub_route)) return routes_listA simpler utility that flattens the route hierarchy into a flat list of
Route objects without building a document. Used for inspection and testing.
14.10 Complete Build Flow
Section titled “14.10 Complete Build Flow”sequenceDiagram
participant App as SilloApp
participant AD as APIDocumentation
participant Config as OpenAPIConfig
participant Builder as Route Builder
participant Spec as OpenAPI Model
App->>AD: get_openapi(root_router)
AD->>AD: _collect_routes_with_paths(root, "")
AD-->>AD: [(full_path, Route), ...]
loop For each route
AD->>AD: _add_route_to_openapi_spec(path, route)
AD->>AD: _build_parameters_spec(route)
Note over AD: Collect validators, extract schemas
AD->>AD: _build_request_body_spec(route, method)
Note over AD: JSON body or form body
AD->>AD: _build_responses_spec(route)
Note over AD: response_model or responses dict
AD->>AD: _route_security(route)
Note over AD: Explicit or from auth gate
AD->>AD: Create Operation object
AD->>Config: paths[path].method = operation
end
AD->>Spec: model_dump(by_alias, exclude_none, mode="json")
Spec-->>AD: dict[str, Any]
AD-->>App: OpenAPI spec dict
App->>App: Serve at /openapi.json
App->>App: Mount DocsUI presenters
14.11 Integration with Validation
Section titled “14.11 Integration with Validation”The OpenAPI system reads from the same CompiledValidator objects used for
runtime validation. This is the mechanism that keeps documentation and
enforcement in sync:
# _builder.py:662-681def _schemas_for_spec(self, spec): raw = spec.model.model_json_schema(by_alias=True, ref_template="#/$defs/{model}") processed = self._extract_and_add_nested_schemas(raw) return { name: Schema(**prop) for name, prop in (processed.get("properties") or {}).items() }The model_json_schema() call produces the same JSON Schema that Pydantic
uses internally for validation. Constraints declared on markers (gt, ge,
min_length, pattern) appear in both the documented schema and the runtime
validator because they come from the same FieldInfo objects.
14.12 Performance Characteristics
Section titled “14.12 Performance Characteristics”| Operation | When | Cost |
|---|---|---|
get_openapi() | Once (startup) | O(routes × models) |
_collect_routes_with_paths | Once | O(route tree depth) |
_add_route_to_openapi_spec | Once per route | O(validators) |
_extract_and_add_nested_schemas | Once per model | O($defs depth) |
model_dump() | Once | O(total spec size) |
Serving /openapi.json | Per request | O(1): static dict |
The document is built once and served as a pre-serialized dict. No per-request computation is needed.