OpenAPI plugin
Swagger UI + machine-readable JSON spec for your REST endpoints, auto-generated from the model registry.
umbral-openapi walks the model registry and serves a live Swagger UI and a
machine-readable JSON spec. Add it to any app that already uses
umbral-rest - the OpenAPI plugin depends on REST's
routes and will not compile without it.
Quickstart
use umbral::prelude::*;use umbral_rest::RestPlugin;use umbral_openapi::OpenApiPlugin; App::builder() .settings(settings) .database("default", pool) .plugin(RestPlugin::default()) .plugin(OpenApiPlugin::default()) .build()?;With defaults the plugin registers two endpoints:
| URL | What you get |
|---|---|
/openapi and /openapi/ | Swagger UI (both paths serve the same UI) |
/openapi/openapi.json | Machine-readable spec |
Customising the spec
Chain builder methods before passing the plugin to .plugin():
OpenApiPlugin::default() .at("/api/docs") // move the mount base; /api/docs and /api/docs/ both work .title("Acme API") // info.title in the spec .version("1.2.0") // info.version .description("Markdown is fine here.\n\nShown above the operations list.").at() is the only method that affects URLs. The Swagger UI and JSON spec
always live at <base>/ and <base>/openapi.json relative to whatever base
you supply.
Excluding models
By default the plugin exposes every model that umbral-rest exposes.
The framework's internal tables (auth_user, session, umbral_migrations,
the permissions_* tables, task_row, admin_audit_log) sit on umbral-rest's
default block-list, so they stay out of the spec unless you opt them back in
on RestPlugin.
To block additional models from the spec only:
OpenApiPlugin::default() .exclude(["api_keys", "audit_log"])Each string is matched against the model's table name. Excluded models produce no schema object and no operations in the spec.
Column metadata in the schema
Beyond type + format, every column's schema carries the metadata the migration engine already knew about - surfaced so Swagger UI, generated clients, and the umbral-playground can build richer affordances without re-walking the model registry.
| OpenAPI key | Source on the column |
|---|---|
enum | #[umbral(choices)] - closed-set columns get their valid values inline. (Skipped for is_multichoice columns whose wire value is a CSV.) |
maxLength | #[umbral(max_length = N)] |
default | #[umbral(default = "...")] |
minimum / maximum | #[umbral(min = N)] / #[umbral(max = N)] |
description | #[umbral(help = "...")] |
example | #[umbral(example = "...")] |
readOnly | #[umbral(noform)] - the field is never accepted in any request body (POST, PUT, or PATCH); the server fills it in |
nullable | Option<T> on the model field |
#[umbral(noedit)] is not mapped to readOnly - noedit is an admin edit-form
hint (the field stays writable on create), so mapping it to OpenAPI readOnly
(which means "never accepted in any body") would wrongly hide required fields from
client autofill on POST. It surfaces instead as the x-umbral-noedit vendor extension below.
Vendor extensions
OpenAPI 3.0 reserves x-* keys for tool-specific data. The plugin emits these for affordances no standard OpenAPI key captures:
| Extension | Meaning |
|---|---|
x-umbral-choice-labels | Human labels paired position-for-position with enum (from ChoiceField). |
x-umbral-multichoice + x-umbral-choices | Flag for multichoice columns whose wire value is a CSV of the choice set. |
x-umbral-fk-target | Name of the table a foreign-key column points at. |
x-umbral-fk-ref | JSON pointer (#/components/schemas/<Target>) to the FK target's schema, when known. |
x-umbral-m2m + x-umbral-m2m-target | Flags a many-to-many slot and names the child schema (the property is an array of child PKs). |
x-umbral-auto-now / x-umbral-auto-now-add | Server-populated timestamp columns; aware clients skip them on autofill. |
x-umbral-noform | Companion to readOnly - the column the API never accepts in a body. |
x-umbral-noedit | Admin edit-form hint; the field is still writable in the API contract. |
x-umbral-string-repr | The model's __str__-equivalent display column. |
x-umbral-filter-field + x-umbral-filter-lookup | On filter query parameters (see below) - names the column and lookup the parameter wraps. |
Filter parameters
Query-string filtering is on by default for every resource (opt out per resource with ResourceConfig::disable_filters()). For each filterable resource the plugin emits one OpenAPI parameters entry per (column, lookup) pair on the list operation. Generated clients, Swagger UI, and the playground all see what's filterable without re-implementing the lookup grammar.
Parameter naming follows the runtime parser:
| Suffix | Parameter name |
|---|---|
eq (default) | bare column name (?status=) |
| any other | <field>__<lookup> (?title__contains=, ?id__in=) |
Types follow the column (eq, ne, gt, lt, gte, lte); __in is a CSV string; __isnull is a boolean. PK columns are skipped - filtering on id adds no value over the detail URL /api/<table>/{id}.
use umbral_rest::{RestPlugin, ResourceConfig}; RestPlugin::default() .resource(ResourceConfig::new("article")) // filtering is on by defaultNow the GET /api/article/ operation in the emitted spec carries title, title__contains, status, status__in, id__gte, etc. as discoverable query parameters.
Pagination parameters
List operations also advertise the pagination query parameters that match the
RestPlugin's configured backend, so the spec mirrors what the route actually accepts:
| Configured pagination | Emitted parameters |
|---|---|
PageNumberPagination | page, page_size |
LimitOffsetPagination | limit, offset |
NoPagination (default) / custom | none |
Pairing with REST
umbral-openapi declares "rest" in its dependencies(), so the plugin
topological sort always places it after RestPlugin. The spec mirrors the
routes that REST actually registers: if a model is excluded from REST, it will
not appear in the spec either. If you exclude a model from REST but forget to
exclude it from OpenAPI (or vice versa), the spec will reference an endpoint
that returns 404 - exclude consistently on both plugins to stay in sync.
If you want framework-wide trailing-slash normalisation rather than relying on the plugin registering both paths, opt in on the builder:
App::builder() .slash_redirect(SlashRedirect::Append) // ...See Trailing-slash redirect policy for details.
Security schemes
When the configured RestPlugin auth chain contributes a securitySchemes entry (for example BearerAuthentication), the plugin emits a components.securitySchemes block plus a global security array referencing each scheme. The global security is an OR: any one scheme satisfies the request, matching how ChainAuthentication([Session, Bearer]) actually behaves at runtime. If the wired auth class contributes no scheme (the default NoAuthentication), the spec carries no securitySchemes block.
What's not in v1
The following are deferred to a later release:
- No per-operation tagging - all operations appear in the default tag group.
- No response examples -
200responses describe the schema shape only; no inline example payloads.