Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Uuid Normalizer Laravel Package

gbprod/uuid-normalizer

A Symfony Serializer normalizer for Ramsey UUIDs. Install via Composer, register UuidNormalizer and UuidDenormalizer as serializer.normalizer services, and serialize or deserialize UUIDs without "Not a time-based UUID" errors. Requires PHP 7.4+.

View on GitHub
Deep Wiki
Context7

Getting Started

This package ships two small normalizers that teach Symfony Serializer how to turn Ramsey\Uuid objects into strings and back again. That is all it does — there is no ServiceProvider, facade, config file, or Laravel integration. On a stock Laravel app it is not something you need, because Eloquent's uuid cast, HasUuids, and response()->json() already emit strings. It becomes useful the moment you introduce Symfony Serializer as a real encode/decode layer (typically for DTOs, hexagonal/DDD-style boundaries, or when porting a Symfony codebase into Laravel).

Minimal path if you do want it:

composer require symfony/serializer symfony/property-access symfony/property-info
composer require gbprod/uuid-normalizer

Then wire a Serializer manually in a service provider — Laravel has no autoconfiguration, so you must build the normalizer stack yourself and put the UUID normalizers ahead of ObjectNormalizer:

// app/Providers/SerializerServiceProvider.php
use GBProd\UuidNormalizer\UuidDenormalizer;
use GBProd\UuidNormalizer\UuidNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

public function register(): void
{
    $this->app->singleton(Serializer::class, fn () => new Serializer(
        [
            new UuidNormalizer(),
            new UuidDenormalizer(),
            new DateTimeNormalizer(),
            new ObjectNormalizer(),
        ],
        [new JsonEncoder()],
    ));
}

First use case: a DTO with a UUID property, serialized to JSON for an API response and deserialized from a request body:

final class OrderDto
{
    public function __construct(
        public readonly string $reference,
        public readonly \Ramsey\Uuid\UuidInterface $customerId,
    ) {}
}

$json = $serializer->serialize($dto, 'json');          // {"reference":"…","customerId":"9b2f…"}
$dto  = $serializer->deserialize($json, OrderDto::class, 'json');

If you are in Symfony, skip the provider: the classes are picked up by autoconfiguration when services.yaml has autoconfigure: true, otherwise tag both with serializer.normalizer as the README shows. Everything below assumes the manual Laravel wiring or an equivalent container binding.

Implementation Patterns

DTO boundary, not entity boundary. The normalizer operates on any UuidInterface it encounters in the object graph, including nested DTOs and arrays. Keep it on input/output DTOs and let your repository map DTO → Eloquent model. Serializing Eloquent models directly is where you get surprises (lazy-loaded relations, dates, model internals), and the UUID normalizer will not save you from that.

Constructor injection for denormalization. ObjectNormalizer uses the constructor when arguments are available, and UuidDenormalizer returns a Uuid instance for UuidInterface-typed parameters. Type-hint the interface, not a concrete subclass:

public function __construct(public readonly UuidInterface $id) {}

Request flow pattern. Decode once, denormalize to a DTO, then hand the DTO to a form request/validator or straight to an action. This keeps UUID conversion errors in one place instead of scattered Uuid::fromString() calls:

try {
    $dto = $this->serializer->deserialize(
        $request->getContent(),
        CreateOrder::class,
        'json'
    );
} catch (\Ramsey\Uuid\Exception\InvalidUuidStringException $e) {
    throw new HttpResponseException(response()->json(['message' => 'Invalid UUID'], 422));
}

Round-trip tests are the cheapest safety net. The package is two classes; unit-test your own DTOs with it directly rather than through HTTP:

$uuid = Uuid::uuid4();
$json = $serializer->serialize(new OrderDto('ref', $uuid), 'json');
$this->assertStringContainsString($uuid->toString(), $json);
$this->assertSame($uuid->toString(), $serializer->denormalize(json_decode($json, true), OrderDto::class)['customerId']->toString());

Symfony-side integration. If you are writing a package that must run on both Symfony and Laravel, register via serializer.normalizer tags with an explicit priority: 10 (or any positive value) so the UUID normalizers are consulted before ObjectNormalizer's fallback behaviour. With autoconfigure enabled, the classes already implement the right interfaces and are tagged automatically — do not double-register them or supportsNormalization() will be called twice per value.

Composition over extension. There is no configuration surface. If you need a different wire format (uppercase, base58, Uuid::fromString vs Uuid::fromBytes), write a five-line normalizer of your own that delegates to this one, rather than forking the package.

Gotchas and Tips

  • No Laravel integration, by design. Installing this will not change Model::toArray(), response()->json(), uuid casts, or HasUuids. If you are not already using symfony/serializer, this package adds nothing. With 0 dependents and two tiny classes under WTFPL, copying both classes into app/Support/Serializer/ is a defensible alternative to a dependency — especially if corporate policy chokes on the WTFPL licence.
  • Ordering is the number one failure mode. If ObjectNormalizer runs first, Ramsey UUIDs get reflected as objects (private properties, or an exception about time-based UUIDs from Uuid::getDateTime()-style introspection). Always place UuidNormalizer/UuidDenormalizer before ObjectNormalizer, and in Symfony verify the serializer.normalizer tag priority.
  • Denormalization target must match what the denormalizer supports. Type-hint UuidInterface or Ramsey\Uuid\Uuid. If you hint a custom subclass, expect to receive the base Uuid instance (or a non-supporting return), because the denormalizer constructs a generic UUID from the string.
  • Nullable vs empty string. ?UuidInterface with null is fine — the Serializer short-circuits. But "customerId": "" reaches the denormalizer and throws. Normalize empty strings to null before denormalizing, or add a small sanitizing normalizer.
  • Invalid strings throw, they don't return null. InvalidUuidStringException (ramsey/uuid v4; the exception hierarchy differs in v3) bubbles out of deserialize(). Map it to a 422 in bootstrap/app.php's exception handler, otherwise clients see a 500.
  • Binary storage is out of scope. This normalizes to and from the canonical 36-character string. If your schema uses uuid_binary/16-byte columns, you still need Uuid::fromBytes/getBytes at the persistence layer.
  • Serialization groups and #[Ignore] are not honoured by these normalizers. A UUID property is always emitted. If a field must disappear in some contexts, use separate DTOs rather than trying to configure the normalizer.
  • Symfony cache. In Symfony, serializer metadata and the normalizer list are compiled into the container. After registering new normalizers, bin/console cache:clear. In Laravel, if you bind Serializer as a singleton, remember it captures the normalizer array at first resolution; rebinding in tests ($this->app->forgetInstance(Serializer::class)) is usually needed.
  • Debugging a silent non-invocation. Dump $serializer->supportsNormalization($uuid, 'json') and supportsDenormalization($string, UuidInterface::class, 'json'). If both are true but the output is wrong, another normalizer is winning the priority race. If false, you have a version mismatch — check composer why ramsey/uuid and that the package's constraint matches your Ramsey major (v3 vs v4 exception and factory differences).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels