---
title: "Validation"
manual: "TCA_API"
version: "2.2"
permalink: "https://docs.typo3.org/permalink/maikschneider/tca-api:validation@2.2"
source: "Configuration/Validation.rst"
rendered: "2026-10-12T13:17:40+00:00"
---

# Validation {#validation}

Configure validators per column to enforce data integrity on write operations.
Validators run on every POST, PUT, and PATCH request before the record is written.

## Auto-derivation from TCA {#auto-derivation-from-tca}

TCA API reads your existing TCA configuration at boot time and automatically
derives validators from constraints that are already declared there. **No
configuration is required in your resource definition** — the constraints are
picked up for free.

The following TCA constraints are derived automatically:

| TCA column type | TCA key | Derived validator | Notes |
| --- | --- | --- | --- |
| `input`, `text` | `config.max` | `maxLength` | Derived when `max > 0` |
| `number` | `config.range.lower` | `minValue` | Only when `lower` is set |
| `number` | `config.range.upper` | `maxValue` | Only when `upper` is set |
| `group`, `inline`, `file`, `category` | `config.maxitems` | `maxItems` | Always derived when set |
| `group`, `inline`, `file`, `category` | `config.minitems` | `minItems` | Only when `minitems > 0` |
| any | `config.required` | `required` flag | Standard TYPO3 field flag |

Derivation is **gap-fill only**: if you declare an explicit validator of the
same type in the resource config, the TCA-derived one is skipped for that type.
Explicit configuration always wins.

```php
// TCA declares max=255 for 'title', but your resource sets maxLength=20 —
// the explicit maxLength=20 is kept and 255 is NOT added.
'title' => [
    'groups'     => ['list', 'show', 'create', 'update'],
    'validators' => [
        ['type' => 'maxLength', 'max' => 20],  // wins over TCA max=255
    ],
],

// 'first_name' has no explicit validators — TCA max=255 is injected automatically.
'first_name' => [
    'groups' => ['list', 'show', 'create', 'update'],
],

// 'profile_photo' (type=file, maxitems=1 in TCA) — maxItems:1 injected automatically.
'profile_photo' => [
    'groups' => ['list', 'show', 'create', 'update'],
    'upload' => ['folder' => '1:/uploads/'],
],
```

### Opt-out per column {#opt-out-per-column}

