Core: Can't serialize Ramsey\Uuid\Uuid identifier

Created on 27 Jan 2017  路  19Comments  路  Source: api-platform/core

When api-platform try to serialize my entity, I get the Resource "Ramsey\Uuid\Uuid" not found. error. It seems to only happen on deep serialization. In the following stack trace, I'm exposing AppBundle\Entity\Theme entities which have a bidirectional ManyToOne to a AppBundle\Entity\Category.

Also, I tried to expose my AppBundle\Entity\User entities which use the same identifier configuration and serialization works well when using application/json format but not application/ld+json format.

Stack trace:

[1] ApiPlatform\Core\Exception\ResourceClassNotFoundException: Resource "Ramsey\Uuid\Uuid" not found.
    at n/a
        in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php line 72

    at ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory->handleNotFound(null, 'Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php line 50

    at ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/ShortNameResourceMetadataFactory.php line 35

    at ApiPlatform\Core\Metadata\Resource\Factory\ShortNameResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/OperationResourceMetadataFactory.php line 35

    at ApiPlatform\Core\Metadata\Resource\Factory\OperationResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Resource/Factory/CachedResourceMetadataFactory.php line 53

    at ApiPlatform\Core\Metadata\Resource\Factory\CachedResourceMetadataFactory->create('Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php line 156

    at ApiPlatform\Core\Metadata\Property\Factory\SerializerPropertyMetadataFactory->getEffectiveSerializerGroups(array(), 'Ramsey\\Uuid\\Uuid')
        in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php line 51

    at ApiPlatform\Core\Metadata\Property\Factory\SerializerPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes', array())
        in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php line 48

    at ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes', array())
        in /var/www/vendor/api-platform/core/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php line 53

    at ApiPlatform\Core\Metadata\Property\Factory\CachedPropertyMetadataFactory->create('Ramsey\\Uuid\\Uuid', 'bytes')
        in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php line 134

    at ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter->getIdentifiersFromItem(object(Category))
        in /var/www/vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php line 83

    at ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter->getIriFromItem(object(Category))
        in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 420

    at ApiPlatform\Core\Serializer\AbstractItemNormalizer->normalizeRelation(object(PropertyMetadata), object(Category), 'AppBundle\\Entity\\Category', 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
        in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 397

    at ApiPlatform\Core\Serializer\AbstractItemNormalizer->getAttributeValue(object(Theme), 'categorie', 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
        in /var/www/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php line 78

    at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true, 'cache_key' => '9db9240ee742c4b9577d52a00843bee3', 'circular_reference_limit' => array('00000000040ec8b30000000028ca7688' => 1)))
        in /var/www/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php line 84

    at ApiPlatform\Core\Serializer\AbstractItemNormalizer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json', 'api_sub_level' => true, 'api_normalize' => true))
        in /var/www/vendor/symfony/serializer/Serializer.php line 142

    at Symfony\Component\Serializer\Serializer->normalize(object(Theme), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
        in /var/www/vendor/symfony/serializer/Serializer.php line 152

    at Symfony\Component\Serializer\Serializer->normalize(object(Paginator), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
        in /var/www/vendor/symfony/serializer/Serializer.php line 115

    at Symfony\Component\Serializer\Serializer->serialize(object(Paginator), 'json', array('collection_operation_name' => 'get', 'resource_class' => 'AppBundle\\Entity\\Theme', 'request_uri' => '/api/themes.json'))
        in /var/www/vendor/api-platform/core/src/EventListener/SerializeListener.php line 64

    at ApiPlatform\Core\EventListener\SerializeListener->onKernelView(object(GetResponseForControllerResultEvent), 'kernel.view', object(TraceableEventDispatcher))
        in  line 

    at call_user_func(array(object(SerializeListener), 'onKernelView'), object(GetResponseForControllerResultEvent), 'kernel.view', object(TraceableEventDispatcher))
        in /var/www/vendor/symfony/event-dispatcher/Debug/WrappedListener.php line 106

    at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(GetResponseForControllerResultEvent), 'kernel.view', object(ContainerAwareEventDispatcher))
        in  line 

    at call_user_func(object(WrappedListener), object(GetResponseForControllerResultEvent), 'kernel.view', object(ContainerAwareEventDispatcher))
        in /var/www/vendor/symfony/event-dispatcher/EventDispatcher.php line 174

    at Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(array(object(WrappedListener), array(object(ValidateListener), 'onKernelView'), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.view', object(GetResponseForControllerResultEvent))
        in /var/www/vendor/symfony/event-dispatcher/EventDispatcher.php line 43

    at Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.view', object(GetResponseForControllerResultEvent))
        in /var/www/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php line 136

    at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch('kernel.view', object(GetResponseForControllerResultEvent))
        in /var/www/vendor/symfony/http-kernel/HttpKernel.php line 158

    at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
        in /var/www/vendor/symfony/http-kernel/HttpKernel.php line 68

    at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
        in /var/www/vendor/symfony/http-kernel/Kernel.php line 168

    at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
        in /var/www/web/app_dev.php line 13

Identifier configuration:

    /**
     * @ORM\Id
     * @ORM\Column(type="uuid")
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
     *
     * @var Uuid
     */
    protected $id;

    public function getId()
    {
        return $this->id;
    }

    public function setId(Uuid $id)
    {
        $this->id = $id;
    }
bug

Most helpful comment

@fbourigault OK I've found a solution.

According to your comment, it's not recommended to use objects as identifiers. Now I use a string identifier, but still use Ramsey as generator (overridden):

// src/AppBundle/Entity/Foo.php

class Foo
{
    /**
     * @var string
     *
     * @ORM\Id
     * @ORM\Column(type="string")
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="AppBundle\Doctrine\Generator\UuidGenerator")
     */
    private $id;

And the UuidGenerator overridden:

// src/AppBundle/Doctrine/Generator/UuidGenerator.php

namespace AppBundle\Doctrine\Generator;

use Doctrine\ORM\EntityManager;
use Ramsey\Uuid\Doctrine\UuidGenerator as BaseUuidGenerator;
use Ramsey\Uuid\Uuid;

class UuidGenerator extends BaseUuidGenerator
{
    /**
     * {@inheritdoc}
     */
    public function generate(EntityManager $em, $entity)
    {
        /** @var Uuid $uuid */
        $uuid = parent::generate($em, $entity);

        return $uuid->toString();
    }
}

Hope this solution could help if other people have this issue 馃槂

All 19 comments

@fbourigault did you try to persist such entity manually? Ramsey\Uuid\Uuid is an object, so unless you have the appropriate type for it (which you either do yourself it's relatively simple or there might be a lib for it) Doctrine has no idea on how to persist it, cf. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html

I use https://github.com/ramsey/uuid-doctrine to get an uuid type for doctrine. I use this library with a few projects, I never had issues with serialization but it's the first time I use Ramsey\Uuid\Uuid with api-platform. I also have a normalizer and a normalizer in my application. From the stack trace, I see that api-plaform tries to get an IRI because an object is encountered. But shouldn't this object have been normalized as a string before. By saying it I really need to check normalizers priorities! Stay tuned :)

EDIT: Normalizer priorities does not change anything.

This is related to #916.

@api-platform/core-team I think we need to make fixing this a priority.

I start working on that.

This can be closed @fbourigault @dunglas ?

@fbourigault Did you find a fix? Can you share how please?

I use string instead. See https://github.com/api-platform/core/pull/927#issuecomment-276894661.

@fbourigault OK I've found a solution.

According to your comment, it's not recommended to use objects as identifiers. Now I use a string identifier, but still use Ramsey as generator (overridden):

// src/AppBundle/Entity/Foo.php

class Foo
{
    /**
     * @var string
     *
     * @ORM\Id
     * @ORM\Column(type="string")
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="AppBundle\Doctrine\Generator\UuidGenerator")
     */
    private $id;

And the UuidGenerator overridden:

// src/AppBundle/Doctrine/Generator/UuidGenerator.php

namespace AppBundle\Doctrine\Generator;

use Doctrine\ORM\EntityManager;
use Ramsey\Uuid\Doctrine\UuidGenerator as BaseUuidGenerator;
use Ramsey\Uuid\Uuid;

class UuidGenerator extends BaseUuidGenerator
{
    /**
     * {@inheritdoc}
     */
    public function generate(EntityManager $em, $entity)
    {
        /** @var Uuid $uuid */
        $uuid = parent::generate($em, $entity);

        return $uuid->toString();
    }
}

Hope this solution could help if other people have this issue 馃槂

@vincentchalamon could this be added to the original library? You're basically just casting the UUID object to a string. Is there an upside of generate() returning an object instead?

/cc @ramsey (sorry for the mention, but you are the domain expert here)

The Uuid class allows more operations than a regular string (see https://github.com/ramsey/uuid/blob/master/src/Uuid.php). There is no reason to return a string and lose those features.

@vincentchalamon's solution is simple enough. I would just use composition instead of heritage in the custom generator.

@dunglas of course the UUID object has more features than a string. :)

I meant, is it valuable for it to return the object in this context? If using it as an ID, you're most often using it as an opaque value, you're not really interested in the content, same as auto-increments. Having it be an object is nice, but you run into all sorts of trouble, like this one or, for example, not being able to use the stock Symfony serializer on an entity (because the serializer will trigger an exception in getDateTime() which is not available with UUIDv4 used here).

If these are some of quite severe downsides, what are the upsides? For example, if you want to access UUID features, you can always toString() it back. I guess, UUID validation?

None of this is really related to this repo (sorry about that), but the interaction between these libs is important, why not make it as simple as possible.

How does the stock Symfony serializer work? The Uuid object implements JsonSerializable and Serializable, so if Symfony is using either of these methods to serialize/unserialize data, everything should Just Work.

See: https://github.com/ramsey/uuid/blob/master/src/Uuid.php#L178-L214

That's weird. I don't think it's calling it.

I've setup the serializer manually (because #1091), as shown in the docs:

    $normalizers = [new \Symfony\Component\Serializer\Normalizer\ObjectNormalizer()];
    $encoders = [new \Symfony\Component\Serializer\Encoder\JsonEncode()];
    $this->serializer = new \Symfony\Component\Serializer\Serializer($normalizers, $encoders);

If I $body = $this->serializer->serialize($entity, 'json'); (entity is a random Doctrine entity properly using RamseyUuid as PK), I get:

[1] Ramsey\Uuid\Exception\UnsupportedOperationException: Not a time-based UUID
at n/a
    in /app/vendor/ramsey/uuid/src/Uuid.php line 313

at Ramsey\Uuid\Uuid->getDateTime()
    in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 483

at Symfony\Component\PropertyAccess\PropertyAccessor->readProperty(array(object(Uuid)), 'dateTime')
    in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 406

at Symfony\Component\PropertyAccess\PropertyAccessor->readPropertiesUntil(array(object(Uuid)), object(PropertyPath), 1, true)
    in /app/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 178

at Symfony\Component\PropertyAccess\PropertyAccessor->getValue(object(Uuid), object(PropertyPath))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php line 93

at Symfony\Component\Serializer\Normalizer\ObjectNormalizer->getAttributeValue(object(Uuid), 'dateTime', 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1, '000000004ee02dca0000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 79

at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(Uuid), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1, '000000004ee02dca0000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142

at Symfony\Component\Serializer\Serializer->normalize(object(Uuid), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 97

at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(User), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1, '000000004ee02dcf0000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142

at Symfony\Component\Serializer\Serializer->normalize(object(User), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php line 97

at Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->normalize(object(OauthAccessToken), 'json', array('cache_key' => '6de9eb2685609b8387658584987a05b0', 'circular_reference_limit' => array('000000004ee02cb30000000070ad8799' => 1)))
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 142

at Symfony\Component\Serializer\Serializer->normalize(object(OauthAccessToken), 'json', array())
    in /app/vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php line 115

at Symfony\Component\Serializer\Serializer->serialize(object(OauthAccessToken), 'json')
    in /app/src/AppBundle/Queue/EventConverter/DoctrineEventConverter.php line 146

...etc.

Yeah, I expected the ObjectNormalizer to figure out it needs to use JsonSerializableNormalizer, it doesn't. Never mind, PEBKAC.

The point about returning a string instead of object from generate() by default still stand, though.

We can add __toString support in getIdentifiersFromItem.

@teohhanhui @dunglas
Maybe it's too early to ask but are there any news for this issue? Yesterday I encountered very similar problem - I have an object identifier

final class MessageId extends Identity
{
    // parent Identity has constructor accepting id as a string
    public static function generate(): MessageId
    {
        return new self((string)Uuid::uuid4());
    }
}

The stacktrace is almost identical to the one sent by @fbourigault but the exception message says that Resource of ...\MessageId not found.

Thanks.

@eps90 Does MessageId implement __toString? If yes, then my proposed solution above (yet to be implemented) should fix it.

@teohhanhui Yes it does. Great, I'll be waiting then!

@teohhanhui is this what you had in mind?

@@ -126,6 +126,9 @@ final class IriConverter implements IriConverterInterface

             if (!is_object($identifiers[$propertyName])) {
                 continue;
+            } else if (method_exists($identifiers[$propertyName], '__toString')) {
+                $identifiers[$propertyName] = (string) $identifiers[$propertyName];
+                continue;
             }

             $relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);

Was this page helpful?
0 / 5 - 0 ratings