Skip to content

Response DTOs

A Response DTO is a typed object that represents what you send back to the client. Instead of building arrays by hand, you return a class and Antares serializes it to JSON — applying field renaming, hiding internal properties, and computing derived values along the way.


Annotate a readonly class with #[ResponseDto]:

use Antares\Serialization\Attributes\ResponseDto;
#[ResponseDto]
readonly class UserResponse
{
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
}

Return it from a controller method:

#[Get('/users/{id}')]
public function show(int $id): UserResponse
{
return new UserResponse(id: $id, name: 'John', email: 'john@example.com');
}

The client receives:

{
"id": 1,
"name": "John",
"email": "john@example.com"
}

Use #[Hide] to exclude a property from the serialized output. Useful for internal fields that should never leave the server:

#[ResponseDto]
readonly class UserResponse
{
public function __construct(
public int $id,
public string $name,
#[Hide]
public string $passwordHash,
#[Hide]
public string $internalNotes,
) {}
}

Use #[SerializeAs('key')] to output a property under a different name:

use Antares\Serialization\Attributes\SerializeAs;
#[ResponseDto]
readonly class PostResponse
{
public function __construct(
public int $id,
public string $title,
#[SerializeAs('author_id')]
public int $userId,
#[SerializeAs('published_at')]
public string $createdAt,
) {}
}
{
"id": 1,
"title": "Hello World",
"author_id": 42,
"published_at": "2024-01-15 10:30:00"
}

Use #[Computed] on a public method to include its return value as a field in the response. The method name becomes the key, converted using the DTO’s case setting:

use Antares\Serialization\Attributes\Computed;
#[ResponseDto]
readonly class PostResponse
{
public function __construct(
public int $id,
public string $title,
public string $body,
) {}
#[Computed]
public function excerpt(): string
{
return mb_strimwidth($this->body, 0, 120, '...');
}
}
{
"id": 1,
"title": "Hello World",
"body": "Full body here...",
"excerpt": "Full body here..."
}

By default property names are output in camelCase. Pass a case argument to #[ResponseDto] to change this:

#[ResponseDto(case: 'snake_case')]
readonly class UserResponse
{
public function __construct(
public int $id,
public string $firstName,
public int $authorId,
) {}
}
{
"id": 1,
"first_name": "John",
"author_id": 42
}

Available values: 'snake_case', 'pascal_case', and 'kebab_case'. #[SerializeAs] always takes precedence over the case setting for any specific field.


use Antares\Serialization\Attributes\ResponseDto;
use Antares\Serialization\Attributes\Hide;
use Antares\Serialization\Attributes\SerializeAs;
use Antares\Serialization\Attributes\Computed;
#[ResponseDto(case: 'snake_case')]
readonly class ArticleResponse
{
public function __construct(
public int $id,
public string $title,
public string $body,
public string $status,
#[SerializeAs('author_id')]
public int $userId,
#[Hide]
public string $internalNotes,
#[SerializeAs('created')]
public string $createdAt,
) {}
#[Computed]
public function excerpt(): string
{
return mb_strimwidth($this->body, 0, 120, '...');
}
}
{
"id": 1,
"title": "Hello World",
"body": "Full article body here...",
"status": "published",
"author_id": 42,
"created": "2024-01-15 10:30:00",
"excerpt": "Full article body here..."
}

internalNotes is hidden. userId is renamed to author_id via #[SerializeAs]. createdAt is renamed to created via #[SerializeAs]. excerpt is computed on the fly.