{"title":"Schema Basics","description":"Field types, constraints, sources, and relationships in Amber V2 schemas","section":"guides/schema-api","version":"v2","path":"guides/schema-api/basics","canonical_url":"https://amberframework.org/docs/v2/guides/schema-api/basics","markdown_url":"https://amberframework.org/docs/v2/guides/schema-api/basics.md","inherited":false,"content_markdown":"# Schema basics\n\n> **Released in `2.0.0-beta.5`:** controller schema declarations are enforced\n> automatically before the action runs.\n\nSchema classes live under `src/schemas/`. They declare the data an action\naccepts or returns; the controller binds those classes to actions with `schema`\nand `response_schema`.\n\n## Where the examples go\n\n- Put named and reusable contracts in `src/schemas/*.cr`.\n- Put `schema` and `response_schema` bindings inside the matching class under\n  `src/controllers/`.\n- Put route declarations inside the existing router block in `config/routes.cr`.\n- The short field and relationship fragments on this page belong inside an\n  `Amber::Schema::Definition` subclass under `src/schemas/`; they are not\n  terminal commands or standalone Crystal files.\n\n## Built-in field types\n\n**File: a schema under `src/schemas/` — these are field declarations inside an\n`Amber::Schema::Definition` subclass.**\n\n```crystal\nfield :name, String\nfield :quantity, Int32\nfield :account_id, Int64\nfield :ratio, Float32\nfield :price, Float64\nfield :active, Bool\nfield :published_at, Time\nfield :external_id, UUID\nfield :tags, Array(String)\nfield :scores, Hash(String, Int32)\n```\n\nAmber also supports typed arrays and `Hash(String, T)` for the built-in value\ntypes. If any collection member cannot be coerced, the field fails validation;\nAmber does not discard the invalid item and report the shortened collection as\nvalid.\n\nAn unknown custom type fails closed unless the application registers an\nexplicit coercion for it.\n\n## Required, default, and closed fields\n\n```crystal\nclass RegistrationSchema < Amber::Schema::Definition\n  content_type \"application/json\"\n  additional_properties false\n\n  field :email, String, required: true, format: \"email\"\n  field :role, String, default: \"member\", enum: [\"member\", \"admin\"]\n  field :age, Int32, min: 13, max: 120\nend\n```\n\n- `required: true` rejects a missing or null value.\n- `default:` supplies and coerces a value when the field is absent.\n- `additional_properties false` rejects undeclared input or response keys.\n- The default is open for backwards compatibility, so existing APIs can adopt\n  fields incrementally.\n\n## Constraints\n\n```crystal\nfield :email, String, required: true, format: \"email\"\nfield :role, String, enum: [\"member\", \"admin\"]\nfield :score, Float64, min: 0.0, max: 1.0\nfield :nickname, String, min_length: 2, max_length: 30\nfield :slug, String, pattern: \"^[a-z0-9-]+$\"\n```\n\nSupported formats include `email`, `url` or `uri`, `uuid`, `iso8601` or\n`datetime`, `date`, `time`, `ipv4`, `ipv6`, and `hostname`. A different format\nstring is treated as a regular-expression pattern; an invalid pattern fails\nvalidation instead of silently becoming a no-op.\n\n## Body, path, query, header, and cookie values\n\nThe request body is the default source. Set `source` for every value that comes\nfrom another part of the request. Use `source_name` when the wire name should\nnot become the Crystal getter name.\n\n**File: `src/schemas/show_pet_schema.cr` — create this file.**\n\n```crystal\nclass ShowPetSchema < Amber::Schema::Definition\n  field :id, Int64,\n    required: true,\n    source: Amber::Schema::ParamSource::Path\n\n  field :include_visits, Bool,\n    default: false,\n    source: Amber::Schema::ParamSource::Query,\n    source_name: \"include_visits\"\n\n  field :request_id, String,\n    source: Amber::Schema::ParamSource::Header,\n    source_name: \"X-Request-ID\"\n\n  field :session_hint, String,\n    source: Amber::Schema::ParamSource::Cookie,\n    source_name: \"pet_session\"\nend\n```\n\n**File: `src/controllers/pets_controller.cr` — bind and use the schema inside\n`PetsController`.**\n\n```crystal\nschema :show, ShowPetSchema\n\ndef show\n  input = validated_as(ShowPetSchema)\n  pet = Pet.find!(input.id.not_nil!)\n  # Render or return the pet.\nend\n```\n\n**File: `config/routes.cr` — add the path that supplies `:id`.**\n\n```crystal\nget \"/pets/:id\", PetsController, :show\n```\n\nOpenAPI emits path, query, header, and cookie fields as parameters rather than\nincorrectly placing them in the JSON request body.\n\n## Conditional fields\n\n**File: `src/schemas/account_schema.cr` — create this schema.**\n\n```crystal\nclass AccountSchema < Amber::Schema::Definition\n  field :kind, String, required: true, enum: [\"person\", \"business\"]\n\n  when_field :kind, \"person\" do\n    field :first_name, String, required: true\n    field :last_name, String, required: true\n  end\n\n  when_field :kind, \"business\" do\n    field :company_name, String, required: true\n    field :tax_id, String, required: true\n  end\nend\n```\n\n`when_present :field` provides the same conditional-required behavior when the\ntrigger only needs to exist. Conditional fields are normalized and constrained\nthrough the same request-local validation pass as ordinary fields.\n\n## Cross-field and nested relationships\n\n```crystal\nclass AddressSchema < Amber::Schema::Definition\n  field :city, String, required: true\n  field :postal_code, String, required: true\nend\n\nclass DeliverySchema < 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  nested :address, AddressSchema, required: true\nend\n```\n\n- `requires_together` requires all named fields when any one appears.\n- `requires_one_of` requires exactly one named field.\n- `nested` validates an object with another schema and prefixes nested error\n  paths, such as `address.city`.\n- `embedded_array :addresses, AddressSchema` applies the nested contract to\n  each object in an array and reports indexed paths.\n\nThese relationships also become OpenAPI `dependentRequired`, `oneOf`, nested\n`$ref`, and conditional `if`/`then` structures.\n\n## Inline action schemas\n\nKeep reusable contracts in `src/schemas/`. For a genuinely action-local input,\nthe controller can declare the fields inline:\n\n**File: `src/controllers/health_controller.cr`.**\n\n```crystal\nclass HealthController < ApplicationController\n  schema :check do\n    field :verbose, Bool,\n      default: false,\n      source: Amber::Schema::ParamSource::Query\n  end\n\n  def check\n    values = validated_params.not_nil!\n    # Build the health response.\n  end\nend\n```\n\nThe inline declaration is enforced automatically just like a named schema."}