AshOaskit Usage Rules

View Source

OpenAPI specification generator for Ash Framework domains. Supports OpenAPI 3.0 and 3.1.

Spec Modules (preferred)

# Define a cached, oaskit-native spec module
defmodule MyAppWeb.ApiSpec do
  use AshOaskit,
    domains: [MyApp.Blog],
    title: "My API",
    api_version: "1.0.0"
end

# Customize the generated spec (result is cached)
defmodule MyAppWeb.ApiSpec do
  use AshOaskit, domains: [MyApp.Blog]

  @impl AshOaskit.Spec
  def modify_spec(spec) do
    put_in(spec, ["components", "securitySchemes"], %{
      "bearerAuth" => %{"type" => "http", "scheme" => "bearer"}
    })
  end
end

# Serve it (Phoenix or Plug.Router) with optional Redoc UI
use AshOaskit.Router,
  spec: MyAppWeb.ApiSpec,
  open_api: "/openapi",
  redoc: "/redoc"

# Dual version: two spec modules
use AshOaskit.Router,
  spec: [{"3.1", MyAppWeb.ApiSpecV31}, {"3.0", MyAppWeb.ApiSpecV30}],
  open_api: "/openapi"

# Export the exact served spec
# mix openapi.dump MyAppWeb.ApiSpec --pretty -o openapi.json

Rules:

  • The spec is cached in :persistent_term; disable in dev with config :ash_oaskit, cache_specs: false (or per module with cache: false).
  • One spec module = one OpenAPI version; define two modules for dual-version output.
  • Only public? true attributes/calculations/aggregates/relationships appear in specs.
  • Request body schemas follow the routed action's accept list plus its public arguments.

Programmatic API

# Generate spec (defaults to 3.1)
AshOaskit.spec(domains: [MyApp.Blog])
AshOaskit.spec_30(domains: [MyApp.Blog])  # Force 3.0
AshOaskit.spec_31(domains: [MyApp.Blog])  # Force 3.1

# Full options
AshOaskit.spec(
  domains: [MyApp.Blog, MyApp.Accounts],
  version: "3.1",
  title: "My API",
  api_version: "2.0.0",
  description: "API description",
  servers: [%{"url" => "https://api.example.com"}],
  contact: %{"name" => "Support", "email" => "api@example.com"},
  license: %{"name" => "MIT"},
  security: [%{"bearerAuth" => []}]
)

CLI

# Preferred once a spec module exists:
mix openapi.dump MyAppWeb.ApiSpec --pretty -o openapi.json

mix ash_oaskit.generate -d MyApp.Blog -o openapi.json
mix ash_oaskit.generate -d MyApp.Blog,MyApp.Accounts -v 3.0 -o openapi.yaml -f yaml
mix ash_oaskit.generate --domains MyApp.Blog --title "My API" --api-version 1.0.0

Router Macro

# Preferred: spec module mode (cached, supports redoc:)
use AshOaskit.Router,
  spec: MyAppWeb.ApiSpec,
  open_api: "/openapi",
  redoc: "/redoc"

# Plug.Router — same options, place before catch-all `match _`

# DEPRECATED (regenerates spec per request, warns at compile time):
use AshOaskit.Router,
  domains: [MyApp.Blog],
  open_api: "/openapi",
  title: "My API"

Domain Setup

defmodule MyApp.Blog do
  use Ash.Domain, extensions: [AshJsonApi.Domain]

  resources do
    resource MyApp.Blog.Post
  end

  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
end

Resource Setup

defmodule MyApp.Blog.Post do
  use Ash.Resource, domain: MyApp.Blog, extensions: [AshJsonApi.Resource]

  json_api do
    type "post"
  end

  attributes do
    uuid_primary_key :id

    # public? true is REQUIRED for fields to appear in the spec
    attribute :title, :string,
      public?: true,
      allow_nil?: false,
      constraints: [min_length: 1, max_length: 255]

    attribute :body, :string, public?: true, description: "Post content"
    attribute :status, :atom, public?: true, constraints: [one_of: [:draft, :published]], default: :draft
  end

  actions do
    defaults [:read, :destroy]
    create :create, accept: [:title, :body, :status]
    update :update, accept: [:title, :body, :status]
  end
