Documentation

OpenAPI 3.1

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

OpenAPI 3.1

Released in 2.0.0-beta.5: OpenAPI generation reads the same controller contracts that Amber enforces at runtime.

Amber::Schema::OpenAPI.generate builds an OpenAPI 3.1 document from the ordinary Amber router and the request and response schemas registered by controllers. The generated document describes the same contracts Amber enforces at runtime.

There is no separate OpenAPI registry, route-level request or response contract keyword, or manually maintained schema list. Controller declarations are the Amber V2 source of truth.

Where the examples go

  • Put request and response contracts under src/schemas/.
  • Put their action bindings in the matching controller under src/controllers/.
  • Put the document-serving action in src/controllers/open_api_controller.cr.
  • Put every ordinary application route, including /openapi.json, inside the existing router block in config/routes.cr.
  • The relationship fragment on this page belongs inside a schema class; it is not a standalone Crystal file.

What Amber records

For each routed controller action, generation can include:

  • the HTTP verb and path, with :id converted to {id};
  • a deterministic operation ID from the controller and action;
  • path, query, header, and cookie parameters;
  • the body-only request component;
  • every declared JSON, CBOR, or COSE media type;
  • the declared success status and description;
  • field types, required fields, defaults, ranges, lengths, formats, patterns, and enums;
  • nested object and array references;
  • dependentRequired for requires_together;
  • oneOf for requires_one_of;
  • conditional if/then relationships; and
  • the automatic 400, 415, 422, and 500 contract responses.

application/cose is described as a binary COSE Encrypt0 body containing the declared CBOR schema. It is not mislabeled as a plain JSON object.

1. Bind schemas to controller actions

File: src/controllers/pets_controller.cr.

Crystal
require "../schemas/pet_schemas"

class PetsController < ApplicationController
  schema :create, CreatePetSchema
  response_schema :create,
    PetResponseSchema,
    status: 201,
    description: "Pet created"

  def create
    input = validated_as(CreatePetSchema)
    # Create and return the pet with schema-aware respond_with.
  end
end

2. Register the ordinary route

File: config/routes.cr — add this inside the router block.

Crystal
post "/pets", PetsController, :create

Route registration supplies the endpoint metadata. Keep request and response contract declarations on the controller; the ordinary route needs no extra contract options.

3. Serve the document

File: src/controllers/open_api_controller.cr — create this controller.

Crystal
class OpenAPIController < ApplicationController
  def show
    response.content_type = "application/json"

    Amber::Schema::OpenAPI.generate(
      title: "Pet Tracker API",
      version: "2.0.0",
      description: "The executable contract for the Pet Tracker API",
      server_url: ENV["PUBLIC_URL"]? || "http://127.0.0.1:3000"
    )
  end
end

File: config/routes.cr — add the document endpoint.

Crystal
get "/openapi.json", OpenAPIController, :show

Run from: the application root while the server is running.

Terminal
curl --fail-with-body \
  --header 'Accept: application/json' \
  http://127.0.0.1:3000/openapi.json

The method returns formatted JSON. Store a generated copy under public/ only when the application intentionally publishes a build artifact; generation can also happen per request as shown above.

Parameters stay out of the body

File: src/schemas/update_pet_schema.cr.

Crystal
class UpdatePetSchema < Amber::Schema::Definition
  field :id, Int64,
    required: true,
    source: Amber::Schema::ParamSource::Path

  field :preview, Bool,
    default: false,
    source: Amber::Schema::ParamSource::Query

  field :request_id, String,
    required: true,
    source: Amber::Schema::ParamSource::Header,
    source_name: "X-Request-ID"

  field :name, String, required: true
end

The generated request-body component contains name, but not id, preview, or request_id. Those values become OpenAPI parameters at their real request locations. This keeps generated clients from sending a required header inside the JSON document.

Relationships remain machine-readable

Crystal
class ContactSchema < Amber::Schema::Definition
  field :latitude, Float64
  field :longitude, Float64
  requires_together :latitude, :longitude

  field :email, String
  field :phone, String
  requires_one_of :email, :phone

  field :kind, String, enum: ["person", "business"]
  when_field :kind, "business" do
    field :company_name, String, required: true
  end
end

Amber emits the paired-coordinate dependency, exact contact alternative, and business-only requirement. OpenAPI is therefore more than a field-name dump; it preserves the relationships needed by validation-aware clients and tools.

Current boundary

OpenAPI generation currently derives operation IDs, request and response components, parameters, media types, statuses, descriptions, constraints, and relationships. Application-wide tags, authentication schemes, contact metadata, and a bundled Swagger UI are not configured through the Schema API today. Add those in a separately owned document transformation or UI layer instead of copying unsupported configuration examples into the application.