AshOaskit

Hex.pmDocsCICoverageLicense

OpenAPI 3.0 and 3.1 specification generation for Ash Framework.

Installation | Quick Start | Livebooks | Configuration | API Reference | Phoenix Integration


Overview

AshOaskit derives OpenAPI documents from Ash resources and AshJsonApi routes. It can emit OpenAPI 3.0 and 3.1 from the same domain definitions.

Try It Interactively

The Livebook notebooks are executable guides, not extra API reference pages. Start with the quick-start notebook in a browser:

Run in Livebook

Background

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:

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 uses Oaskit to normalize generated specs and render ordered JSON. Generation checks local schema references; call AshOaskit.validate/1 for full OpenAPI validation.

Features

AshOaskit provides:

Feature Comparison

FeatureOpenAPI 3.0OpenAPI 3.1
Nullable Typesnullable: truetype: ["string", "null"]
JSON SchemaExtended Wright Draft 00 subsetDraft 2020-12
Tool SupportWider compatibilityModern validation

Installation

Requires Elixir 1.17+, Ash 3.33+, Spark 2.6+, Plug 1.20.3+, and Oaskit 0.14.2+. Optional integrations require AshJsonApi 1.7.1+, Phoenix 1.8.13+, and Igniter 0.6.29+. These minimums include the security fixes used by this library.

Ash 3.33 requires an explicit string-length policy in your application:

config :ash, default_string_length_count: :codepoints

Codepoints match JSON Schema's length semantics. Configure this in your application; dependencies cannot supply application-wide Ash settings.

Add ash_oaskit to your dependencies in mix.exs:

defp deps do
[
{:ash_oaskit, "~> 0.4.1"},
# Optional: For YAML output
{:ymlr, "~> 5.0", optional: true}
]
end

Quick Start

defmodule MyAppWeb.ApiSpec do
use AshOaskit,
domains: [MyApp.Blog],
title: "My API",
api_version: "1.0.0"
end

Serve 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.

Development {: .tip}

Add config :ash_oaskit, cache_specs: false to config/dev.exs so code reloads regenerate the spec.

Generate a Spec Programmatically

# 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", ...}

CLI Generation

# 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.yaml

Field Visibility

Specs 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
end

Configuration

Application Config

config :ash_oaskit,
version: "3.1", # Default OpenAPI version
title: "My API", # Default API title
api_version: "1.0.0" # Default API version

Spec Options

OptionTypeDescription
:domains[module()]Required. Ash domains to include
:versionString.t()OpenAPI version: "3.0" or "3.1"
:titleString.t()API title for info section
:api_versionString.t()API version string
:descriptionString.t()API description
:servers[map()]Server URLs or server objects
:contactmap()Contact information
:licensemap()License information
:terms_of_serviceString.t()Terms of service URL
:security[map()]Security requirements
:resource_scope:all or :routedSchema/tag seed scope. Use :routed to omit unrouted, unreferenced internal resources

API Reference

Core Functions

# 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.1

Spec Validation

Generated 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)

Type Mapping

Ash TypeJSON SchemaFormat
:string, :ci_string, :atom, :modulestring-
:integerinteger-
:floatnumberfloat
:decimalstringexact decimal pattern
:booleanboolean-
:datestringdate
:time, :time_usecstringtime
:datetime, :utc_datetime, :utc_datetime_usec, :naive_datetimestringdate-time
:durationstringduration
:uuid, :uuid_v7stringuuid
:binarystringbinary
:url_encoded_binary, Ash.Type.Filestringbyte
:map, :keyword, :tupleobject-
:vectorarray of number-
:term, :function{} (any)-
{:array, type}arrayitems: nested
Ash.Type.Enum implementorsstring + enumfrom 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. A declared callback must return a schema map; exceptions and malformed return values stop generation with the custom type and attribute in the error instead of emitting a guessed schema.

Component names come from each resource's JSON:API type, falling back to the resource module's final segment. If two resources resolve to the same name, generation fails with both modules and the conflicting component name. Give such resources distinct JSON:API types so $ref identity remains unambiguous.

Constraint Mapping

Ash ConstraintJSON Schema
:min_lengthminLength for strings; minItems for arrays
:max_lengthmaxLength for strings; maxItems for arrays
:minminimum
:maxmaximum
:match (Regex)pattern
:one_ofenum
array :itemsConstraints on the nested item schema
array :nil_items?Nullable item schema
UUIDv7 :strict?Version 7 UUID pattern

Router Integration

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.
end

Generates:

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.0
Legacy 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:.

AshJsonApi Integration

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

Why Dual Version Support?

Development

mix test # Run tests
mix check # Run quality checks
mix docs # Generate documentation
mix coveralls.html # Check test coverage

References

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License. See LICENSE.md for details.