---
title: "Writing a custom Extbase validator"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:extbase-domain-validator-request@main"
source: "ExtensionArchitecture/Extbase/Validation/Custom.rst"
rendered: "2026-09-22T15:26:06+00:00"
---

# Writing a custom Extbase validator {#extbase-domain-validator-request}

When the [built-in validators](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin@main) cannot
express a domain rule — for example "a conference end date must be after its
start date" or "registration is only open while seats remain" — write a
custom validator class and attach it with `#[Validate]` just like any
built-in validator.

**Model property vs. action parameter** — where you place the validator
determines what it can see and when it runs.

A validator on a *model property* receives only that property's value. It
runs on every action that takes the model as an argument. Use it for
self-contained rules that must always hold: `$title` must not be empty,
`$contactEmail` must be a valid address.

A validator on an *action parameter* receives the whole object and runs only
for that specific action. This makes it the right choice for two distinct
situations: rules that only apply in a particular context (a seat count check
that matters for `registerAction()` but not for `showAction()`), and
rules that span multiple properties. For example: if a conference starts more
than four weeks from now, a speaker assignment is optional — but if it starts
sooner, a speaker is required. That rule cannot be expressed on any single
property; it needs both `$startDate` and `$speaker` at the same time:

**EXT:my_extension/Classes/Validation/Validator/ConferenceSpeakerValidator.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Validation\Validator;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

class ConferenceSpeakerValidator extends AbstractValidator
{
  protected function isValid(mixed $value): void
  {
    $secondsUntilStart = $value->getStartDate()->getTimestamp() - time();
    $weeksUntilStart = (int)($secondsUntilStart / 604800);
    if ($weeksUntilStart < 4 && $value->getSpeaker() === null) {
      $this->addErrorForProperty(
        'speaker',
        $this->translateErrorMessage(
          'my_extension.messages:validator.conference.speakerRequired',
        ),
        1716300100,
      );
    }
  }
}

```

Both approaches can be combined on the same type — property validators run
first, and action-parameter validators run afterwards.

**On this page**

-   [Structure of a custom validator](https://docs.typo3.org/permalink/t3coreapi:structure-of-a-custom-validator@main)
-   [Reporting errors on a specific property](https://docs.typo3.org/permalink/t3coreapi:reporting-errors-on-a-specific-property@main)
-   [Supporting options and substitution values in messages](https://docs.typo3.org/permalink/t3coreapi:supporting-options-and-substitution-values-in-messages@main)
-   [Making the error message overridable](https://docs.typo3.org/permalink/t3coreapi:making-the-error-message-overridable@main)
-   [What to read next](https://docs.typo3.org/permalink/t3coreapi:what-to-read-next@main)

## Structure of a custom validator {#extbase-domain-validator-model-complete}

Custom validators extend
`\TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator` and
implement a single method `isValid(mixed $value): void`. If a value
fails validation, call `$this->addError()` or
`$this->addErrorForProperty()` — do not throw an exception and do not
return anything. The framework reads `$this->result` after
`isValid()` returns.

Place validators in `Classes/Validation/Validator/` inside your
extension, following the naming convention `<Subject>Validator`:

**EXT:my_extension/Classes/Validation/Validator/ConferenceDateValidator.php**

```php
<?php

namespace MyVendor\MyExtension\Validation\Validator;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

class ConferenceDateValidator extends AbstractValidator
{
  protected string $message = 'my_extension.messages:validator.conference.endBeforeStart';

  protected array $translationOptions = ['message'];

  protected $supportedOptions = [
    'message' => [null, 'Custom translation key or message text', 'string'],
  ];

  protected function isValid(mixed $value): void
  {
    if ($value->getEndDate() <= $value->getStartDate()) {
      $this->addErrorForProperty(
        'endDate',
        $this->translateErrorMessage($this->message),
        1716299001,
      );
    }
  }
}

