Examples
Real-world examples of using Symfony JSON Schema Validation in various scenarios.
Try Live Examples
The bundle includes working example controllers that you can test in your own project. To enable them:
Set environment variable in your
.env.local:bashOUTCOMER_VALIDATION_ENABLE_EXAMPLES=trueImport the example routes, unless your
config/routes.yamlalready importsrouting.controllers(the default Symfony skeleton does - it auto-discovers every attribute-routed controller registered as a service, including the bundle's, so no explicit import is needed in that case):php<?php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes): void { // Your existing routes... // Enable example routes conditionally if (($_ENV['OUTCOMER_VALIDATION_ENABLE_EXAMPLES'] ?? 'false') === 'true') { $routes->import('../vendor/outcomer/symfony-json-schema-validation/config/routes.yaml'); } };Clear cache and access example routes:
bashphp bin/console cache:clear/_examples/validation/user- User validation example/_examples/validation/user-dto- DTO injection example/_examples/validation/profile- Profile creation example/_examples/validation/api-user- OpenAPI documented endpoint/_examples/validation/order- Multi-level nested schemas: relative and absolute $ref/_examples/validation/info- Lists all example endpoints
Explore the source code in the bundle:
- Controllers:
vendor/outcomer/symfony-json-schema-validation/src/Examples/Controller/ - Schemas:
vendor/outcomer/symfony-json-schema-validation/src/Examples/Schemas/ - DTOs:
vendor/outcomer/symfony-json-schema-validation/src/Examples/Dto/
- Controllers:
How the Example Schemas Reference Each Other
The bundled schemas aren't flat files - they $ref each other, the same way you'd split your own schemas into reusable pieces. This is the actual dependency graph among the files in Examples/Schemas/:
- Relative
$ref(e.g.customer.json's"$ref": "name.json") resolve against the referencing file's own directory - see How It Works and Schema Basics. - Absolute
$ref(the red node,common/address.json) is registered under its own domain viaSchemaValidator::registerSchemaDir()- see Configuration - which is how the bundle keeps its own examples from colliding with your app'sschemas_path.
If you're new to JSON Schema itself (not this bundle), the official Understanding JSON Schema guide and the Opis JSON Schema references documentation cover $ref, $id and base-URI resolution in depth - this bundle doesn't reinvent any of it, it's the same Opis resolver mechanics documented there.
These examples demonstrate real implementations you can use as reference when building your own API.
Basic User Registration
Schema: config/validation/schemas/user-register.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "User registration endpoint",
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "User email address"
},
"password": {
"type": "string",
"minLength": 8,
"description": "User password (min 8 characters)"
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 100,
"description": "User full name"
}
},
"required": ["email", "password", "name"]
}
}
}Controller:
<?php
namespace App\Controller;
use App\Dto\UserRegisterDto;
use Outcomer\ValidationBundle\Attribute\MapRequest;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class AuthController extends AbstractController
{
#[Route('/api/auth/register', methods: ['POST'])]
public function register(#[MapRequest('user-register.json')] UserRegisterDto $dto): JsonResponse
{
$user = $this->userService->register(
email: $dto->email,
password: $dto->password,
name: $dto->name
);
return $this->json([
'id' => $user->getId(),
'message' => 'Registration successful'
], 201);
}
}DTO:
<?php
namespace App\Dto;
use Outcomer\ValidationBundle\Model\ValidatedDtoInterface;
readonly class UserRegisterDto implements ValidatedDtoInterface
{
public function __construct(
public string $email,
public string $password,
public string $name,
public array $violations = [],
) {}
public static function fromPayload(Payload $payload, array $violations = []): static
{
$data = $payload->getBody();
return new static(
$data->email,
$data->password,
$data->name,
$violations
);
}
public function isValid(): bool
{
return empty($this->violations);
}
public function getViolations(): array
{
return $this->violations;
}
}Product Search with Filters
Schema: config/validation/schemas/product-search.json
{
"type": "object",
"properties": {
"query": {
"type": "object",
"properties": {
"q": {
"type": "string",
"minLength": 1,
"description": "Search query"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "books", "food"],
"description": "Product category filter"
},
"minPrice": {
"type": "number",
"minimum": 0,
"description": "Minimum price filter"
},
"maxPrice": {
"type": "number",
"minimum": 0,
"description": "Maximum price filter"
},
"page": {
"type": "integer",
"minimum": 1,
"default": 1
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20
}
},
"required": ["q"]
}
}
}Controller:
#[Route('/api/products/search', methods: ['GET'])]
public function search(#[MapRequest('product-search.json')] ProductSearchDto $dto): JsonResponse
{
$results = $this->productRepository->search(
query: $dto->q,
category: $dto->category,
minPrice: $dto->minPrice,
maxPrice: $dto->maxPrice,
page: $dto->page,
limit: $dto->limit
);
return $this->json($results);
}API with Authentication Header
Schema: config/validation/schemas/protected-action.json
{
"type": "object",
"properties": {
"headers": {
"type": "object",
"properties": {
"authorization": {
"type": "string",
"pattern": "^Bearer [A-Za-z0-9-._~+/]+=*$",
"description": "JWT Bearer token"
}
},
"required": ["authorization"]
},
"body": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["approve", "reject", "pending"]
},
"comment": {
"type": "string",
"maxLength": 500
}
},
"required": ["action"]
}
}
}Controller:
#[Route('/api/requests/{id}/action', methods: ['POST'])]
public function takeAction(string $id, #[MapRequest('protected-action.json')] ActionDto $dto): JsonResponse
{
// Authorization header is validated
// Extract and verify token in your security layer
$this->requestService->takeAction($id, $dto->action, $dto->comment);
return $this->json(['status' => 'success']);
}Complex Nested Object
Schema: config/validation/schemas/order-create.json
{
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"customer": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"phone": {"type": "string", "pattern": "^\\+?[1-9]\\d{1,14}$"}
},
"required": ["name", "email"]
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"productId": {"type": "string", "format": "uuid"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number", "minimum": 0}
},
"required": ["productId", "quantity", "price"]
}
},
"shipping": {
"type": "object",
"properties": {
"address": {"type": "string"},
"city": {"type": "string"},
"zipCode": {"type": "string"},
"country": {"type": "string", "minLength": 2, "maxLength": 2}
},
"required": ["address", "city", "zipCode", "country"]
}
},
"required": ["customer", "items", "shipping"]
}
}
}DTOs:
readonly class CustomerDto
{
public function __construct(
public string $name,
public string $email,
public ?string $phone = null,
) {}
}
readonly class OrderItemDto
{
public function __construct(
public string $productId,
public int $quantity,
public float $price,
) {}
}
readonly class ShippingDto
{
public function __construct(
public string $address,
public string $city,
public string $zipCode,
public string $country,
) {}
}
readonly class CreateOrderDto implements ValidatedDtoInterface
{
/**
* @param OrderItemDto[] $items
*/
public function __construct(
public CustomerDto $customer,
public array $items,
public ShippingDto $shipping,
public array $violations = [],
) {}
public static function fromPayload(Payload $payload, array $violations = []): static
{
$data = $payload->getBody();
$customer = new CustomerDto(
$data->customer->name,
$data->customer->email
);
$items = array_map(
fn($item) => new OrderItemDto(
$item->productId,
$item->quantity,
$item->price
),
$data->items
);
$shipping = new ShippingDto(
$data->shipping->address,
$data->shipping->city,
$data->shipping->zipCode,
$data->shipping->country
);
return new static($customer, $items, $shipping, $violations);
}
public function isValid(): bool
{
return empty($this->violations);
}
public function getViolations(): array
{
return $this->violations;
}
}File Upload with Metadata
Schema: config/validation/schemas/file-upload.json
{
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 1000
},
"tags": {
"type": "array",
"items": {"type": "string"},
"maxItems": 10
},
"visibility": {
"type": "string",
"enum": ["public", "private", "unlisted"],
"default": "private"
}
},
"required": ["title"]
}
}
}Controller:
#[Route('/api/files/upload', methods: ['POST'])]
public function upload(#[MapRequest('file-upload.json')] FileUploadDto $dto, Request $request): JsonResponse
{
$file = $request->files->get('file');
$uploadedFile = $this->fileService->upload(
file: $file,
title: $dto->title,
description: $dto->description,
tags: $dto->tags,
visibility: $dto->visibility
);
return $this->json(['id' => $uploadedFile->getId()], 201);
}Next Steps
- How It Works → - Understand the philosophy
- Schema Basics → - JSON Schema fundamentals
- DTO Injection → - Type-safe DTOs