Validation Schemas

Edit on GitHub

This document explains how to define validation rules for API Platform resources in Spryker.

Validation schema basics

Validation schemas define constraints for resource properties per operation type. They are defined in separate YAML files alongside resource schemas.

Validation schema location

Validation schemas must be placed in the same directory as resource schemas, using the .validation.yml suffix:

>src/
└── Pyz/
    └── Glue/
        └── Customer/
            └── resources/
                └── api/
                    └── backend/
                        ├── customers.yml            # Resource schema
                        └── customers.validation.yml # Validation schema

Basic validation syntax

post:
  email:
    - NotBlank:
        message: "Email is required"
    - Email:
        message: "Invalid email format"

  firstName:
    - NotBlank
    - Length:
        min: 2
        max: 100
        minMessage: "Name must be at least  characters"
        maxMessage: "Name cannot exceed  characters"

patch:
  email:
    - Optional:
        constraints:
          - NotBlank
          - Email

Symfony validation constraints

API Platform supports all Symfony validation constraints out of the box.

String constraints

# Required field
- NotBlank:
    message: "This field is required"

# Email validation
- Email:
    message: "Invalid email format"

# Length validation
- Length:
    min: 2
    max: 100
    minMessage: "Too short"
    maxMessage: "Too long"

# Regular expression
- Regex:
    pattern: '/^[A-Z][a-z]+$/'
    message: "Must start with uppercase letter"

# Choice from list
- Choice:
    choices: ["active", "inactive", "pending"]
    message: "Invalid status"

# URL validation
- Url:
    message: "Invalid URL"

Numeric constraints

# Positive number
- Positive:
    message: "Must be positive"

# Range validation
- Range:
    min: 0
    max: 100
    notInRangeMessage: "Must be between {{ min }} and {{ max }}"

# Greater than
- GreaterThan:
    value: 0
    message: "Must be greater than {{ compared_value }}"

Date constraints

# Date format
- Date:
    message: "Invalid date format"

# DateTime format
- DateTime:
    message: "Invalid datetime format"

# Future date
- GreaterThan:
    value: "today"
    message: "Must be a future date"

Security constraints

# Password strength
- NotCompromisedPassword:
    message: "This password has been leaked in a data breach"

# Complex password requirements
- Regex:
    pattern: '/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/'
    message: "Password must contain uppercase, lowercase, number, and special character"

Custom constraint classes (FQCN)

In addition to Symfony’s built-in constraints, you can use Fully Qualified Class Names (FQCNs) to reference custom constraint classes from Spryker modules or third-party packages.

Basic FQCN usage

Reference custom constraints using their fully qualified class name:

post:
  email:
    - NotBlank
    - Email
    - \Spryker\Zed\Customer\Business\Validator\UniqueEmail

Generated code:

use Symfony\Component\Validator\Constraints as Assert;
use Spryker\Zed\Customer\Business\Validator\UniqueEmail;

#[Assert\NotBlank(groups: ['customers:create'])]
#[Assert\Email(groups: ['customers:create'])]
#[UniqueEmail(groups: ['customers:create'])]
public ?string $email = null;

FQCN normalization

The leading backslash is optional - both formats are supported:

# With leading backslash
- \Spryker\Zed\Customer\Business\Validator\UniqueEmail

# Without leading backslash (also valid)
- Spryker\Zed\Customer\Business\Validator\UniqueEmail

Both generate the same code.

FQCN with constraint options

Pass parameters to custom constraints just like Symfony constraints:

post:
  email:
    - \Spryker\Zed\Customer\Business\Validator\UniqueEmail:
        message: "This email address is already registered"
        ignoreDeleted: true

Generated code:

use Spryker\Zed\Customer\Business\Validator\UniqueEmail;

#[UniqueEmail(message: 'This email address is already registered', ignoreDeleted: true, groups: ['customers:create'])]
public ?string $email = null;

Collision handling and automatic aliasing

When multiple constraints have the same short name, the generator automatically creates aliases to avoid conflicts.

Spryker module collision

When constraints from different Spryker modules have the same name, the module name is included in the alias:

post:
  email:
    - \Spryker\Zed\Customer\Business\Validator\Email
    - \Spryker\Glue\Product\Business\Validator\Email

Generated code:

use Spryker\Zed\Customer\Business\Validator\Email as SprykerCustomerEmail;
use Spryker\Glue\Product\Business\Validator\Email as SprykerProductEmail;

#[SprykerCustomerEmail(groups: ['customers:create'])]
#[SprykerProductEmail(groups: ['customers:create'])]
public ?string $email = null;

Multi-vendor collision

When constraints from different vendors collide, the vendor name is used as the alias prefix:

post:
  value:
    - \Spryker\Zed\Validator\NotNull
    - \Acme\Validation\NotNull

Generated code:

use Spryker\Zed\Validator\NotNull as SprykerNotNull;
use Acme\Validation\NotNull as AcmeNotNull;

#[SprykerNotNull(groups: ['customers:create'])]
#[AcmeNotNull(groups: ['customers:create'])]
public ?string $value = null;

Collision with Symfony constraints

