How to make JMS Serializer throw an exception on deserializing JSON instead of coercing types?
Asked Answered
G

2

6

I'm trying to write a REST API which consumes a JSON from a PUT request in Symfony2. Deserializing the JSON to an entity sort of works – but the JMS Serializer seems to coerce types from the JSON instead of throwing an exception if the type of a property in the JSON does not match the entity’s corresponding property.

For example …

{ "id" : "123" }

… will result in …

int(123)

… if the property id is defined as an integer in the entity.

But I would like JMS Serializer to throw an exception instead. Does anyone know how to achieve this?

Update 2016-02-27

One problem with JMS Serializer’s type handling I found is this:

{ "id" : "n123" }

will result in …

int(0)

which is totally undesired.

Can someone please point me into the right direction?

Graceless answered 19/2, 2016 at 10:3 Comment(0)
G
6

After getting help over at Github I want to share an answer on my own question.

The key to a solution is using a custom handler which implements the JMS\Serializer\Handler\SubscribingHandlerInterface (e.g. a StrictIntegerHandler).

<?php
namespace MyBundle\Serializer;

use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class StrictIntegerHandler implements SubscribingHandlerInterface
{
    public static function getSubscribingMethods()
    {
        return [
            [
                'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
                'format' => 'json',
                'type' => 'strict_integer',
                'method' => 'deserializeStrictIntegerFromJSON',
            ],
            [
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => 'strict_integer',
                'method' => 'serializeStrictIntegerToJSON',
            ],
        ];
    }

    public function deserializeStrictIntegerFromJSON(JsonDeserializationVisitor $visitor, $data, array $type)
    {
        return $data;
    }

    public function serializeStrictIntegerToJSON(JsonSerializationVisitor $visitor, $data, array $type, Context $context)
    {
        return $visitor->visitInteger($data, $type, $context);
    }
}

You will then need to define the serializer as a service:

services:
    mybundle.serializer.strictinteger:
        class: MyBundle\Serializer\StrictIntegerHandler
        tags:
            - { name: jms_serializer.subscribing_handler }

Then you will be able to use the type strict_integer:

MyBundle\Entity\MyEntity:
    exclusion_policy: ALL
    properties:
        id:
            expose: true
            type: strict_integer

Deserializing in the controller then works as usual.

Bonus: Using a type validator now finally makes sense:

MyBundle\Entity\MyEntity:
    properties:
        id:
            - Type:
                type: integer
                message: id {{ value }} is not an integer.

I hope this helps those with the same problem.

Graceless answered 20/4, 2016 at 12:58 Comment(0)
G
3

Building on top of reieRMeister's answer, when it comes to JSON deserialisation, I don't think it's wise to coerce primitive types by default.

reieRMaster's answer is good when a distinction needs to be made between "strict" and "loose" type by explicitly setting the type to strict_integer for each property. But this becomes somewhat tedious if you wish to use the built in feature where the type is determined using doctrine metadata.

You can override this default behaviour for all the primitive types by overriding the jms_serializer.json_deserialization_visitor.class with one of your own classes like below:

<?php
namespace MyBundle\Serializer\Visitor;

use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\Context;

class JsonNativeDeserializationVisitor extends JsonDeserializationVisitor
{

    public function visitString($data, array $type, Context $context)
    {
        return $data;
    }

    public function visitBoolean($data, array $type, Context $context)
    {
        return $data;
    }

    public function visitInteger($data, array $type, Context $context)
    {
        return $data;
    }

    public function visitDouble($data, array $type, Context $context)
    {
        return $data;
    }

}

and in services.xml (or services.yml) override jms_serializer.json_deserialization_visitor.class

<parameters>
    <parameter key="jms_serializer.json_deserialization_visitor.class">MyBundle\Serializer\Visitor\JsonNativeDeserializationVisitor</parameter>
</parameters>

Gotcha! Make sure that your bundle is registered AFTER JMSSerializer bundle, or the above will not work. How to do that is described here

Germiston answered 30/6, 2016 at 11:28 Comment(1)
Thank you for sharing this! I will have a look into this.Graceless

© 2022 - 2024 — McMap. All rights reserved.