Schemas and Resource Generation

Edit on GitHub

This document explains how API Platform schemas are defined and how resources are generated in Spryker.

Schema file structure

API Platform uses YAML files to define resource schemas. Schemas describe the structure, operations, and behavior of your API resources.

Schema location

Schemas must be placed in the resources/api/{api-type}/ directory within your module:

>src/
├── Spryker/
│   └── {Module}/
│       └── resources/
│           └── api/
│               ├── storefront/
│               │   ├── resource-name.yml
│               │   └── resource-name.validation.yml
│               └── backend/
│                   ├── resource-name.yml
│                   └── resource-name.validation.yml
├── SprykerFeature/
│   └── {Feature}/
│       └── resources/
│           └── api/
│               └── backend/
│                   └── resource-name.yml
└── Pyz/
    └── Glue/
        └── {Module}/
            └── resources/
                └── api/
                    └── backend/
                        └── resource-name.yml

Basic schema structure

A complete resource schema consists of two files:

  1. Resource schema ({resource-name}.yml) - Defines structure and operations
  2. Validation schema ({resource-name}.validation.yml) - Defines validation rules

Resource schema syntax

Minimal example

resource:
  name: Products
  shortName: Product
  description: "Product resource"

  operations:
    - type: Get
    - type: GetCollection

  properties:
    id:
      type: integer
      writable: false
      identifier: true

    name:
      type: string

Complete example with all options

# yaml-language-server: $schema=../../../../SprykerSdk/Api/resources/schemas/api-resource-schema-v1.json

resource:
  # Resource identification
  name: Customers                    # Internal name (used for schema merging)
  shortName: Customer                # URL name (becomes /customers)
  description: "Customer resource"   # OpenAPI description

  # State providers and processors
  provider: "Pyz\\Glue\\Customer\\Api\\Backend\\Provider\\CustomerBackendProvider"
  processor: "Pyz\\Glue\\Customer\\Api\\Backend\\Processor\\CustomerBackendProcessor"

  # Pagination configuration
  paginationEnabled: true
  paginationItemsPerPage: 10
  paginationMaximumItemsPerPage: 100
  paginationClientEnabled: true
  paginationClientItemsPerPage: true

  # Security
  security: "is_granted('ROLE_ADMIN')"
  securityPostDenormalize: "is_granted('EDIT', object)"

  # Operations
  operations:
    - type: Post                     # Create new resource
    - type: Get                      # Get single resource
    - type: GetCollection            # Get collection with pagination
    - type: Put                      # Replace entire resource
    - type: Patch                    # Update partial resource
    - type: Delete                   # Delete resource

  # Properties
  properties:
    idCustomer:
      type: integer
      description: "The unique identifier of the customer."
      writable: false                # Read-only property
      readable: true                 # Include in responses (default: true)

    email:
      type: string
      description: "The email address."
      required: true                 # Required for all operations
      openapiContext:
        example: "[email protected]"
        format: "email"

    firstName:
      type: string
      description: "First name."
      openapiContext:
        example: "John"
        minLength: 1
        maxLength: 100

    status:
      type: string
      description: "Customer status."
      openapiContext:
        example: "active"
        schema:
          enum: ["active", "inactive", "pending"]

    customerReference:
      type: string
      description: "Unique customer reference."
      writable: false
      identifier: true               # Use as URL identifier instead of @id

    dateOfBirth:
      type: string
      description: "Date of birth."
      openapiContext:
        format: "date"
        example: "1990-01-01"

    isActive:
      type: boolean
      description: "Active status."
      default: true

    creditLimit:
      type: number
      description: "Credit limit."
      openapiContext:
        format: "float"
        example: 5000.00

Property types

Supported types

Type PHP Type Example Description
string string "John" Text values
integer int 42 Whole numbers
number float 3.14 Decimal numbers
boolean bool true True/false values
array array ["a", "b"] Arrays
object object {"key": "value"} Nested objects

Property attributes

writable

Controls if property can be sent in requests (POST/PUT/PATCH):

password:
  type: string
  writable: true    # Can be sent in requests
  readable: false   # Not included in responses

readable

Controls if property is included in responses:

idCustomer:
  type: integer
  writable: false   # Cannot be modified
  readable: true    # Included in responses

identifier

Marks property as URL identifier:

customerReference:
  type: string
  identifier: true  # URL becomes /customers/{customerReference}

required

Makes property mandatory (use validation schema for detailed rules):

email:
  type: string
  required: true    # Must be present

default

Sets default value:

isActive:
  type: boolean
  default: true     # Defaults to true if not provided

Validation schema syntax

Validation schemas define constraints for each operation type.

Basic validation

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

Available validation constraints

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"

Operation-specific validation

Define different 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)

Resource generation process

Generation workflow

>1. Schema Discovery
   ↓
2. Schema Loading (YAML)
   ↓
3. Schema Parsing
   ↓
4. Schema Validation
   ↓
5. Schema Merging (Core → Feature → Project)
   ↓
6. Resource Class Generation
   ↓
7. Cache Update

Multi-layer schema merging

Spryker automatically merges schemas from multiple layers:

Core layer (lowest priority):

# vendor/spryker/customer/resources/api/backend/customer.yml
resource:
  name: Customers
  properties:
    email:
      type: string
    firstName:
      type: string

Feature layer (medium priority):

# src/SprykerFeature/CRM/resources/api/backend/customer.yml
resource:
  name: Customers
  properties:
    phone:
      type: string      # Added property

