CuyZ / Valinor

PHP library that helps to map any input into a strongly-typed value object structure.

Home Page:https://valinor.cuyz.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`JsonSerializable` is not respected in `Normalizer`

boesing opened this issue · comments

Hey there,

I am currently playing around with the normalizer.
I've implemented some custom visitor in JMS to actually let json_encode do its magic when JsonSerializable interface is used.

I've added something like that to our code already and ended up with some special cases:

namespace Foo;

final class ArrayObject implements \JsonSerializable
{
     /** @return non-empty-array|object */
     public function jsonSerialize(): array|object
     {
         if ($this->count() === 0) {
             return new stdClass;
         }

         return $this->toArray();
     }
}

This is due to the fact that empty arrays do not retain as objects when used with json_encode. So the fix for us was to actually implement this workaround for deprecated stuff while still being able to use JMS serializer.

Sadly, there is no way for us to move the JsonSerializable logic to json_encode since the ArrayNormalizer applies the normalizeIterator logic. I was trying to use the JsonNormalizer but that is not possible due to the lack of object support. :-/

Hi @boesing, I'm not sure I really understand what you want to achieve here. Could you provide an example of an input and its JSON output in your current implementation with JMS?

<?php
namespace Foo;

final class ArrayObject implements \JsonSerializable
{
     /** @return non-empty-array|object */
     public function jsonSerialize(): array|object
     {
         if ($this->count() === 0) {
             return new stdClass;
         }

         return $this->toArray();
     }
}
$object = new \Foo\ArrayObject([]);
/** @var JMS\Serializer\SerializerInterface $serializer */
$serializer = null;


$serializer->serialize($object, 'json');

Generates: {}


But I could also have something like:

final class Whatever implements \JsonSerializable
{
         public function jsonSerialize(): array
         {
              return ['foo' => 'bar'];
         }
}

$object = new Whatever;
/** @var JMS\Serializer\SerializerInterface $serializer */
$serializer = null;

$serializer->serialize($object, 'json');

Generates: {"foo":"bar"}

I do just want json_encode to handle the serialization rather than the normalizer.

So in case there is instanceOf \JsonSerializable, the "value" should be passed to json_encode instead of applying whatever normalizer.

Hi @boesing, here is a hint on how you could achieve that, if I understood correctly:

final class SomeObject implements \JsonSerializable
{
    public function __construct(private array $data) {}

    public function jsonSerialize(): \ArrayObject
    {
        return new \ArrayObject($this->data);
    }
}

$normalizer = (new MapperBuilder())
    ->registerTransformer(
        fn (\JsonSerializable $object) => $object->jsonSerialize(),
    )
    ->normalizer(Format::json());

$emptyObject = new SomeObject([]);
$emptyResult = $normalizer->normalize($emptyObject);

$emptyResult === '{}'; // true
$emptyResult === json_encode($emptyObject); // true

$objectWithValues = new SomeObject(['foo' => 'bar']);
$resultWithValues = $normalizer->normalize($objectWithValues);

$resultWithValues === '{"foo":"bar"}'; // true
$resultWithValues === json_encode($objectWithValues); // true

Would it be ok?

We have a bunch of array objects in use and therefore have to keep the iterable type. So that wont really work for me 🫤

Sorry I once again don't understand what you mean. 😅

What do you mean by “have to keep the iterable type”?

I will have a look again tomorrow, maybe I am missing something. But your example requires me to call the json serializable method on my own rather than let json_encode do it. might be no biggy and thus I think that would be okayish.
But I am unsure if ur example does work for iterable implementing jsonserializable - so will have to evaluate that again.

I double checked that, it works. I am not 100% concerned about having to call that json-specific method by myself but it works. I will close here.