Spec Modules
View SourceA spec module turns your Ash domains into a long-lived OpenAPI spec that plugs into the whole oaskit toolchain. It is the recommended way to use AshOaskit.
Defining a spec module
defmodule MyAppWeb.ApiSpec do
use AshOaskit,
domains: [MyApp.Blog, MyApp.Accounts],
title: "My API",
api_version: "1.0.0"
enduse AshOaskit implements the Oaskit behaviour: MyAppWeb.ApiSpec.spec/0
returns the generated spec map, cached in :persistent_term so the Ash
domain walk runs once — not on every request.
All options:
| Option | Type | Default | Description |
|---|---|---|---|
:domains | [module()] | required | Ash domains to include |
:version | "3.0" or "3.1" | "3.1" | OpenAPI version |
:title | String.t() | "API" | info.title |
:api_version | String.t() | "1.0.0" | info.version |
:description | String.t() | nil | info.description |
:terms_of_service | String.t() | nil | info.termsOfService |
:contact | map() | nil | info.contact |
:license | map() | nil | info.license |
:servers | list | [] | servers array |
:security | [map()] | nil | Top-level security requirements |
:external_docs | map() | nil | External documentation object |
:router | module() | nil | Phoenix router for controller introspection |
:modify_open_api | function/MFA | nil | Post-generation hook |
:spec_builder | module() | nil | AshOaskit.SpecBuilder implementation |
:cache | boolean() | true | Cache the generated spec |
Serving the spec
Through the router macro (Phoenix or Plug.Router):
use AshOaskit.Router,
spec: MyAppWeb.ApiSpec,
open_api: "/openapi",
redoc: "/redoc"This serves GET /openapi.json (append ?pretty=1 for readable output)
and a Redoc UI at GET /redoc.
Or wire Oaskit.SpecController directly:
get "/openapi.json", Oaskit.SpecController, spec: MyAppWeb.ApiSpec
get "/redoc", Oaskit.SpecController, redoc: "/openapi.json"Scoped routers
Redoc fetches the spec by absolute URL. If the router macro runs inside
a scope "/api", pass redoc_spec_url: "/api/openapi.json".
Customizing the generated spec
Override modify_spec/1 — it runs after generation and its result is
what gets cached:
defmodule MyAppWeb.ApiSpec do
use AshOaskit, domains: [MyApp.Blog]
@impl AshOaskit.Spec
def modify_spec(spec) do
spec
|> put_in(["components", "securitySchemes"], %{
"bearerAuth" => %{"type" => "http", "scheme" => "bearer"}
})
|> Map.put("security", [%{"bearerAuth" => []}])
end
endThe Oaskit callbacks are also overridable:
cache/1— swap:persistent_termfor another cache backendcache_variant/0— key the cache, e.g. per tenantjsv_opts/0— JSV validation options for request validation
Caching in development
:persistent_term survives code reloads, so a stale spec can linger in
dev. Disable caching there:
# config/dev.exs
config :ash_oaskit, cache_specs: falseThe switch is read at runtime on every call, so no recompilation is
needed. A single module can also opt out with use AshOaskit, cache: false.
Dual-version output
An Oaskit spec module is one spec by contract. To serve OpenAPI 3.0 and 3.1 side by side, define two modules and register both:
defmodule MyAppWeb.ApiSpecV31 do
use AshOaskit, domains: [MyApp.Blog], version: "3.1"
end
defmodule MyAppWeb.ApiSpecV30 do
use AshOaskit, domains: [MyApp.Blog], version: "3.0"
end
use AshOaskit.Router,
spec: [{"3.1", MyAppWeb.ApiSpecV31}, {"3.0", MyAppWeb.ApiSpecV30}],
open_api: "/openapi"
# GET /openapi.json -> 3.1 (first entry)
# GET /openapi/3.1.json -> 3.1
# GET /openapi/3.0.json -> 3.0Exporting the spec
mix openapi.dump MyAppWeb.ApiSpec --pretty -o priv/static/openapi.json
oaskit's dump task uses the exact spec your application serves —
including modify_spec/1 post-processing.