Dual-version OpenAPI specification generator for Ash Framework
Installation | Quick Start | Configuration | API Reference | Phoenix Integration
AshOaskit generates OpenAPI specifications from your Ash domains, supporting both 3.0 and 3.1 versions.
This project was created to address the need for OpenAPI 3.1 specification support in the Ash ecosystem. AshJsonApi provides excellent JSON:API compliance and served as significant inspiration for this library's approach to introspecting Ash resources. However, it generates OpenAPI 3.0 specifications.
OpenAPI 3.1 brings full alignment with JSON Schema 2020-12, enabling:
- Type arrays for nullable fields (
["string", "null"]instead ofnullable: true) - Better validation tooling compatibility
- Improved schema reuse patterns
This library complements AshJsonApi by reading its route configurations and generating modern OpenAPI specifications while maintaining backwards compatibility with 3.0 for teams that need it.
AshOaskit is built on top of Oaskit, a toolkit for building and manipulating OpenAPI specifications in Elixir. All generated specs are normalized and validated through Oaskit's pipeline, and JSON output uses Oaskit's SpecDumper for proper key ordering.
AshOaskit provides:
- Automatic Schema Extraction - Derives JSON Schema from Ash resource attributes
- AshJsonApi Integration - Builds paths from configured routes
- Dual Version Support - Generate 3.0 or 3.1 specs from the same codebase
- Spec Validation - Validate generated specs against the OpenAPI schema via Oaskit
- Phoenix Controller - Serve specs directly from your application
- CLI Generation - Generate static spec files for documentation
| Feature | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| Nullable Types | nullable: true |
type: ["string", "null"] |
| JSON Schema | Draft 04 subset | Draft 2020-12 |
| Tool Support | Wider compatibility | Modern validation |
Add ash_oaskit to your dependencies in mix.exs:
def deps do
[
{:ash_oaskit, "~> 0.2"},
# Optional: For YAML output
{:ymlr, "~> 5.0", optional: true}
]
enddefmodule MyAppWeb.ApiSpec do
use AshOaskit,
domains: [MyApp.Blog],
title: "My API",
api_version: "1.0.0"
endServe it (with a Redoc UI) from your Phoenix or Plug router:
use AshOaskit.Router,
spec: MyAppWeb.ApiSpec,
open_api: "/openapi",
redoc: "/redoc"The spec module implements the oaskit
behaviour: the generated spec is cached in :persistent_term, served by
Oaskit.SpecController, exportable with mix openapi.dump MyAppWeb.ApiSpec, and usable with Oaskit.Plugs.SpecProvider for
request validation of hand-written controllers. See the
Spec Modules guide.
Add
config :ash_oaskit, cache_specs: falsetoconfig/dev.exsso code reloads regenerate the spec.
# OpenAPI 3.1 (default)
spec = AshOaskit.spec(domains: [MyApp.Blog], title: "My API")
#=> %{"openapi" => "3.1.0", "info" => %{"title" => "My API", ...}, ...}
# OpenAPI 3.0
spec = AshOaskit.spec_30(domains: [MyApp.Blog])
#=> %{"openapi" => "3.0.3", ...}# Export a spec module (preferred — uses the exact spec your app serves)
mix openapi.dump MyAppWeb.ApiSpec --pretty -o openapi.json
# Generate without a spec module
mix ash_oaskit.generate -d MyApp.Blog -o openapi.json
# Generate OpenAPI 3.0 spec
mix ash_oaskit.generate -d MyApp.Blog -v 3.0 -o openapi-3.0.json
# Generate YAML format (requires ymlr)
mix ash_oaskit.generate -d MyApp.Blog -f yaml -o openapi.yamlSpecs include only fields marked public? true — the same set
AshJsonApi serializes:
attributes do
uuid_primary_key :id
attribute :title, :string do
public? true
end
# Not public: never appears in the generated spec
attribute :internal_notes, :string
endconfig :ash_oaskit,
version: "3.1", # Default OpenAPI version
title: "My API", # Default API title
api_version: "1.0.0" # Default API version| Option | Type | Description |
|---|---|---|
:domains |
[module()] |
Required. Ash domains to include |
:version |
String.t() |
OpenAPI version: "3.0" or "3.1" |
:title |
String.t() |
API title for info section |
:api_version |
String.t() |
API version string |
:description |
String.t() |
API description |
:servers |
[map()] |
Server URLs or server objects |
:contact |
map() |
Contact information |
:license |
map() |
License information |
:terms_of_service |
String.t() |
Terms of service URL |
:security |
[map()] |
Security requirements |
# Generate spec with options
AshOaskit.spec(domains: [Domain], title: "API", api_version: "1.0")
# Version-specific shortcuts
AshOaskit.spec_30(domains: [Domain]) # Force 3.0
AshOaskit.spec_31(domains: [Domain]) # Force 3.1Generated specs can be validated against the OpenAPI schema:
spec = AshOaskit.spec(domains: [MyApp.Blog])
# Returns {:ok, %Oaskit.Spec.OpenAPI{}} or {:error, reason}
{:ok, validated} = AshOaskit.validate(spec)
# Raises on invalid specs
validated = AshOaskit.validate!(spec)| Ash Type | JSON Schema | Format |
|---|---|---|
:string, :ci_string, :atom, :module |
string |
- |
:integer |
integer |
- |
:float |
number |
float |
:decimal |
number |
double |
:boolean |
boolean |
- |
:date |
string |
date |
:time, :time_usec |
string |
time |
:datetime, :utc_datetime, :utc_datetime_usec, :naive_datetime |
string |
date-time |
:duration |
string |
duration |
:uuid, :uuid_v7 |
string |
uuid |
:binary |
string |
binary |
:url_encoded_binary, Ash.Type.File |
string |
byte |
:map, :keyword, :tuple |
object |
- |
:vector |
array of number |
- |
:term, :function |
{} (any) |
- |
{:array, type} |
array |
items: nested |
Ash.Type.Enum implementors |
string + enum |
from values/0 |
Ash.Type.NewType wrappers |
(subtype schema) | via subtype_of/0 |
See AshOaskit.TypeMapper for unions, structs, embedded resources, and
custom types with a json_schema/1 callback.
| Ash Constraint | JSON Schema |
|---|---|
:min_length |
minLength |
:max_length |
maxLength |
:min |
minimum |
:max |
maximum |
:match (Regex) |
pattern |
:one_of |
enum |
Works in both Phoenix Router and Plug.Router — the macro detects the router type:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshOaskit.Router,
spec: MyAppWeb.ApiSpec,
open_api: "/openapi",
redoc: "/redoc"
# Your other routes, pipelines, scopes, etc.
endGenerates:
GET /openapi.json— the spec, served from cache (?pretty=1to format)GET /redoc— Redoc UI
Serve OpenAPI 3.0 and 3.1 side by side with two spec modules:
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.0Legacy domains mode (deprecated)
Passing :domains directly still works but regenerates the spec on
every request and emits a compile-time deprecation warning:
use AshOaskit.Router,
domains: [MyApp.Blog, MyApp.Accounts],
open_api: "/docs/openapi",
title: "My API",
version: "1.0.0"Migrate by moving the options into a spec module (use AshOaskit) and
passing it as spec:.
AshOaskit reads routes from domains using AshJsonApi.Domain:
defmodule MyApp.Blog do
use Ash.Domain, extensions: [AshJsonApi.Domain]
json_api do
routes do
base_route "/posts", MyApp.Blog.Post do
get :read
index :read
post :create
patch :update
delete :destroy
end
end
end
resources do
resource MyApp.Blog.Post
end
end- Legacy Tooling - Some API gateways only support OpenAPI 3.0
- Modern Validation - OpenAPI 3.1 uses JSON Schema 2020-12
- Gradual Migration - Upgrade specs without breaking consumers
mix test # Run tests
mix check # Run quality checks
mix docs # Generate documentation
mix coveralls.html # Check test coverageSee CONTRIBUTING.md for guidelines.
MIT License. See LICENSE.md for details.