This version is in beta. Some features may change before release.

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

Code
rust
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:

URLWhat you get
/openapi and /openapi/Swagger UI (both paths serve the same UI)
/openapi/openapi.jsonMachine-readable spec

Customising the spec

Chain builder methods before passing the plugin to .plugin():

Code
rust
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:

Code
rust
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 keySource 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
nullableOption<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:

ExtensionMeaning
x-umbral-choice-labelsHuman labels paired position-for-position with enum (from ChoiceField).
x-umbral-multichoice + x-umbral-choicesFlag for multichoice columns whose wire value is a CSV of the choice set.
x-umbral-fk-targetName of the table a foreign-key column points at.
x-umbral-fk-refJSON pointer (#/components/schemas/<Target>) to the FK target's schema, when known.
x-umbral-m2m + x-umbral-m2m-targetFlags 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-addServer-populated timestamp columns; aware clients skip them on autofill.
x-umbral-noformCompanion to readOnly - the column the API never accepts in a body.
x-umbral-noeditAdmin edit-form hint; the field is still writable in the API contract.
x-umbral-string-reprThe model's __str__-equivalent display column.
x-umbral-filter-field + x-umbral-filter-lookupOn 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:

SuffixParameter 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}.

Code
rust
use umbral_rest::{RestPlugin, ResourceConfig};
 
RestPlugin::default()
.resource(ResourceConfig::new("article")) // filtering is on by default

Now 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 paginationEmitted parameters
PageNumberPaginationpage, page_size
LimitOffsetPaginationlimit, offset
NoPagination (default) / customnone

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.

Info
Field-level `hide` is honored too. A column hidden in REST (`ResourceConfig::hide` / `RestPlugin::hide_model`) is excluded from the model schema, dropped from the `?fields=` picker, and never shown in Swagger UI, so the spec matches what the API actually returns. A `password_hash` you hide from responses won't leak into the generated documentation either.

If you want framework-wide trailing-slash normalisation rather than relying on the plugin registering both paths, opt in on the builder:

Code
rust
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

Info

The following are deferred to a later release:

  • No per-operation tagging - all operations appear in the default tag group.
  • No response examples - 200 responses describe the schema shape only; no inline example payloads.