Search code examples
phpsymfonyserializationdenormalization

Error with Symfony Serializer can't normalize int to float


I get an error when I try to denormalize an object with a float attribute but an int value.

Symfony\Component\Serializer\Exception\NotNormalizableValueException:

The type of the "solde" attribute for class "Wallet" must be one of "float" ("int" given).

My Serializer configuration:

class SerializerService implements SerializerInterface, NormalizerInterface, DenormalizerInterface
{
    public const JSON_FORMAT = 'json';

    /**
     * @var Serializer
     */
    private $serializer;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param LoggerInterface $logger
     */
    public function __construct(
        LoggerInterface $logger
    ) {
        $this->logger = $logger;

        $this->serializer = new Serializer([
            new DateTimeNormalizer(),
            new ObjectNormalizer(null, null, null, new ReflectionExtractor()),
            new ArrayDenormalizer()
        ], [
            new JsonEncoder(
                new JsonEncode(),
                new JsonDecode(),
            )
        ]);
    }
...
...

I didn't find anything relevant in the Symfony Serializer documentation.

Can you help me please?


Solution

  • The problem is not so much with the serializer, but your data. You probably have something like:

    {
      “solde”: 1.0
    }
    

    but it should be: {“solde”: 1}, i.e. your data should be an int and not a float.

    If you make $solde in your Wallet-object a float, instead of an int you won’t have this problem because converting from int to float is usually lossless, i.e. 1 to 1.0. Also, you should not accept floats for this kind of data in the first place. Assuming “1.0” stands for something like 1$ or 1 EUR, then it is customary to instead save it as a string or int as cent value, i.e. 100 instead and I would recommend also using this format for your JSON. That way, you don’t need floats and you won’t run into weird floating point issues. I highly recommend going that route. Since you are doing the conversion the other way (from float to int) you will lose precision, i.e. 1.5 will either become 1 or 2, which obviously is bad because your users might not know which of the 2 possible outcomes it will be.

    Coming back to your error. In your object you can resolve this type issue either when you receive the data, i.e. set the field from JSON or whenever you read the data. For the former do the lossy conversion (casting to int, rounding, …) inside the object’s setter for this field. You might need to adjust your ObjectNormalizer to use the getters and setters for this, but it should use the PropertyAccess-component by default which should use the setter before it looks for the property, if I am not mistaken. It would roughly look like this:

    class Wallet
    {
        private int $solde;
    
        // …
    
        public function setSolde(float|int $solde): void
        {
            $this->solde = (int) $solde; // casting to int will just cut off anything after the “.” in floats
        }
    }
    

    You could also have your field be either float or int and then do the conversion on the getter, i.e. when you use the field but then you might get issues when you want to persist the data in the database.

    class Wallet
    {
        private int|float $solde;
    
        // …
    
        public function getSolde(): int
        {
            return (int) $this->solde; // casting to int will just cut off anything after the “.” in floats
        }
    }
    

    If you don’t want to adjust your object for this, then you can teach the Serializer to do the lossy conversion by writing a custom denormalizer (Symfony combines denormalizers and normalizers in their normalizers, i.e. ObjectNormalizer, if you are looking for a reference how to write them). This denormalizer will then handle your object, i.e. Wallet, and manually map each field from your JSON to the object’s field, including “solde”, where you have the type error. You can then resolve the type error in the Denormalizer by doing the lossy conversion (casting to int, rounding, …). You have to be careful to not make the Denormalizer too generic as you might end up doing this even in places where you don’t want this. Symfony has a part in the docs for custom normalizers/denormalizers, so it should be fairly straightforward. If you really need this, I would recommend going this route instead, assuming you only need to worry about this problem when reading the data from JSON.