```

Key points:

-   Do not declare the class as `final` — the same convention that applies
    to controllers applies here. Third parties can extend the validator.
-   `$acceptsEmptyValues = true` (the default) means `isValid()` is
    skipped when a value is `null` or an empty string. Set it to
    `false` only if a validator should explicitly reject empty values
    (as `NotEmptyValidator`
    does).
-   The error code is an arbitrary integer that must be unique across your
    extension. Using the Unix timestamp at the time of writing is a convenient
    way to generate a unique number.
-   Translation keys use the domain syntax introduced in TYPO3 v14:
    `my_extension.messages:some.key` resolves to
    [`EXT:my_extension/Resources/Private/Language/locallang.xlf`](../../FileStructure/Resources/Private/Language.md#file-extension-resources-private-language-locallang-xlf). See
    [Translation domain syntax as shorter alternative to LLL:EXT: (TYPO3 v14)](https://docs.typo3.org/permalink/t3coreapi:extbase-upgrading-translation-domain-syntax@main) for the full syntax
    including non-default language files.

## Reporting errors on a specific property {#extbase-domain-validator-model}

`$this->addErrorForProperty()` attaches the error to a named property of
the validated object rather than to the object itself. The
[Form.validationResults ViewHelper \<f:form.validationResults>](https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/Global/Form/ValidationResults.html#typo3-fluid-form-validationresults) view helper can then
display the message adjacent to the right form field:

**EXT:my_extension/Resources/Private/Templates/Conference/New.fluid.html**

```html
<f:form.validationResults for="conference.endDate">
    <f:for each="{validationResults.errors}" as="error">
        <p class="error">{error.message}</p>
    </f:for>
</f:form.validationResults>
```

Use `$this->addError()` instead when the error applies to the whole
object and does not belong to any single field.

## Supporting options and substitution values in messages {#extbase-validation-custom-options}

If your validator needs to be configured, for example, there needs to be a
minimum seat count, declare the options in `$supportedOptions`. Each option contains
an array with values `[default, description, type]`. Access resolved values using
`$this->options`:

**EXT:my_extension/Classes/Validation/Validator/SeatCountValidator.php**

```php
<?php

namespace MyVendor\MyExtension\Validation\Validator;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

class SeatCountValidator extends AbstractValidator
{
  protected string $message = 'my_extension.messages:validator.conference.notEnoughSeats';

  protected array $translationOptions = ['message'];

  protected $supportedOptions = [
    'minimum' => [1, 'Minimum number of available seats required', 'integer'],
    'message' => [null, 'Custom translation key or message text', 'string'],
  ];

  protected function isValid(mixed $value): void
  {
    $minimum = $this->options['minimum'];
    if ($value->getAvailableSeats() < $minimum) {
      $this->addError(
        $this->translateErrorMessage($this->message, '', [$minimum]),
        1716300001,
        [$minimum],
      );
    }
  }
}

```

The error message in `locallang.xlf` uses a `%s` placeholder:

**EXT:my_extension/Resources/Private/Language/locallang.xlf (excerpt)**

```xml
<trans-unit id="validator.conference.notEnoughSeats">
    <source>At least %s seat(s) must be available for registration.</source>
</trans-unit>
```

`translateErrorMessage()` accepts the values to be substituted as its third
argument and passes them through `vsprintf()`. The same values are passed
as the third argument to `addError()`. They are stored in the
`Error` object for programmatic
access:

**Substitution values in translateErrorMessage() and addError()**

```php
$this->addError(
    $this->translateErrorMessage($this->message, '', [$minimum]),
    1716300001, // An arbitrary unique number, for example the timestamp when writing the code
    [$minimum],
);
```

Attach the validator with its option on the action parameter:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Conference;
use MyVendor\MyExtension\Validation\Validator\SeatCountValidator;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ConferenceController extends ActionController
{
  public function registerAction(
    #[Validate(SeatCountValidator::class, options: ['minimum' => 1])]
    Conference $conference,
  ): ResponseInterface {
    // Only reached when SeatCountValidator passes
    return $this->htmlResponse();
  }
}

```

## Making the error message overridable {#extbase-validation-custom-translated-messages}

To allow callers to override the message text via the `options` array
without having to subclass (the same mechanism used by built-in validators) declare
a `protected string $message` property, add it to
`$translationOptions`, and list it in `$supportedOptions`. The
`SeatCountValidator` above follows this pattern. A caller can
then replace the message at the call site:

**Overriding the message at the call site**

```php
#[Validate(SeatCountValidator::class, options: [
    'minimum' => 1,
    'message' => 'my_extension.messages:validator.conference.notEnoughSeats.short',
])]
```

## What to read next {#extbase-validation-custom-next}

-   [Built-in validators and the #\[Validate\] attribute](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin@main) — check whether a built-in validator
    already covers a constraint before writing a custom one.
-   [Validation in Extbase](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-overview@main) — how the framework triggers
    `errorAction()` and how to display errors in Fluid templates.
