Validation Schemas
Edit on GitHubThis 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
- API Platform - Architecture overview
- Resource Schemas - Define resource structure
- API Platform Enablement - Creating resources
- API Platform Testing - Writing and running tests
- Troubleshooting - Common issues
- Symfony Validation Documentation - Official Symfony validation docs
Thank you!
For submitting the form