{"title":"OpenAPI 3.1","description":"Generate OpenAPI 3.1 from Amber's enforced controller contracts","section":"guides/schema-api","version":"v2","path":"guides/schema-api/openapi","canonical_url":"https://amberframework.org/docs/v2/guides/schema-api/openapi","markdown_url":"https://amberframework.org/docs/v2/guides/schema-api/openapi.md","inherited":false,"content_markdown":"# OpenAPI 3.1\n\n> **Released in `2.0.0-beta.5`:** OpenAPI generation reads the same controller\n> contracts that Amber enforces at runtime.\n\n`Amber::Schema::OpenAPI.generate` builds an OpenAPI 3.1 document from the\nordinary Amber router and the request and response schemas registered by\ncontrollers. The generated document describes the same contracts Amber\nenforces at runtime.\n\nThere is no separate OpenAPI registry, route-level request or response contract\nkeyword, or manually maintained schema list. Controller declarations are the\nAmber V2 source of truth.\n\n## Where the examples go\n\n- Put request and response contracts under `src/schemas/`.\n- Put their action bindings in the matching controller under `src/controllers/`.\n- Put the document-serving action in `src/controllers/open_api_controller.cr`.\n- Put every ordinary application route, including `/openapi.json`, inside the\n  existing router block in `config/routes.cr`.\n- The relationship fragment on this page belongs inside a schema class; it is\n  not a standalone Crystal file.\n\n## What Amber records\n\nFor each routed controller action, generation can include:\n\n- the HTTP verb and path, with `:id` converted to `{id}`;\n- a deterministic operation ID from the controller and action;\n- path, query, header, and cookie parameters;\n- the body-only request component;\n- every declared JSON, CBOR, or COSE media type;\n- the declared success status and description;\n- field types, required fields, defaults, ranges, lengths, formats, patterns,\n  and enums;\n- nested object and array references;\n- `dependentRequired` for `requires_together`;\n- `oneOf` for `requires_one_of`;\n- conditional `if`/`then` relationships; and\n- the automatic 400, 415, 422, and 500 contract responses.\n\n`application/cose` is described as a binary COSE Encrypt0 body containing the\ndeclared CBOR schema. It is not mislabeled as a plain JSON object.\n\n## 1. Bind schemas to controller actions\n\n**File: `src/controllers/pets_controller.cr`.**\n\n```crystal\nrequire \"../schemas/pet_schemas\"\n\nclass PetsController < ApplicationController\n  schema :create, CreatePetSchema\n  response_schema :create,\n    PetResponseSchema,\n    status: 201,\n    description: \"Pet created\"\n\n  def create\n    input = validated_as(CreatePetSchema)\n    # Create and return the pet with schema-aware respond_with.\n  end\nend\n```\n\n## 2. Register the ordinary route\n\n**File: `config/routes.cr` — add this inside the router block.**\n\n```crystal\npost \"/pets\", PetsController, :create\n```\n\nRoute registration supplies the endpoint metadata. Keep request and response\ncontract declarations on the controller; the ordinary route needs no extra\ncontract options.\n\n## 3. Serve the document\n\n**File: `src/controllers/open_api_controller.cr` — create this controller.**\n\n```crystal\nclass OpenAPIController < ApplicationController\n  def show\n    response.content_type = \"application/json\"\n\n    Amber::Schema::OpenAPI.generate(\n      title: \"Pet Tracker API\",\n      version: \"2.0.0\",\n      description: \"The executable contract for the Pet Tracker API\",\n      server_url: ENV[\"PUBLIC_URL\"]? || \"http://127.0.0.1:3000\"\n    )\n  end\nend\n```\n\n**File: `config/routes.cr` — add the document endpoint.**\n\n```crystal\nget \"/openapi.json\", OpenAPIController, :show\n```\n\n**Run from: the application root while the server is running.**\n\n```bash\ncurl --fail-with-body \\\n  --header 'Accept: application/json' \\\n  http://127.0.0.1:3000/openapi.json\n```\n\nThe method returns formatted JSON. Store a generated copy under `public/` only\nwhen the application intentionally publishes a build artifact; generation can\nalso happen per request as shown above.\n\n## Parameters stay out of the body\n\n**File: `src/schemas/update_pet_schema.cr`.**\n\n```crystal\nclass UpdatePetSchema < Amber::Schema::Definition\n  field :id, Int64,\n    required: true,\n    source: Amber::Schema::ParamSource::Path\n\n  field :preview, Bool,\n    default: false,\n    source: Amber::Schema::ParamSource::Query\n\n  field :request_id, String,\n    required: true,\n    source: Amber::Schema::ParamSource::Header,\n    source_name: \"X-Request-ID\"\n\n  field :name, String, required: true\nend\n```\n\nThe generated request-body component contains `name`, but not `id`, `preview`,\nor `request_id`. Those values become OpenAPI parameters at their real request\nlocations. This keeps generated clients from sending a required header inside\nthe JSON document.\n\n## Relationships remain machine-readable\n\n```crystal\nclass ContactSchema < Amber::Schema::Definition\n  field :latitude, Float64\n  field :longitude, Float64\n  requires_together :latitude, :longitude\n\n  field :email, String\n  field :phone, String\n  requires_one_of :email, :phone\n\n  field :kind, String, enum: [\"person\", \"business\"]\n  when_field :kind, \"business\" do\n    field :company_name, String, required: true\n  end\nend\n```\n\nAmber emits the paired-coordinate dependency, exact contact alternative, and\nbusiness-only requirement. OpenAPI is therefore more than a field-name dump;\nit preserves the relationships needed by validation-aware clients and tools.\n\n## Current boundary\n\nOpenAPI generation currently derives operation IDs, request and response\ncomponents, parameters, media types, statuses, descriptions, constraints, and\nrelationships. Application-wide tags, authentication schemes, contact\nmetadata, and a bundled Swagger UI are not configured through the Schema API\ntoday. Add those in a separately owned document transformation or UI layer\ninstead of copying unsupported configuration examples into the application."}