I was looking for a way to serialize domain objects into JSON strings and back again into objects of the original class. This is essential for webservices, especially those built around the JSON-RPC specification that allow clients to make calls to their server and, for example add new personal data to their database of contacts. It makes life a lot easier when this data matches the field names and inner structure of your own domain objects. For example, a JSON string like {"name":"Matthias Noback"} should be easily translatable to an object of class Acme\Webservice\Entity\Person with a private property name. But how do you know which class to instantiate, when you only have the simple JSON string above?

The JSON-RPC specifies under the title "JSON Class hinting" a way you could add a clue about which class would be appropriate (and also which constructor arguments should be used), as part of your JSON object. Following this specification, the example above should look like this:

{"__jsonclass__":["Acme\\WebserviceBundle\\Entity\\Person",[]],"name":"Matthias Noback"}

We have a specification, now we need some tools. Symfony2 already gives us the beautiful Serializer component, which is not widely used inside the framework itself (as far as I can see, nowhere?), but is an essential tool when creating webservices. The Serializer receives a set of Encoder objects and a set of Normalizer objects. Encoders take care of the conversion between simple PHP data types (like null, scalar and array) and an XML or JSON string for example. Normalizers are there to convert more complicated data types (like objects) to the simple ones. Writing a custom normalizer allows you to make this conversion very specific for your own objects. Because we are going to implement the "JSON Class hinting" specification, we will create a JsonClassHintingNormalizer.

Creating the custom Normalizer

First we implement the normalize() method. This method receives an object of (for example) class Acme\WebserviceBundle\Entity\Person. It will eventually return an array containing the data the may be encoded directly into JSON.

namespace Acme\WebserviceBundle\Normalizer;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class JsonClassHintingNormalizer implements NormalizerInterface
{
    public function normalize($object, $format = null)
    {
        $data = array();

        $reflectionClass = new \ReflectionClass($object);

        $data['__jsonclass__'] = array(
            get_class($object),
            array(), // constructor arguments
        );

        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
            if (strtolower(substr($reflectionMethod->getName(), 0, 3)) !== 'get') {
                continue;
            }

            if ($reflectionMethod->getNumberOfRequiredParameters() > 0) {
                continue;
            }

            $property = lcfirst(substr($reflectionMethod->getName(), 3));
            $value = $reflectionMethod->invoke($object);

            $data[$property] = $value;
        }

        return $data;
    }
}

As you can see, the $data array will be filled by calling all the original object's getter methods that have no required arguments. The normalize() method also adds a key called "jsonclass" to the array, which contains the class name of the object and an array of constructor arguments (I leave the implementation for this to the reader).

Next, we define the denormalize() method. This method receives data that is decoded from a JSON string into an array.

class JsonClassHintingNormalizer implements NormalizerInterface
{
    ...

    public function denormalize($data, $class, $format = null)
    {
        $class = $data['__jsonclass__'][0];
        $reflectionClass = new \ReflectionClass($class);

        $constructorArguments = $data['__jsonclass__'][1] ?: array();

        $object = $reflectionClass->newInstanceArgs($constructorArguments);

        unset($data['__jsonclass__']);

        foreach ($data as $property => $value) {
            $setter = 'set' . $property;
            if (method_exists($object, $setter)) {
                $object->$setter($value);
            }
        }

        return $object;
    }
}

It looks for the key "jsonclass" and based on it's value, it instantiates the proper class. Then it walks through all the values in the data array and tries to find a corresponding setter method for them.

Finally, the Serializer needs some other methods, so it can ask the JsonClassHintingNormalizer if it supports normalization and/or denormalization for the given object and/or data:

class JsonClassHintingNormalizer implements NormalizerInterface
{
    ...

    public function supportsNormalization($data, $format = null)
    {
        return is_object($data) && 'json' === $format;
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return isset($data['__jsonclass__']) && 'json' === $format;
    }
}

This means the normalizer works only for the "json" format and can only denormalize if a "jsonclass" is specified.

Using Serializer with the new Normalizer

All this may be put to work by creating an instance of Serializer and providing it with our new normalizer and a JSON encoder:

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Acme\WebserviceBundle\Normalizer\JsonClassHintingNormalizer;
use Acme\WebserviceBundle\Entity\Person;

$serializer = new Serializer(array(
    new JsonClassHintingNormalizer(),
), array(
    'json' => new JsonEncoder(),
));

$person = new Person();
$person->setName('Matthias Noback');

$serialized = $serializer->serialize($person, 'json');
echo $serialized;

The result of this will be:

{"__jsonclass__":["Acme\\WebserviceBundle\\Entity\\Person",[]],"name":"Matthias Noback"}

And when we deserialize this string:

$deserialized = $serializer->deserialize($serialized, null, 'json');
var_dump($deserialized);

This is what we will get (and what we really wanted!):

object(Acme\WebserviceBundle\Entity\Person)#619 (1) {
  ["name":"Acme\WebserviceBundle\Entity\Person":private]=>
  string(15) "Matthias Noback"
}

What about recursion?

It is fairly easy to adapt the implementation given above to allow for recursively defined objects. I will demonstrate this in another post, yet to write.

PHP Symfony2 reflection serializer