schmittjoh / serializer

Library for (de-)serializing data of any complexity (supports JSON, and XML)
http://jmsyst.com/libs/serializer
MIT License
2.32k stars 588 forks source link

Detect extra fields in deserialization #830

Open enumag opened 6 years ago

enumag commented 6 years ago

I need to deserialize a json into a class. It works fine but I noticed that when I add a field to the json that does not exist on the class it's silently ignored. I need to know about it somehow to throw a correct exception - such data are considered as invalid for my use case.

goetas commented 6 years ago

Hi, thanks for the request. Currently there is nothing in the core that allows something similar. Maybe a pre/post serialization listener can do the work for you here...

enumag commented 6 years ago

That's quite surprising. This seems like a very basic feature that everybody should need when deserializing to a class.

How do I get a list of fields that deserializer is able to fill in the handler?

goetas commented 6 years ago

PropertyMetadata gives you the type of data you are going to unserialize. from it you can infer what you need

enumag commented 6 years ago

And how do I create a handler that is used always? When I remove the type key from getSubscribingMethods I get Each method returned from getSubscribingMethods of service "MyHandler" must have a "type", and "format" attribute.

goetas commented 6 years ago

:( handlers are always per "type"...

enumag commented 6 years ago

Then it's kind of unsolveable with a handler... any tip where in the code of Serializer this should be handled + how the user should configure that they want to use this behavior? I want to send a PR but don't know where to begin.

goetas commented 6 years ago

A nice thing to do can be to allow listeners to be triggered on any type... and that should be something here

enumag commented 6 years ago

According to documentation event subscribers can work on any type already.

goetas commented 6 years ago

what about your message from one hour ago?

And how do I create a handler that is used always? When I remove the type key from getSubscribingMethods I get Each method returned from getSubscribingMethods of service "MyHandler" must have a "type", and "format" attribute.

enumag commented 6 years ago

The link you sent is not about handlers but about Events, right? In event subscribers class is optional. Should I use events instead of handler then?

goetas commented 6 years ago

My bad, on my first message I wanted to write :

Hi, thanks for the request. Currently there is nothing in the core that allows something similar. Maybe a pre/post serialization listener can do the work for you here...

I've edited the comment for clarity. So you have to implement and event listener.

enumag commented 6 years ago

Ok, I'll try it. Thanks.

enumag commented 6 years ago

I think it will work... This is what I have so far. What do you think?

<?php declare(strict_types = 1);

namespace App\Web;

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;

class MyEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            [
                'event' => 'serializer.pre_deserialize',
                'method' => 'onPreDeserialize',
            ],
        ];
    }

    public function onPreDeserialize(PreDeserializeEvent $event)
    {
        $metadata = $event->getContext()->getMetadataFactory()->getMetadataForClass($event->getType()['name']);
        $attributes = array_keys($event->getData());
        $properties = array_keys($metadata->propertyMetadata);
        $extraAttributes = array_diff($attributes, $properties);

        if ($extraAttributes) {
            throw new \Exception();
        }
    }
}
goetas commented 6 years ago

array_keys($event->getData()); will work only for json/yml but the general idea is correct

pdugas commented 6 years ago

Ran into this today. I expected invalid properties to produce an error when deserialized. I see from this issue that's not the expected behavior but I agree with the OP that it isn't unreasonable to expect. Perhaps a configuration setting to enable this like Symfony's serializer does?

See https://symfony.com/doc/current/components/serializer.html#deserializing-an-object.

douglasjam commented 5 years ago

I ran into this issue today as well, I agree that allow_extra_attributes option in the context would be awesome.

4n70w4 commented 4 years ago

The same problem. At the development stage, I would like to receive exceptions in case of differences in the classes and structure of the json.

4n70w4 commented 4 years ago

@enumag your solution does not work if @Discriminator is used. https://jmsyst.com/libs/serializer/master/reference/annotations#discriminator

Does anyone have a workaround?

4n70w4 commented 4 years ago

Workaround for @Discriminator

<?php declare(strict_types = 1);

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;

class JmsSerializerEventSubscriber implements EventSubscriberInterface {

    public static function getSubscribedEvents(): array {
        return [
            [
                'event' => 'serializer.pre_deserialize',
                'method' => 'onPreDeserialize',
            ],
        ];
    }

    public function onPreDeserialize(PreDeserializeEvent $event) {
        $metadata = $event->getContext()->getMetadataFactory()->getMetadataForClass($event->getType()['name']);

        if($metadata->discriminatorFieldName) {
            $event->getData()[$metadata->discriminatorFieldName];
            $class = $metadata->discriminatorMap[$event->getData()[$metadata->discriminatorFieldName]];
            $metadata = $event->getContext()->getMetadataFactory()->getMetadataForClass($class);
        }

        $attributes = array_keys($event->getData());
        $properties = array_keys($metadata->propertyMetadata);
        $extraAttributes = array_diff($attributes, $properties);

        if($extraAttributes) {
            throw new \Exception(json_encode($extraAttributes) );
        }

    }

}

Usage:

    /**
     * @param string $data
     *
     * @return Response
     */
    protected function deserialize(string $data): Response {
        AnnotationRegistry::registerLoader('class_exists');

        // $serializer = SerializerBuilder::create()->build();
        $builder = SerializerBuilder::create();
        $builder->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
        $builder->configureListeners(function(EventDispatcherInterface $EventDispatcher) {
            $EventDispatcher->addSubscriber(new JmsSerializerEventSubscriber() );
        });

        $serializer = $builder->build();

        $object = $serializer->deserialize($data, Response::class, 'json');
        return $object;
    }