# Fields and validation

> Define fields once and Ikiform validates every submission on the server, with clear messages and typed values.

Fields are optional. A form without fields accepts any data your form sends. Once you publish fields, Ikiform validates every submission against them on the server, rejects bad data with clear messages, and stores clean, typed values.

## Defining fields

Open a form in the dashboard and go to **Fields**. Add fields in the visual editor or edit the JSON definition directly. Each field's `id` is the input `name` your form must send.

`Form definition`:

```json
{
  "version": 1,
  "title": "Contact",
  "fields": [
    { "id": "name", "type": "text", "label": "Name", "required": true, "validation": { "max_length": 120 } },
    { "id": "email", "type": "email", "label": "Email", "required": true },
    {
      "id": "topic",
      "type": "select",
      "label": "Topic",
      "options": [
        { "value": "sales", "label": "Sales" },
        { "value": "support", "label": "Support" }
      ]
    },
    { "id": "message", "type": "textarea", "label": "Message", "validation": { "min_length": 10 } }
  ],
  "settings": { "submit_label": "Send", "success_message": "Thanks, we'll reply within a day." }
}
```

### The definition

| Key | Required | Rules |
| --- | --- | --- |
| `version` | Yes | Always `1` |
| `title` | Yes | 1 to 200 characters |
| `description` | No | Up to 2,000 characters |
| `fields` | Yes | 1 to 100 fields with unique ids, at most 2,000 options in total |
| `settings.submit_label` | No | 1 to 100 characters, used by renderers such as the [shadcn/ui component](https://ikiform.com/docs/shadcn.md) |
| `settings.success_message` | No | 1 to 1,000 characters |
| `settings.redirect_url` | No | An `http:` or `https:` URL up to 2,048 characters |
| `$schema` | No | Any string up to 200 characters, ignored |

Unknown keys are refused, at the top level and on every field, so a typo like `requried` shows up as an error when you save.

### Keys on every field

| Key | Required | Rules |
| --- | --- | --- |
| `id` | Yes | Lowercase snake_case, starts with a letter, at most 64 characters, for example `team_size` |
| `type` | Yes | One of the 12 types below |
| `label` | Yes | 1 to 200 characters. Used in error pages and notification emails |
| `required` | No | `true` makes an empty value an error |
| `placeholder` | No | Up to 200 characters |
| `help_text` | No | Up to 1,000 characters |

## Playground

The interactive playground on this page lets you edit a form definition and a sample JSON payload. It runs Ikiform's real validator in the browser and shows the exact `201` or `422` response the server would return, the stored, coerced values, and generated HTML markup for the form.

## Field types

| Type | Stored as | Accepts |
| --- | --- | --- |
| `text` | string | Any single-line text |
| `textarea` | string | Multi-line text |
| `email` | string, domain lowercased | One email address |
| `number` | number | Numbers, or numeric strings from HTML forms |
| `select` | string | One listed option value |
| `radio` | string | One listed option value |
| `multi_select` | string[] | Listed option values, duplicates removed |
| `checkbox` | boolean | `on`, `true`, `1`, `yes`, or nothing |
| `date` | string, `YYYY-MM-DD` | A real calendar date |
| `url` | string, normalized | A web address, `https://` added when missing |
| `phone` | string | Digits, spaces, `+ ( ) . -` |
| `rating` | integer | A whole number from 1 to `max` |

### text

| validation | Values | Default |
| --- | --- | --- |
| `min_length` | 0 to 10,000 | None |
| `max_length` | 1 to 10,000, or at most 256 with a pattern | 1,000, or 256 with a pattern |
| `pattern` | A regular expression up to 200 characters, matched against the whole value | None |
| `pattern_message` | 1 to 200 characters | `Invalid format` |

Patterns run with the `u` flag and must match the whole value. When you save, Ikiform refuses patterns that could backtrack catastrophically: backreferences, repeated groups that contain repeats or alternatives (`(a+)+`, `(a|b)*`), and too many nested repeats.

### textarea

| Key | Values | Default |
| --- | --- | --- |
| `validation.min_length` | 0 to 16,000 | None |
| `validation.max_length` | 1 to 16,000 | 10,000 |
| `rows` | 1 to 50, a display hint for renderers | None |

### email

| validation | Values | Default |
| --- | --- | --- |
| `allowed_domains` | 1 to 100 domains such as `example.com`, matched exactly | Any domain |

Addresses longer than 254 characters are refused. The domain part is stored lowercased; the part before `@` is kept as sent.

### number

| validation | Values | Default |
| --- | --- | --- |
| `min` | Any number | None |
| `max` | Any number, at least `min` | None |
| `integer` | `true` to refuse decimals | `false` |

### select, radio and multi_select

| Key | Values | Default |
| --- | --- | --- |
| `options` | 1 to 500 `{ "value", "label" }` objects with unique values, each 1 to 200 characters | Required |
| `validation.min_items` | `multi_select` only, 0 to 500 | 0, or 1 when required |
| `validation.max_items` | `multi_select` only, 1 to 500 | The number of options |

Only listed values are accepted. `select` and `radio` take one value; send `multi_select` as repeated names, `name[]` or a JSON array.

### checkbox

No extra keys. A missing checkbox is `false`. `required: true` means it must be checked, which suits consent boxes. Values other than `on`, `true`, `1`, `yes`, `off`, `false`, `0`, `no` or empty are refused.

### date

| validation | Values | Default |
| --- | --- | --- |
| `min` | A `YYYY-MM-DD` date | None |
| `max` | A `YYYY-MM-DD` date, on or after `min` | None |

### url

| validation | Values | Default |
| --- | --- | --- |
| `https_only` | `true` to refuse `http:` | `false` |
| `max_length` | 1 to 4,096 | 2,048 |

Only `http:` and `https:` URLs with a dotted hostname and no username or password are accepted. The stored value is the normalized URL.

### phone

| validation | Values | Default |
| --- | --- | --- |
| `e164` | `true` to require international format, stored without separators | `false` |

Without `e164`, a phone number needs 6 to 15 digits and is stored as sent. With it, `+1 (415) 555-0123` is stored as `+14155550123`.

### rating

| validation | Values | Default |
| --- | --- | --- |
| `max` | 2 to 10 | 5 |

## Coercion

HTML forms send everything as text. Ikiform turns it into the type each field declares before storing. With `name` as text, `email`, `guests` as a number, `newsletter` as a checkbox, `site` as a URL and `phone` as a phone field:

`Posted, urlencoded`:

```text
name=++Ada+Lovelace++&email=Ada@Example.COM&guests=3&newsletter=on&site=example.com&phone=%2B1%20415%20555%200123
```

`Stored, with phone set to e164`:

```json
{
  "name": "Ada Lovelace",
  "email": "Ada@example.com",
  "guests": 3,
  "newsletter": true,
  "site": "https://example.com/",
  "phone": "+14155550123"
}
```

- Values are trimmed. An empty string counts as missing.
- A field that expects one value refuses an array with more than one item.
- Numbers accept plain decimal and exponent notation up to 64 characters, such as `3`, `-2.5` or `1e3`.

## Validation errors

A submission that fails validation is rejected with `422 validation_failed` and nothing is stored. `error.fields` maps each field id to its messages. Browser posts see the same messages on a hosted error page, labelled with your field labels.

`422 Unprocessable Content`:

```json
{
  "ok": false,
  "error": {
    "code": "validation_failed",
    "message": "Some fields are invalid.",
    "fields": {
      "email": ["Enter a valid email address"],
      "website": ["Unknown field \"website\""]
    }
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

### Unknown fields

With fields defined, any field that is not in the definition is rejected as `Unknown field "name"`. Reserved fields that start with `_`, the honeypot and `cf-turnstile-response` don't count. Without a definition, any field names are accepted.

### Messages

| Problem | Message |
| --- | --- |
| Missing required value | `This field is required` |
| Required checkbox unchecked | `This box must be checked` |
| Too short or too long | `Must be at least 10 characters`, `Must be at most 120 characters` |
| Pattern mismatch | Your `pattern_message`, or `Invalid format` |
| Email | `Enter a valid email address`, `Email must be at one of: example.com` |
| Number | `Enter a number`, `Enter a whole number`, `Must be at least 1` |
| Options | `Choose one of the listed options`, `Choose at least 2 options`, `Choose at most 3 options` |
| Date | `Enter a valid date (YYYY-MM-DD)`, `Must be on or after 2026-01-01` |
| URL | `Enter a valid URL`, `Enter a valid https:// URL` |
| Phone | `Enter a valid phone number`, `Use international format, e.g. +14155550123` |
| Rating | `Choose a rating`, `Rating must be at most 5` |

## Validating in the browser

The SDK's `validate` mirrors the server rules and messages for all 12 field types, so you can show errors before posting. The server stays the source of truth. See [validate](https://ikiform.com/docs/sdk.md#validate).
