Skip to main content
Version: Thelia 3

Serialization Groups

Serialization groups control which fields are included in API responses and accepted in requests. The same resource can then expose different data depending on the context.

Group naming convention

Thelia uses a consistent naming pattern:

{context}:{resource}:{operation}

Examples:

  • admin:product:read - Admin reading product data
  • admin:product:write - Admin writing product data
  • front:product:read - Public front-office reading
  • front:product:read:single - Front-office single item (more detail)

Standard groups

Admin groups

GroupPurpose
admin:*:readAdmin collection and item reads
admin:*:read:singleAdditional fields for single item
admin:*:writeAdmin create/update operations
admin:*:write:updateAdditional fields for updates only

Front groups

GroupPurpose
front:*:readPublic collection and item reads
front:*:read:singleAdditional fields for single item
front:*:writePublic write operations (if any)

Defining groups on resources

Resource constants

Define groups as class constants:

class Product extends AbstractTranslatableResource
{
public const GROUP_ADMIN_READ = 'admin:product:read';
public const GROUP_ADMIN_READ_SINGLE = 'admin:product:read:single';
public const GROUP_ADMIN_WRITE = 'admin:product:write';
public const GROUP_ADMIN_WRITE_UPDATE = 'admin:product:write:update';
public const GROUP_FRONT_READ = 'front:product:read';
public const GROUP_FRONT_READ_SINGLE = 'front:product:read:single';

// ...
}

Applying groups to properties

// Always visible to admin
#[Groups([self::GROUP_ADMIN_READ])]
public ?int $id = null;

// Visible to admin and front
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_FRONT_READ])]
public string $ref;

// Writable by admin only
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_ADMIN_WRITE])]
public bool $visible;

// Only in single item responses (not collections)
#[Groups([self::GROUP_ADMIN_READ_SINGLE, self::GROUP_FRONT_READ_SINGLE])]
public array $featureProducts = [];

// Only writable during updates (not create)
#[Groups([self::GROUP_ADMIN_WRITE_UPDATE])]
public ?\DateTime $updatedAt = null;

Applying groups to operations

Normalization context (output)

#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/admin/products',
normalizationContext: ['groups' => ['admin:product:read']],
),
new Get(
uriTemplate: '/admin/products/{id}',
normalizationContext: ['groups' => [
'admin:product:read',
'admin:product:read:single',
]],
),
],
)]

Denormalization context (input)

#[ApiResource(
operations: [
new Post(
uriTemplate: '/admin/products',
denormalizationContext: ['groups' => ['admin:product:write']],
),
new Put(
uriTemplate: '/admin/products/{id}',
denormalizationContext: ['groups' => [
'admin:product:write',
'admin:product:write:update',
]],
),
],
)]

Practical examples

Collection vs single item

class Product extends AbstractTranslatableResource
{
// Included in collection AND single
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_FRONT_READ])]
public ?int $id = null;

#[Groups([self::GROUP_ADMIN_READ, self::GROUP_FRONT_READ])]
public string $ref;

// ONLY in single item (reduces collection payload)
#[Groups([self::GROUP_ADMIN_READ_SINGLE, self::GROUP_FRONT_READ_SINGLE])]
public array $featureProducts = [];

#[Groups([self::GROUP_ADMIN_READ_SINGLE])]
public array $images = [];
}

Collection response (JSON, default):

[
{"id": 1, "ref": "PROD-001"},
{"id": 2, "ref": "PROD-002"}
]

Single item response:

{
"id": 1,
"ref": "PROD-001",
"featureProducts": [...],
"images": [...]
}

Admin vs front

class Customer extends AbstractTranslatableResource
{
// Visible everywhere
#[Groups([
self::GROUP_ADMIN_READ,
self::GROUP_FRONT_READ,
])]
public ?int $id = null;

// Admin only - sensitive data
#[Groups([self::GROUP_ADMIN_READ])]
public string $email;

// Admin only - internal notes
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_ADMIN_WRITE])]
public ?string $internalNotes = null;

// Public profile
#[Groups([
self::GROUP_ADMIN_READ,
self::GROUP_FRONT_READ,
])]
public string $firstname;
}

Create vs update

class Product extends AbstractTranslatableResource
{
// Writable on create and update
#[Groups([self::GROUP_ADMIN_WRITE])]
public string $ref;

// Only settable on create (not updateable)
#[Groups([self::GROUP_ADMIN_WRITE])]
// Note: Use validation to prevent updates
public int $creatorId;

// Only on updates
#[Groups([self::GROUP_ADMIN_WRITE_UPDATE])]
public bool $forceReindex = false;
}

Cross-resource groups

To include data from a related resource:

class OrderProduct
{
public const GROUP_ADMIN_READ = 'admin:order_product:read';

#[Groups([
self::GROUP_ADMIN_READ,
Order::GROUP_ADMIN_READ, // Also include when reading Order
])]
public ?int $id = null;
}

class Order
{
#[Relation(targetResource: OrderProduct::class)]
#[Groups([self::GROUP_ADMIN_READ_SINGLE])]
public array $orderProducts = [];
}

Validation groups

Validation can be tied to serialization groups:

use Symfony\Component\Validator\Constraints\NotBlank;

#[NotBlank(groups: [self::GROUP_ADMIN_WRITE])]
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_ADMIN_WRITE])]
public string $ref;

Best practices

1. Minimize collection payload

Include only essential fields in collections:

// Good: Collections are lean
#[Groups([self::GROUP_ADMIN_READ])]
public ?int $id = null;

#[Groups([self::GROUP_ADMIN_READ])]
public string $ref;

// Heavy data only in single
#[Groups([self::GROUP_ADMIN_READ_SINGLE])]
public array $allRelatedData = [];

2. Separate admin and front

Never expose admin-only data on the front:

// Bad: Admin data exposed to front
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_FRONT_READ])]
public ?string $internalNotes = null;

// Good: Admin only
#[Groups([self::GROUP_ADMIN_READ])]
public ?string $internalNotes = null;

3. Use constants

Use constants rather than string literals to keep group names consistent:

// Good
#[Groups([self::GROUP_ADMIN_READ, self::GROUP_FRONT_READ])]

// Bad (typos, inconsistency)
#[Groups(['admin:product:read', 'front:product:read'])]

4. Document special groups

Comment non-obvious group usage:

/**
* Only included when viewing from Order context.
*/
#[Groups([
self::GROUP_ADMIN_READ,
Order::GROUP_ADMIN_READ_SINGLE, // Include in order details
])]
public array $details = [];

Debugging groups

Check which groups are active:

// In a custom normalizer or listener
$context['groups'] // Contains active groups

Use API Platform's Swagger/OpenAPI documentation to verify field visibility.

Next steps