{"title":"Request and Response Schemas","description":"Executable request, response, and OpenAPI contracts in Amber V2","section":"guides","version":"v2","path":"guides/schema-api","canonical_url":"https://amberframework.org/docs/v2/guides/schema-api","markdown_url":"https://amberframework.org/docs/v2/guides/schema-api.md","inherited":false,"content_markdown":"# Request and response schemas\n\n> **Released in `2.0.0-beta.5`:** the framework now enforces these request and\n> response contracts automatically. Amber CLI `2.0.6` generates controllers\n> that use this path by default.\n\nAmber V2 schemas are executable controller contracts. One declaration controls\nrequest parsing, validation, typed values, response validation, content\nnegotiation, and OpenAPI output. A declared controller schema runs automatically\nbefore the action; it cannot become documentation that the application forgets\nto enforce.\n\nThe V1 `params.validation` API remains functional in `2.0.0-beta.5`, but\nit is deprecated. Amber plans to keep it throughout the initial V2 compatibility\nwindow and remove it\nno earlier than a later minor release such as 2.5. The exact removal release\nwill be announced separately. Upgrade the framework first, then migrate one\naction at a time.\n\n## Build a complete JSON endpoint\n\nThis example creates a pet through `POST /pets`. Every block names the file\nwhere it belongs.\n\n### 1. Define the request and response contracts\n\n**File: `src/schemas/pet_schemas.cr` — create this file.**\n\n```crystal\nclass CreatePetSchema < Amber::Schema::Definition\n  content_type \"application/json\"\n  additional_properties false\n\n  field :name, String, required: true, min_length: 1, max_length: 80\n  field :species, String, required: true, enum: [\"cat\", \"dog\", \"other\"]\n  field :age, Int32, min: 0, max: 50\n  field :request_id, String,\n    required: true,\n    source: Amber::Schema::ParamSource::Header,\n    source_name: \"X-Request-ID\"\nend\n\nclass PetResponseSchema < Amber::Schema::Definition\n  content_type \"application/json\"\n  additional_properties false\n\n  field :id, Int64, required: true\n  field :name, String, required: true\n  field :species, String, required: true\n  field :age, Int32\nend\n```\n\n`additional_properties false` closes the contract. An undeclared request-body\nor response field then fails validation. Omit that line while an existing API\nmust continue accepting and carrying fields that are not yet declared.\n\n### 2. Bind both contracts to the controller action\n\n**File: `src/controllers/pets_controller.cr` — add the declarations above the\naction and the action inside `PetsController`.**\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    pet = Pet.create!(\n      name: input.name.not_nil!,\n      species: input.species.not_nil!,\n      age: input.age\n    )\n\n    payload = {\n      \"id\"      => JSON::Any.new(pet.id),\n      \"name\"    => JSON::Any.new(pet.name),\n      \"species\" => JSON::Any.new(pet.species),\n    }\n    payload[\"age\"] = JSON::Any.new(pet.age.not_nil!) if pet.age\n\n    respond_with(payload, status: 201)\n  end\nend\n```\n\nAmber parses and validates the request before `create` runs. `validated_as`\nreturns the request-local schema instance and its generated typed getters.\nUse `validated_params` when a `Hash(String, JSON::Any)` is more convenient.\n\n`respond_with` validates the response object and status before writing bytes.\nIf application code produces a shape or status outside `PetResponseSchema`,\nAmber returns a 500 contract error instead of silently serving an undocumented\nresponse.\n\n### 3. Add the route\n\n**File: `config/routes.cr` — add this inside the existing router block.**\n\n```crystal\npost \"/pets\", PetsController, :create\n```\n\nSchemas bind to controller actions, not to extra route keywords. The ordinary\nroute also supplies the metadata used by OpenAPI generation.\n\n### 4. Exercise the contract\n\n**Run from: the application root while `amber watch` is running.**\n\n```bash\ncurl --fail-with-body \\\n  --request POST \\\n  --header 'Content-Type: application/json' \\\n  --header 'Accept: application/json' \\\n  --header 'X-Request-ID: guide-1' \\\n  --data '{\"name\":\"Mochi\",\"species\":\"cat\",\"age\":3}' \\\n  http://127.0.0.1:3000/pets\n```\n\n## Automatic contract responses\n\n| Status | Meaning |\n|---|---|\n| `400` | The JSON, CBOR, or COSE body is malformed. |\n| `406` | The requested response media type is not declared by the response schema. |\n| `415` | The request `Content-Type` is not declared by the request schema. |\n| `422` | The document parsed, but its values do not satisfy the schema. |\n| `500` | Application code produced a response shape or status outside its declared contract. |\n| `503` | A COSE request arrived before a key provider was configured. |\n\nActions without a declared schema retain their existing params behavior.\n\n## Continue from here\n\n- [Schema basics](basics/) covers field types, constraints, parameter sources,\n  nested objects, and conditional relationships.\n- [Validation and migration](validation/) explains enforcement, response\n  contracts, and the deprecated-validator compatibility bridge.\n- [Request formats](parsers/) covers JSON, forms, XML, CBOR, and encrypted COSE.\n- [OpenAPI](openapi/) serves an OpenAPI 3.1 document from the same registered\n  controller contracts."}