mhujer/jms-serializer-uuid
Adds JMS Serializer support for ramsey/uuid: serialize UUID objects as strings and deserialize string values back into UUID instances, so UUID fields work cleanly in your JMS Serializer mappings.
Install and inspect before trusting.
composer require mhujer/jms-serializer-uuid
composer show mhujer/jms-serializer-uuid # check the PHP / ramsey-uuid / jms-serializer constraints
The last release is from 2016, so Composer's constraint solver is your first obstacle. Look at vendor/mhujer/jms-serializer-uuid/src/ — the whole package is one SubscribingHandlerInterface implementation with a serialize/deserialize callback pair (~40 lines). That file is the documentation; read it first and note the exact class name and the type string it subscribes to.
Register the handler.
Standalone SerializerBuilder:
use JMS\Serializer\SerializerBuilder;
use Mhujer\JmsSerializer\Uuid\UuidHandler; // verify the FQCN in vendor/
$serializer = SerializerBuilder::create()
->addDefaultHandlers()
->configureHandlers(function ($handlers) {
$handlers->registerSubscribingHandler(new UuidHandler());
})
->build();
Symfony (JMS Serializer Bundle auto-collects the tag):
services:
Mhujer\JmsSerializer\Uuid\UuidHandler:
tags: ['jms_serializer.subscribing_handler']
Declare the type on every UUID property. This is the step people miss — JMS picks the handler from the declared type, not from the runtime object:
use JMS\Serializer\Annotation as Serializer;
class User
{
/** @Serializer\Type("Ramsey\Uuid\Uuid") */
private $id;
/** @Serializer\Type("array<Ramsey\Uuid\Uuid>") */
private $roleIds;
}
First use case: a REST endpoint returning {"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"} where the entity property is a Ramsey\Uuid\Uuid. GET proves serialization; POST with a UUID string proves deserialization and gives you a Uuid object in the hydrated DTO.
Round-trip test as the acceptance criterion. Write this before wiring anything else — it catches almost every failure mode (handler not registered, wrong type string, Ramsey version mismatch):
$json = $serializer->serialize($user, 'json');
self::assertStringContainsString('"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7"', $json);
$user = $serializer->deserialize($json, User::class, 'json');
self::assertInstanceOf(\Ramsey\Uuid\UuidInterface::class, $user->getId());
Collections work for free. array<Ramsey\Uuid\Uuid>, ArrayCollection of UUIDs, and nested DTOs all route each element through the same handler — as long as the generic type string matches the subscribed type exactly.
Nullable / optional fields. JMS short-circuits null before the handler is called, so "id": null round-trips fine. Empty string "" does not — it reaches the handler and Uuid::fromString('') throws. Decide early whether "" means null (normalize in a custom callback) or a 400.
Subclass the handler instead of patching vendor. The only two things you'll realistically want to change are the type list and validation:
final class AppUuidHandler extends UuidHandler
{
public static function getSubscribingMethods(): array
{
$methods = [];
foreach (['Ramsey\Uuid\Uuid', 'Ramsey\Uuid\UuidInterface'] as $type) {
foreach (['json', 'xml', 'yaml'] as $format) {
$methods[] = ['type' => $type, 'format' => $format, 'method' => 'serializeUuid'];
$methods[] = ['type' => $type, 'format' => $format, 'method' => 'deserializeUuid'];
}
}
return $methods;
}
}
(Keep the parent's method names; in JMS Serializer 3.x getSubscribingMethods() is static and returns an array without a direction key, which means "both directions".)
Register your subclass instead of the vendor class, never both — duplicate subscriptions for the same type/format make the winning callback order-dependent and effectively undefined.
API Platform 1.x/2.x with the JMS backend uses the same annotations, so this handler integrates with no extra work; the property-level @Type is the only contract.
If you control the codebase, consider replacing the dependency with a 30-line in-app handler. The maintenance surface is tiny:
public function deserialize(VisitorInterface $visitor, $data, array $type, Context $context)
{
if ($data === null || $data === '') {
return null;
}
try {
return \Ramsey\Uuid\Uuid::fromString((string) $data);
} catch (\InvalidArgumentException $e) {
throw new \JMS\Serializer\Exception\RuntimeException(
sprintf('Invalid UUID "%s"', $data), 0, $e
);
}
}
public function serialize(VisitorInterface $visitor, $uuid, array $type, Context $context): string
{
return $uuid->toString();
}
This also lets you type against UuidInterface and keep up with ramsey/uuid 4.x.
Ramsey\Uuid\Uuid concrete class of the 2.x/3.x era. ramsey/uuid 4.x returns UuidV1/UuidV4/LazyUuidFromString implementations of UuidInterface that are not instances of Uuid. If the handler's type hint is the concrete class, it silently never matches. Check vendor/ before assuming it works.{}. If a UUID property serializes to an empty JSON object ("id":{}), JMS fell through to the generic object visitor — the type string in @Type doesn't match any subscribed type. Same root cause for deserialization returning an unpopulated object.Ramsey\Uuid\Uuid ≠ Ramsey\Uuid\UuidInterface ≠ ramsey\uuid\uuid. Annotating an UuidInterface-typed property with @Type("Ramsey\Uuid\Uuid") is legitimate (the handler creates the concrete object), but the annotation string must be what the handler subscribes to.doctrine/annotations must be present for @HandlerCallback docblock annotations to be read. On JMS Serializer 3.x running without it (or with attributes configured), the callbacks may not be discovered at all. If your @Serializer\Type annotations work but UUIDs don't, this is the second thing to check after the type string.InvalidUuidStringException, which is not a JMS\Serializer\Exception\RuntimeException. Unwrapped, it escapes Symfony's jms_serializer error handling and returns a 500 instead of a clean 400. Always wrap it in your subclass.getSubscribingMethods() are handled. If you serialize csv or yaml in a batch export, UUIDs fall back to object serialization. Add formats via the subclass rather than assuming wildcard support.var_dump((new UuidHandler())->getSubscribingMethods());
and log inside serialize/deserialize to confirm the callback actually fires. If it fires but the output is wrong, the problem is in the callback; if it never fires, it's registration/type/annotation.new the handler per request in Symfony — let the bundle's compiler pass manage it as a shared service.ramsey/uuid and JMS Serializer in composer.json, and keep the round-trip test in your suite so a transitive upgrade that breaks the handler fails loudly.How can I help you explore Laravel packages today?