To disable all TCA derivation for a specific column, set \``'tcaValidation' =>
false`\`:

```php
'notes' => [
    'groups'         => ['list', 'show', 'create', 'update'],
    'tcaValidation'  => false,  // no validators derived from TCA for this column
],
```

The `required` flag follows the same gap-fill rule: it is only injected from
TCA when the `required` key is completely absent from the column config. An
explicit `'required' => false` is honored and never overridden.

## Validator types {#validator-types}

The following validator types are available, both for explicit configuration
and as targets for TCA auto-derivation.

| Type | Parameters | Error code | Description |
| --- | --- | --- | --- |
| `required` | *(none)* | `REQUIRED` | Field must be present and non-empty on POST/PUT. Set via the `required` column key, not inside `validators`. Skipped on PATCH for absent fields. Auto-derived from TCA `config.required: true`. |
| `maxLength` | `max` (int) | `MAX_LENGTH` | Maximum string length (multi-byte safe). Auto-derived from `input`/`text` TCA `config.max`. |
| `minLength` | `min` (int) | `MIN_LENGTH` | Minimum string length. |
| `maxValue` | `max` (int\|float) | `MAX_VALUE` | Maximum numeric value. Skipped for non-numeric values. Auto-derived from `number` TCA `config.range.upper`. |
| `minValue` | `min` (int\|float) | `MIN_VALUE` | Minimum numeric value. Skipped for non-numeric values. Auto-derived from `number` TCA `config.range.lower`. |
| `maxItems` | `max` (int) | `MAX_ITEMS` | Maximum number of items in an array-valued relation field. Skipped for non-array values. Auto-derived from `group`/`inline`/`file`/ `category` TCA `config.maxitems`. |
| `minItems` | `min` (int) | `MIN_ITEMS` | Minimum number of items. Skipped for non-array values. Auto-derived from `config.minitems` when `> 0`. |
| `regex` | `pattern` (string) | `REGEX` | PCRE pattern (including delimiters) the value must match. The pattern is validated at config load time — broken patterns raise an exception immediately rather than silently failing at runtime. |

## Configuration {#configuration}

Validators are declared in the `validators` array of a column definition:

```php
'columns' => [
    'title' => [
        'groups'     => ['list', 'show', 'create', 'update'],
        'required'   => true,
        'validators' => [
            ['type' => 'maxLength', 'max' => 255],
            ['type' => 'minLength', 'min' => 3],
            ['type' => 'regex', 'pattern' => '/^[\w\s]+$/u'],
        ],
    ],
    'rating' => [
        'groups'     => ['list', 'show', 'create', 'update'],
        'validators' => [
            ['type' => 'minValue', 'min' => 1],
            ['type' => 'maxValue', 'max' => 5],
        ],
    ],
],
```

## Error response {#error-response}

Validation failures return **422 Unprocessable Entity** with a Hydra-compatible
error body. All violations are collected and returned together — the request is
not aborted on the first failure.

```json
{
    "@context": "http://www.w3.org/ns/hydra/context.jsonld",
    "@type": "hydra:Error",
    "hydra:title": "Validation Failed",
    "hydra:description": "2 validation error(s)",
    "violations": [
        {
            "propertyPath": "title",
            "message": "Field 'title' is required.",
            "code": "REQUIRED"
        },
        {
            "propertyPath": "rating",
            "message": "Field 'rating' must not exceed 5.",
            "code": "MAX_VALUE"
        }
    ]
}
```

## PATCH behaviour {#patch-behaviour}

On PATCH requests (partial updates), validation only runs on fields that are
present in the request body. Absent fields — including `required` ones — are
skipped. Auto-derived validators follow the same rule.

## Custom validators {#custom-validators}

When the built-in types are not enough, register your own rule by implementing
`\MaikSchneider\TcaApi\Validation\ValidatorInterface` and referencing it by
class-string as the validator `type`. Implementations are auto-discovered via
the `tca_api.validator` DI tag — no `Services.yaml` entry is needed as long
as your extension uses `autoconfigure` (the TYPO3 default).

```php
'iban' => [
    'groups'     => ['list', 'show', 'create', 'update'],
    'validators' => [
        ['type' => \Acme\Validator\IbanValidator::class, 'options' => ['country' => 'DE']],
    ],
],
```

The validator receives a typed
`\MaikSchneider\TcaApi\Validation\ValidationContext` and returns a (possibly
empty) list of `\MaikSchneider\TcaApi\Validation\Violation` objects:

```php
use MaikSchneider\TcaApi\Validation\ValidationContext;
use MaikSchneider\TcaApi\Validation\ValidatorInterface;
use MaikSchneider\TcaApi\Validation\Violation;

final class IbanValidator implements ValidatorInterface
{
    public function validate(ValidationContext $context): array
    {
        if (is_valid_iban((string)$context->value, $context->option('country'))) {
            return [];
        }

        return [new Violation('Not a valid IBAN.', 'IBAN')];
    }
}
```

`ValidationContext` exposes:

| Property | Description |
| --- | --- |
| `value` | The submitted value of the column being validated. |
| `column` / `table` | Column name and resource table. |
| `options` | Per-validator options from the config; use `option($key, $default)`. |
| `body` | The full request body — enables cross-field rules. |
| `partial` | `true` on PATCH requests. |
| `resourceConfig` | The full `ApiDefinition` for the resource. |

Each returned `Violation` carries a `message` and a machine-readable
`code`; its `propertyPath` defaults to the validated column but can be set
explicitly to point a violation at a different field. Violations merge into the
same **422** Hydra error response as the built-in validators. Built-in and
custom validators can be combined freely on the same column.
