
Symfony:api Platform Serialization Skill
- 400 installs
- 190 repo stars
- Updated August 6, 2026
- makfly/superpowers-symfony
symfony:api-platform-serialization is a Claude Code skill that configures API Platform serializers, normalization groups, custom normalizers, and JSON-LD output for Symfony resources exposed to clients and integrators.
About
symfony:api-platform-serialization is a makfly/superpowers-symfony agent skill for delivering explicit API Platform contracts in Symfony with operation-level serialization boundaries. The skill guides normalization groups, custom normalizers, DTO and resource mapping, JSON-LD output, and operation-specific validation and security constraints so runtime payloads match documentation. It targets Symfony 7.4 LTS and 8.x with API Platform v4 from the parent plugin, which bundles seven API Platform skills and seven specialized subagents. Developers reach for symfony:api-platform-serialization when internal entity fields leak into responses, docs drift from runtime behavior, or payloads need policy-safe evolution across releases. The workflow produces changed resources, DTOs, providers, processors, and functional verification results for happy and negative API paths.
- Normalization and denormalization groups
- Custom normalizer patterns
- JSON-LD and OpenAPI alignment
- Versioned API payloads
- Circular reference handling
Symfony:Api Platform Serialization by the numbers
- 400 all-time installs (skills.sh)
- Ranked #1,088 of 4,492 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/makfly/superpowers-symfony --skill symfonyapi-platform-serializationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 400 |
|---|---|
| repo stars | ★ 190 |
| Last updated | August 6, 2026 |
| Repository | makfly/superpowers-symfony ↗ |
How do you configure API Platform serialization groups in Symfony?
Configure API Platform serializers, normalization groups, custom normalizers, and JSON-LD output for Symfony resources exposed to clients and third-party integrators.
Who is it for?
Symfony backend developers exposing API Platform v4 resources who need explicit serialization boundaries and JSON-LD contracts for external integrators.
Skip if: Teams building plain Symfony controllers without API Platform or projects that only need Doctrine entity mapping without REST serialization concerns.
When should I use this skill?
A developer asks to add normalization groups, custom normalizers, DTO serialization, or stop implicit entity field exposure in API Platform Symfony APIs.
What you get
Updated API Platform resources, DTOs, normalizers, and serialization group mappings with documented contract and security decisions plus verification results.
- serialization group config
- custom normalizers
- DTO and resource mappings
By the numbers
- Part of superpowers-symfony with seven API Platform skills and seven specialized subagents
- Targets Symfony 7.4 LTS, 8.x, and API Platform v4
Files
Api Platform Serialization (Symfony)
Use when
- Designing or evolving API Platform contracts and operations.
- Aligning serialization, validation, and security behavior.
Default workflow
1. Define operation-level contract and payload boundaries. 2. Implement resource/DTO/provider/processor changes with explicit mapping. 3. Apply operation-specific validation and security constraints. 4. Validate functional behavior across happy and negative paths.
Guardrails
- Keep API contract explicit and version-aware.
- Avoid exposing internal entity fields implicitly.
- Prevent drift between docs and actual serialization.
Progressive disclosure
- Use this file for execution posture and risk controls.
- Open references when deep implementation details are needed.
Output contract
- API artifacts changed (resource/DTO/provider/processor).
- Contract/security decisions and rationale.
- Functional verification results.
References
reference.mddocs/complexity-tiers.md
Reference
API Platform Serialization
Namespace note (Symfony 7+ / API Platform v4). Serializer attributes live underSymfony\Component\Serializer\Attribute\*(renamed fromAnnotation\*):Groups,Ignore,SerializedName,SerializedPath,Context,MaxDepth,DiscriminatorMap. The oldAnnotation\*names still resolve but new code should useAttribute\*. The core serialization model (groups, contexts,#[ApiProperty]) is stable v3→v4.
Property-level context — #[Context] (Symfony 7.0+)
Apply normalizer options per property (e.g. a date format) without a custom normalizer:
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Event
{
#[Groups(['event:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public \DateTimeImmutable $startsAt;
// group-scoped context: only applies when serializing with the 'extended' group
#[Context(normalizationContext: [DateTimeNormalizer::FORMAT_KEY => \DateTimeInterface::RFC3339], groups: ['extended'])]
public \DateTimeImmutable $endsAt;
}Relations as IRIs — readableLink / writableLink
By default related resources are embedded only if they share a serialization group; otherwise they render as IRIs. Force the IRI form (never embed) with #[ApiProperty]:
use ApiPlatform\Metadata\ApiProperty;
class Post
{
#[ApiProperty(readableLink: false, writableLink: false)]
#[Groups(['post:read'])]
private User $author; // always serialized/accepted as an IRI, never embedded
}Serialization Groups
Basic Groups
<?php
// src/Entity/User.php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['user:list']],
),
new Get(
normalizationContext: ['groups' => ['user:read']],
),
new Post(
denormalizationContext: ['groups' => ['user:create']],
),
new Put(
denormalizationContext: ['groups' => ['user:update']],
),
],
)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['user:list', 'user:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['user:list', 'user:read', 'user:create', 'user:update'])]
private string $name;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['user:read', 'user:create'])] // Not in list, not updatable
private string $email;
#[ORM\Column]
#[Groups(['user:create'])] // Write-only
private string $password;
#[ORM\Column]
#[Groups(['user:read'])] // Read-only, not in list
private \DateTimeImmutable $createdAt;
#[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'author')]
#[Groups(['user:read'])] // Only on detail view
private Collection $posts;
}Nested Serialization
<?php
// src/Entity/Post.php
#[ApiResource(
normalizationContext: ['groups' => ['post:read']],
)]
class Post
{
#[Groups(['post:read', 'user:read'])]
private ?int $id = null;
#[Groups(['post:read', 'user:read'])]
private string $title;
#[Groups(['post:read'])] // Full content only on post detail
private string $content;
#[ORM\ManyToOne(targetEntity: User::class)]
#[Groups(['post:read'])]
private User $author;
}
// src/Entity/User.php
class User
{
// When user:read includes posts, only id and title are shown
#[Groups(['user:read'])]
private Collection $posts;
}Custom Normalizers
Add Computed Fields
<?php
// src/Serializer/Normalizer/UserNormalizer.php
namespace App\Serializer\Normalizer;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class UserNormalizer implements NormalizerInterface
{
public function __construct(
#[Autowire(service: 'serializer.normalizer.object')]
private NormalizerInterface $normalizer,
) {}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
/** @var User $object */
$data = $this->normalizer->normalize($object, $format, $context);
// Add computed fields
$data['fullName'] = $object->getFirstName() . ' ' . $object->getLastName();
$data['postCount'] = $object->getPosts()->count();
$data['isVerified'] = $object->getVerifiedAt() !== null;
return $data;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof User;
}
public function getSupportedTypes(?string $format): array
{
return [User::class => true];
}
}Conditional Serialization
<?php
// src/Serializer/Normalizer/PostNormalizer.php
namespace App\Serializer\Normalizer;
use App\Entity\Post;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PostNormalizer implements NormalizerInterface
{
public function __construct(
private NormalizerInterface $normalizer,
private Security $security,
) {}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
/** @var Post $object */
$data = $this->normalizer->normalize($object, $format, $context);
// Add admin-only fields
if ($this->security->isGranted('ROLE_ADMIN')) {
$data['internalNotes'] = $object->getInternalNotes();
$data['moderationStatus'] = $object->getModerationStatus();
}
// Add owner-only fields
if ($this->security->getUser() === $object->getAuthor()) {
$data['analytics'] = [
'views' => $object->getViewCount(),
'engagement' => $object->getEngagementRate(),
];
}
return $data;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Post;
}
public function getSupportedTypes(?string $format): array
{
return [Post::class => true];
}
}Context Builders
Dynamic Groups Based on User
<?php
// src/Serializer/UserContextBuilder.php
namespace App\Serializer;
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
final class UserContextBuilder implements SerializerContextBuilderInterface
{
public function __construct(
private SerializerContextBuilderInterface $decorated,
private Security $security,
) {}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
// Add admin group for admin users
if ($this->security->isGranted('ROLE_ADMIN')) {
$context['groups'][] = 'admin:read';
}
// Add owner group when viewing own resources
$resourceClass = $context['resource_class'] ?? null;
if ($resourceClass && $this->isOwner($request, $resourceClass)) {
$context['groups'][] = 'owner:read';
}
return $context;
}
private function isOwner(Request $request, string $resourceClass): bool
{
// Implementation depends on your resource structure
return false;
}
}Register as decorator:
# config/services.yaml
services:
App\Serializer\UserContextBuilder:
decorates: 'api_platform.serializer.context_builder'Max Depth
Prevent circular references:
use Symfony\Component\Serializer\Attribute\MaxDepth;
class User
{
#[MaxDepth(1)]
#[Groups(['user:read'])]
private Collection $posts;
}
class Post
{
#[MaxDepth(1)]
#[Groups(['post:read'])]
private User $author;
}Enable in configuration:
#[ApiResource(
normalizationContext: [
'groups' => ['post:read'],
'enable_max_depth' => true,
],
)]
class Post { /* ... */ }Ignore Properties
use Symfony\Component\Serializer\Attribute\Ignore;
class User
{
#[Ignore]
private string $password;
#[Ignore]
private string $resetToken;
}Best Practices
1. Use groups consistently: entity:operation naming convention 2. Separate read/write groups: Different fields for input/output 3. Limit nested depth: Use MaxDepth to prevent deep nesting 4. Computed fields in normalizers: Keep entities clean 5. Context builders for dynamic groups: Role-based field access 6. Document with OpenAPI: Groups affect schema generation
Skill Operating Checklist
Design checklist
- Confirm operation boundaries and invariants first.
- Minimize scope while preserving contract correctness.
- Test both happy path and negative path behavior.
Validation commands
- ./vendor/bin/phpunit --filter=Api
- ./vendor/bin/phpstan analyse
- php bin/console debug:router
Failure modes to test
- Invalid payload or forbidden actor.
- Boundary values / not-found cases.
- Retry or partial-failure behavior for async flows.
Related skills
How it compares
Pick symfony:api-platform-serialization over generic Symfony skills when API Platform response shapes, normalization groups, or JSON-LD contracts need explicit operation-level control.
FAQ
What does symfony:api-platform-serialization configure?
symfony:api-platform-serialization configures API Platform serializers, normalization groups, custom normalizers, and JSON-LD output for Symfony resources. The skill enforces operation-level payload boundaries so only intended fields appear per API operation.
Which Symfony versions does symfony:api-platform-serialization target?
symfony:api-platform-serialization belongs to makfly/superpowers-symfony, which targets Symfony 7.4 LTS and 8.x with API Platform v4. The skill keeps serialization contracts version-aware and aligned with runtime behavior.
What artifacts does symfony:api-platform-serialization produce?
symfony:api-platform-serialization outputs changed API Platform resources, DTOs, providers, processors, contract and security rationale, and functional verification results across happy and negative request paths.