end

Type Mapping

Ash TypeJSON SchemaFormat
:string, :ci_string, :atom, :modulestring-
:integerinteger-
:floatnumberfloat
:decimalnumberdouble
: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}array-
Ash.Type.Enum implementorsstring + enum-
Ash.Type.NewType wrappers(subtype schema)-

Constraint Mapping

AshJSON Schema
:min_lengthminLength
:max_lengthmaxLength
:minminimum
:maxmaximum
:matchpattern
:one_ofenum

Version Differences

  • 3.0: nullable: true for nullable fields
  • 3.1: type: ["string", "null"] for nullable fields (JSON Schema 2020-12)

Module Structure

lib/ash_oaskit.ex                              # Main API (spec, validate)
lib/ash_oaskit/
  open_api.ex                                  # Version routing
  spec.ex                                      # Spec module behaviour (use AshOaskit)
  open_api_controller.ex                       # Controller behaviour
  phoenix_introspection.ex                     # Phoenix router extraction
  router.ex                                    # Router macro
  spec_builder.ex                              # SpecBuilder behaviour
  spec_builder/default.ex                      # Default SpecBuilder
  core/
    config.ex                                  # AshJsonApi DSL reader
    route_gathering.ex                         # Domain + resource route collection
    path_utils.ex                              # Path param conversion
    schema_ref.ex                              # $ref object builder
    spec_modifier.ex                           # Post-generation hooks
    type_mapper.ex                             # Ash → JSON Schema types
  generators/
    generator.ex                               # Main orchestrator
    info_builder.ex                            # Info, servers, tags
    path_builder.ex                            # Paths and operations
    shared.ex                                  # Entry point (both versions)
    v30.ex                                     # OpenAPI 3.0 entry
    v31.ex                                     # OpenAPI 3.1 entry
  parameters/
    filter_builder.ex                          # Filter query params
    query_parameters.ex                        # page, fields, include, sort
    sort_builder.ex                            # Sort param schemas
  resources/
    included_resources.ex                      # Included array schemas
    resource_identifier.ex                     # Type+id linkage
    tag_builder.ex                             # Operation grouping tags
  responses/
    error_schemas.ex                           # JSON:API error responses
    response_links.ex                          # Self, related, pagination links
    response_meta.ex                           # Pagination meta schemas
  routes/
    relationship_routes.ex                     # Relationship endpoints
    route_operations.ex                        # Operation object builder
    route_responses.ex                         # Response schema builder
  schemas/
    embedded_schemas.ex                        # Embedded resource detection
    nullable.ex                                # Version-aware nullable
    property_builders.ex                       # Attrs/calcs/aggregates → schema
    relationship_schemas.ex                    # Relationship linkage schemas
    resource_schemas.ex                        # Resource schema generation
    schema_builder.ex                          # Accumulator + cycle detection
  support/
    controller.ex                              # Phoenix controller
    multipart_support.ex                       # File upload schemas
    security.ex                                # Security schemes
  router/
    plug.ex                                    # Plug for serving specs
mix/tasks/
  ash_oaskit.generate.ex                       # CLI: mix ash_oaskit.generate
  ash_oaskit.install.ex                        # CLI: mix ash_oaskit.install

Configuration

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

# Dev only: regenerate the spec on code reload
config :ash_oaskit, cache_specs: false

Testing

test "generates valid spec" do
  spec = AshOaskit.spec(domains: [MyApp.Blog])
  assert spec["openapi"] == "3.1.0"
  assert is_map(spec["paths"])
  assert is_map(spec["components"]["schemas"])
end

Development

mix deps.get && mix test && mix check