Project layer (highest priority):

# src/Pyz/GLue/Customer/resources/api/backend/customer.yml
resource:
  name: Customers
  properties:
    email:
      required: true    # Override core definition
    customField:
      type: string      # Project-specific field

Merged result:

resource:
  name: Customers
  properties:
    email:
      type: string
      required: true    # From project layer
    firstName:
      type: string      # From core layer
    phone:
      type: string      # From feature layer
    customField:
      type: string      # From project layer

Generated resource class

The generator creates a complete PHP class with API Platform attributes:

<?php

declare(strict_types=1);
namespace Generated\Api\Backend;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiProperty;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;

#[ApiResource(
    operations: [new Post(), new Get(), new GetCollection(), new Patch(), new Delete()],
    shortName: 'Customer',
    provider: CustomerBackendProvider::class,
    processor: CustomerBackendProcessor::class,
    paginationItemsPerPage: 10
)]
final class CustomersBackendResource
{
    #[ApiProperty(writable: false)]
    public ?int $idCustomer = null;

    #[ApiProperty(openapiContext: ['example' => '[email protected]'])]
    #[Assert\NotBlank(groups: ['customers:create'])]
    #[Assert\Email(groups: ['customers:create'])]
    public ?string $email = null;

    #[ApiProperty(identifier: true, writable: false)]
    public ?string $customerReference = null;

    // Getters, setters, toArray(), fromArray() methods...
}

Debugging schemas

Debug commands

# List all resources
docker/sdk cli glue  api:debug --list

# Show specific resource
docker/sdk cli glue  api:debug customers --api-type=backend

# Show merged schema
docker/sdk cli glue  api:debug customers --api-type=backend --show-merged

# Show contributing source files
docker/sdk cli glue  api:debug customers --api-type=backend --show-sources

# Validate schemas without generating
docker/sdk cli glue  api:generate --validate-only

Common schema errors

The generator validates schemas and provides detailed error messages:

# Missing required fields
Error: Resource "customers" is missing required field "name"

# Invalid operation type
Error: Invalid operation type "INVALID". Must be one of: Get, Post, Put, Patch, Delete, GetCollection

# Invalid property type
Error: Property "age" has invalid type "int". Must be one of: string, integer, number, boolean, array, object

# Provider class not found
Error: Provider class "Pyz\Glue\Customer\Api\Backend\Provider\MissingProvider" does not exist

Advanced schema features

Custom operations

Define custom operations beyond standard CRUD:

operations:
  - type: Post
    uriTemplate: "/customers/{id}/activate"
    method: "POST"
    processor: "Pyz\\Glue\\Customer\\Api\\Backend\\Processor\\CustomerActivationProcessor"

Nested resources

Define relationships between resources:

properties:
  addresses:
    type: array
    description: "Customer addresses"
    items:
      type: object
      properties:
        street:
          type: string
        city:
          type: string

Security expressions

Add fine-grained security:

resource:
  security: "is_granted('ROLE_ADMIN')"
  securityPostDenormalize: "is_granted('EDIT', object)"

Generation commands

Basic generation

# Generate all configured API types
docker/sdk cli glue  api:generate

# Generate specific API type
docker/sdk cli glue  api:generate backend
docker/sdk cli glue  api:generate storefront

# Generate with options
docker/sdk cli glue  api:generate --dry-run           # Preview without writing
docker/sdk cli glue  api:generate --validate-only     # Only validate schemas
docker/sdk cli glue  api:generate --resource=customers  # Generate single resource

Output

Generating API resources for ApiType: backend

Discovering schema files...
Validating schemas... OK
Merging schemas... OK

Generating resources:
 10/10 [============================] 100%

Generated: 10 file(s)
Cache updated

Done!

Schema validation rules

The generator enforces these rules:

Required fields

Every resource must have:

  • name - Internal resource name
  • shortName - URL-friendly name
  • At least one operation
  • At least one property

Valid operation types

Only these operation types are allowed:

  • Get - Retrieve single resource
  • GetCollection - Retrieve collection
  • Post - Create resource
  • Put - Replace entire resource
  • Patch - Update partial resource
  • Delete - Delete resource

Valid property types

Only these property types are allowed:

  • string
  • integer
  • number
  • boolean
  • array
  • object

Provider/Processor validation

  • Provider/Processor classes must exist
  • Classes must implement correct interfaces
  • Namespaces must be valid PHP namespaces

Best practices

1. Use semantic naming

# ✅ Good
resource:
  name: Customers
  shortName: Customer

# ❌ Bad
resource:
  name: CustomerData
  shortName: cust

2. Document all properties

# ✅ Good
email:
  type: string
  description: "The customer's email address used for login and notifications"

# ❌ Bad
email:
  type: string

3. 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

4. Leverage schema merging

# Core: Define base properties
# src/Spryker/Customer/resources/api/backend/customer.yml
resource:
  name: Customers
  properties:
    email:
      type: string

# Project: Only override what's needed
# src/Pyz/Glue/Customer/resources/api/backend/customer.yml
resource:
  name: Customers
  properties:
    email:
      required: true  # ← Only the difference

5. Use readable/writable correctly

# Read-only fields (IDs, timestamps)
idCustomer:
  type: integer
  writable: false

# Write-only fields (passwords)
password:
  type: string
  readable: false

# Read-write fields (normal data)
email:
  type: string
  writable: true
  readable: true

Next steps