Symfony constraints always use the Assert\ prefix and never collide with custom constraints. FQCN constraints are imported separately:

post:
  value:
    - NotNull                          # Symfony constraint
    - \Spryker\Validator\NotNull      # Custom Spryker constraint

Generated code:

use Symfony\Component\Validator\Constraints as Assert;
use Spryker\Validator\NotNull as SprykerNotNull;

#[Assert\NotNull(groups: ['customers:create'])]
#[SprykerNotNull(groups: ['customers:create'])]
public ?string $value = null;

FQCN in composite constraints

FQCN constraints work seamlessly in composite constraints like All, Sequentially, and Optional:

post:
  items:
    - All:
        constraints:
          - \Spryker\Zed\Product\Business\Validator\ValidSku
          - \Spryker\Zed\Stock\Business\Validator\InStock

patch:
  items:
    - Optional:
        constraints:
          - All:
              constraints:
                - \Spryker\Zed\Product\Business\Validator\ValidSku

Generated code:

use Spryker\Zed\Product\Business\Validator\ValidSku;
use Spryker\Zed\Stock\Business\Validator\InStock;

#[Assert\All(constraints: [new ValidSku(), new InStock()], groups: ['customers:create'])]
public array $items = [];

Operation-specific validation

Define different validation rules for different operations:

post:
  password:
    - NotBlank
    - Length:
        min: 12
        max: 128
    - Regex:
        pattern: '/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)/'
        message: "Password must contain uppercase, lowercase, and number"

patch:
  password:
    - Optional:
        constraints:
          - Length:
              min: 12
              max: 128

put:
  password:
    - NotBlank

The operation names map to HTTP methods:

  • post → POST (create)
  • get → GET (single resource)
  • getCollection → GET (collection)
  • put → PUT (replace)
  • patch → PATCH (update)
  • delete → DELETE (remove)

Validation constraint deduplication

The generator automatically deduplicates validation constraints by their signature and groups validation groups together.

How deduplication works

If the same constraint is defined for multiple operations, it will be generated once with combined validation groups:

# Schema definition
post:
  name:
    - NotBlank
    - Length:
        max: 100

patch:
  name:
    - NotBlank
    - Length:
        max: 100

Generated code:

// Instead of duplicate constraints:
// #[Assert\NotBlank(groups: ['customers:create'])]
// #[Assert\NotBlank(groups: ['customers:update'])]
// #[Assert\Length(max: 100, groups: ['customers:create'])]
// #[Assert\Length(max: 100, groups: ['customers:update'])]

// The generator produces:
#[Assert\NotBlank(groups: ['customers:create', 'customers:update'])]
#[Assert\Length(max: 100, groups: ['customers:create', 'customers:update'])]
public ?string $name = null;

This ensures cleaner generated code while maintaining the same validation behavior across different operations.

Deduplication with FQCN constraints

Deduplication works the same way for FQCN constraints:

post:
  email:
    - \Spryker\Zed\Customer\Business\Validator\UniqueEmail

patch:
  email:
    - \Spryker\Zed\Customer\Business\Validator\UniqueEmail

Generated code:

#[UniqueEmail(groups: ['customers:create', 'customers:update'])]
public ?string $email = null;

Best practices

1. Use operation-specific validation

# ✅ Good - Different rules per operation
post:
  password:
    - NotBlank
    - Length: { min: 12 }

patch:
  password:
    - Optional:
        constraints:
          - Length: { min: 12 }

# ❌ Bad - Same validation everywhere
password:
  required: true

2. Provide meaningful error messages

# ✅ Good
- Email:
    message: "Please provide a valid email address"

- Length:
    min: 8
    minMessage: "Password must be at least 8 characters for security"

# ❌ Bad
- Email
- Length: { min: 8 }

3. Use composite constraints for arrays

# ✅ Good
items:
  - All:
      constraints:
        - NotBlank
        - Regex:
            pattern: '/^[A-Z0-9]+$/'

# ❌ Bad - Won't validate array elements
items:
  - NotBlank

4. Leverage validation groups

Validation groups are automatically managed based on operations. Use them to apply different rules for create vs update:

# POST requires password
post:
  password:
    - NotBlank

# PATCH allows optional password change
patch:
  password:
    - Optional:
        constraints:
          - Length: { min: 12 }

5. Combine Symfony and custom constraints

# ✅ Good - Mix and match as needed
email:
  - NotBlank                                              # Symfony
  - Email                                                 # Symfony
  - \Spryker\Zed\Customer\Business\Validator\UniqueEmail # Custom

# Works perfectly together

Validation schema merging

Just like resource schemas, validation schemas are merged across layers (Core → Feature → Project).

Core validation (vendor):

post:
  email:
    - NotBlank
    - Email

Project validation (Pyz):

post:
  email:
    - NotBlank
    - Email
    - \Pyz\Zed\Customer\Business\Validator\CompanyEmailDomain

Merged result (with deduplication):

post:
  email:
    - NotBlank          # Deduplicated from both layers
    - Email             # Deduplicated from both layers
    - \Pyz\Zed\Customer\Business\Validator\CompanyEmailDomain # Added in project layer

Next steps