Skip to content

DTOs

A DTO (Data Transfer Object) is how Antares models an incoming request body. You define a readonly class, annotate it with #[Dto], add validation rules to each constructor parameter, and Antares handles the rest — decoding the body, mapping fields, validating, and injecting the populated object into your controller.

If anything fails, a 422 response is returned automatically with a field-level error map. Your controller method is never called.


use Antares\Validation\Attributes\Dto;
use Antares\Validation\Attributes\NotBlank;
use Antares\Validation\Attributes\Email;
use Antares\Validation\Attributes\MinLength;
#[Dto]
readonly class CreateUserRequest
{
public function __construct(
#[NotBlank]
#[MinLength(2)]
public string $name,
#[NotBlank]
#[Email]
public string $email,
) {}
}

Use it in a controller by type-hinting it as a parameter:

#[Post('/users', 201)]
public function store(CreateUserRequest $request): UserResponse
{
return new UserResponse(id: 1, name: $request->name, email: $request->email);
}

Antares sees the #[Dto] annotation, decodes the request body, maps the fields, runs all validation rules, and injects the populated $request. If name is missing or email is invalid, the controller never runs and the client gets:

{
"type": "https://antares.dev/errors",
"title": "Validation failed",
"status": 422,
"errors": {
"name": ["Must be at least 2 characters"],
"email": ["Must be a valid email address"]
}
}

Parameters with defaults are optional in the request body:

#[Dto]
readonly class ListUsersRequest
{
public function __construct(
public int $page = 1,
public int $perPage = 15,
#[In(['asc', 'desc'])]
public string $order = 'asc',
) {}
}

DTOs can nest other DTOs. The hydrator recurses into them automatically:

#[Dto]
readonly class AddressRequest
{
public function __construct(
#[NotBlank] public string $street,
#[NotBlank] public string $city,
#[NotBlank] public string $country,
) {}
}
#[Dto]
readonly class CreateOrderRequest
{
public function __construct(
#[NotBlank] public string $sku,
#[Min(1)] public int $quantity,
public AddressRequest $address,
) {}
}
{
"sku": "WIDGET-42",
"quantity": 3,
"address": {
"street": "123 Main St",
"city": "Athens",
"country": "GR"
}
}

Validation errors from nested DTOs are reported under their parent field name.


By default the hydrator ignores unknown fields in the request body. Add #[Strict] to throw a HydrationException → 400 Bad Request if the request sends any field not declared on the DTO:

#[Dto]
#[Strict]
readonly class CreateUserRequest
{
public function __construct(
public string $name,
public string $email,
) {}
}

Sending "name": "John", "email": "john@example.com", "role": "admin" with #[Strict] throws a HydrationException400 Bad Request.


AttributeDescription
#[NotBlank]Must not be empty
#[MinLength(n)]Minimum string length
#[MaxLength(n)]Maximum string length
#[Email]Valid email address
#[Url]Valid URL
#[Pattern('/regex/')]Matches regular expression
#[Alpha]Alphabetic characters only
#[AlphaNumeric]Alphanumeric characters only
#[Numeric]Numeric string
#[HexColor]Valid hex color (#fff or #ffffff)
#[Uuid]Valid UUID v4
#[Phone]Valid phone number
#[Ip]Valid IP address (v4 or v6)
#[Json]Valid JSON string
AttributeDescription
#[Min(n)]Minimum numeric value
#[Max(n)]Maximum numeric value
#[Between(min, max)]Value within range (inclusive)
#[Positive]Must be positive
#[Negative]Must be negative
AttributeDescription
#[NotNull]Must not be null
#[In([...])]Must be one of the given values
#[InEnum(MyEnum::class)]Must be a valid enum case
#[Size(n)]Array must have exactly n elements
#[ArrayOf(type)]All array elements must be of the given type
#[Date]Valid date string (Y-m-d)
#[DateTime]Valid datetime string (Y-m-d H:i:s)

Implement ValidationAttribute to write your own rule:

use Antares\Validation\Attributes\ValidationAttribute;
use Attribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
final class Slug implements ValidationAttribute
{
public function validate(mixed $value): ?string
{
if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', (string) $value)) {
return 'Must be a valid slug.';
}
return null;
}
}

Return a string error message on failure, null on pass. Then use it like any built-in attribute:

#[Dto]
readonly class CreatePostRequest
{
public function __construct(
#[NotBlank]
public string $title,
#[NotBlank]
#[Slug]
public string $slug,
) {}
}