Skip to content

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:

  1. Set environment variable in your .env.local:

    bash
    OUTCOMER_VALIDATION_ENABLE_EXAMPLES=true
  2. Import the example routes, unless your config/routes.yaml already imports routing.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');
        }
    };
  3. Clear cache and access example routes:

    bash
    php 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
  4. 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/

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 via SchemaValidator::registerSchemaDir() - see Configuration - which is how the bundle keeps its own examples from colliding with your app's schemas_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

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
<?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
<?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

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:

php
#[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

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:

php
#[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

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:

php
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

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:

php
#[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

Released under the MIT License. Built with Opis JSON Schema.