How to Use the JsonSerializable Interface

Martyn Hardy
Tweet

Over the past few years JSON has taken over as the king of data interchange formats. Before JSON, XML ruled the roost. It was great at modeling complex data but it is difficult to parse and is very verbose. JSON really took off with the proliferation of rich AJAX driven sites as it’s a very human readable format, quick to parse and its simple key/value representation cuts out all the verbosity of XML.

I think we could all agree that writing less code that in turn requires less maintenance and introduces less bugs is a goal we would all like to achieve. In this post, I’d like to introduce you to a little known interface that was introduced in PHP 5.4.0 called JsonSerializable.

Before the JsonSerializable interface was available, returning a JSON encoded representation of an object for a consuming service meant one of two things.

The Ugly

The first approach was to construct a data structure outside the object that contained all the data that we wanted to expose.

<?php

class Customer
{

    private $email = null;
    private $name = null;

    public function __construct($email, $name)
    {
        $this->email = $email;
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}

$customer = new Customer('customer@sitepoint.com', 'Joe');

$data = [
    'customer' => [
        'email' => $customer->getEmail(),
        'name' => $customer->getName()
    ]
];

echo json_encode($data);

We used an array here to hold the data from the Customer object that we wanted to encode, but it could just as easily have been an StdClass.

This approach was flexible and served its purpose in very simple situations where we knew that the Customer object wasn’t going to change and we were only going to need Customer data in this format, in this one place. We also had the option of adding data to this array from other sources if we needed to.

However as we’ve all experienced at one time or another, the assumptions we’ve made can be proven false at a moments notice. We might get a requirement that asks us to add more data to the Customer class. That new data will need to be returned to the consuming service and we’ll want to do this in numerous places.

As you can imagine, this approach quickly becomes troublesome. Not only do we have to duplicate this array code all over our application, we have to remember to update all those instances when more changes inevitably come in. There is another way though, that will help us nullify some of these issues.

The Bad

Luckily we were smart when the first change request came in and we realized that duplicating our array was going to be a nightmare, so what we decided to do was internalize that encoding functionality in our object, removing the maintenance issues and reducing the likelihood of introducing bugs.

<?php

class Customer
{

    public $email = null;
    public $name = null;

    public function __construct($email, $name)
    {
        $this->email = $email;
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function toJson()
    {
        return json_encode([
            'customer' => [
                'email' => $this->getEmail(),
                'name' => $this->getName()
            ]
        ]);
    }
}

$customer = new Customer('customer@sitepoint.com', 'Joe');

echo $customer->toJson();

Now if any more change requests come in that want more data to be added to and returned from the Customer object we can just update the toJson method.

This approach has it’s own drawbacks, though. Anyone else that comes along and wants to use our Customer needs to be aware of this toJson method because it’s not something that is easily checked for, so we’d need accurate documentation. We also have to remember that this method returns JSON now, (though we could move the serialization outside the method). This makes combining Customer data with other sources of data more awkward because we have to be careful not to encode the result of this method again as that would cause some nasty bugs.

The Good

Finally, enter the JsonSerializable interface. This gives us all the flexibility of the Ugly scenario with the maintainability benefits of the Bad scenario. Though to use this interface you will need to be running PHP 5.4.0+ which you really should be doing anyway, as there are many improvements over older versions.

So, to business.

<?php

class Customer implements JsonSerializable
{

    private $name;
    private $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function jsonSerialize()
    {
        return [
            'customer' => [
                'name' => $this->name,
                'email' => $this->email
            ]
        ];
    }
}

$customer = new Customer('customer@sitepoint.com', 'Joe');

echo json_encode($customer);

As you can see, we implement JsonSerializable by adding the interface to our class and then adding a jsonSerialize method to the body of our class to satisfy the interfaces contract.

In the jsonSerialize method we construct and return an array of the object data, just as we did with the other examples. Once again if anything changes then we can just update this one method. You’ll notice that the jsonSerialize method just returns an array.

The magic comes when you want to trigger this method, all we have to do now is json encode an instance of this class and this method will be called automatically, the array of data returned and then encoded! Now that the class implements an interface we benefit from being able to check if this class is an instanceof JsonSerializable. If you wanted you could also type hint in methods to make sure a JsonSerializable interface is passed.

Summary

With this simple implementation, we’ve removed duplication, decreased the amount of maintenance and reduced the chances of introducing bugs. We’ve also made it trivial for another person using our code to test for the ability of the object to be encoded by checking if it’s an instance of JsonSerializable.

The examples above are of course contrived, however, I hope I’ve managed to demonstrate the benefits of using this interface and inspire you to go ahead and use it yourself.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Taylor Ren

    Learned this. Straightforward and nice looking.

    • martynthewolf

      Thanks Taylor Ren, hopefully you find some use for it!

  • Moist Von Lipwig

    Out of interest, what happens if this was used in an api and I needed to export this data in XML format. This would cause problems right? If so when would you use this over a more generic response object?

    Usually I use an abstract response class which is extended for individual data types. This would mean the request can be inspected and the response sent back in the correct format.

    • martynthewolf

      Hey Moist Von Lipwig, author here.

      This approach shouldn’t cause any problem exporting to any other data format. I use this approach with ZF1 using the context switch, that way I can have different view scripts to render different responses from a single action (xml, json).

      The context switch pretty much does exactly what you describe. Inspecting the request and setting up the application to return the correct response.

  • CTN

    Nice and clean

    • martynthewolf

      Cheers CTN :)

  • Jory Geerts

    While this looks pretty good at first, this approach breaks the single responsibility principle – the ‘Customer’ class is not only responsible for “being a customer”, but also for serializing that customer to JSON.
    And if it is OK to have JSON serialisation in there, XML should be there as well, as should PDF generation for end users and Excel export for the BI people.

    It also breaks separation of concerns: The ‘output layer’ should be the place that knows and understands what JSON is and how it should be generated.

    As such, I would prefer some sort of “JSON serialisation service”. That way, we have just one place where the JSON structure is created, but its not littering our Customer class.
    An added benefit is that if we need to serialize different classes (eg Order, Product, etc.), we can have a “base JSON serialisation class” that the specific JSON services can extend, a specific “base PDF generation class” for the specific PDF services, etc. which is not even possible of JSON serialisation and PDF generation are done by the same class since it is not possible to extend multiple classes in PHP.

    • http://milkythinking.com/ Jack Hu

      How do you think about __toString()?

      Aha! Just like what you said: the ‘Customer’ class is not only responsible for “being a customer”, but also for convert that customer to string.

      So, JsonSerializable is a nice way for convert an object to JSON.

      • Jory Geerts

        Unless serializing to string is a core concern of the class, I would not implement __toString().

        For example, at work, we have a class who’s job is “take an image + dimensions, crop settings, etc. and return the URL of a resized version of that image”. I can see __toString() being usefull there, if it returns that URL. (And I’m fairly sure we actually have that.)

        But what would __toString() do for the customer? Return the ID? Name? Address? URL for edit page? (Note: this makes the assumption that ‘Customer’ is an entity. For something like value objects, having a simple string representation is often usefull.)

    • martynthewolf

      Hey Jory, author here, I’m glad this article is raising some debate and responses!

      I agree with you, the example given does break the SRP, however I think that in a small application it’s a pragmatic approach that avoids an unnecessarily complex architecture. The proposal you give would be more appropriate in a system that has many output formats as you suggest. The problem you highlight with your approach though is that a class can’t extend more than one other in PHP, this could be solved using compositions and interfaces. Which JsonSerialization could well be a part of.

      • Jory Geerts

        You’re right that this would result in a “bigger” architecture, though not necessarily more complex.
        And you’re absolutely right that, for a simple application, this could be overkill.
        Regarding the “extension vs composition” thing, I see that more as a side effect then as the main reason for creating such an architecture.
        And yes, I do see use for the JsonSerialization interface – but not (or not always) as part of the entities / domain layer / “whatever ‘Customer’ is a part of”.

  • Filippo Fadda

    The same trick can be done overriding the __toString() magic method. :-)
    Maybe it’s the same approach used by the serializer.

    Second, if you want serialize an object, it’s better if you store the information you want serialize in a metadata array, so the class can be extended without override the asJson() method (or asXml(), etc.). This principle can be used also with the JsonSerializable interface.

    Sincerely I prefer provide different methods to serialize the object and call them explicitly because it is more clear.

    This is an example:

    class Doc {

    // You can use a trait so you can access metadata like properties. Ex: $doc->id = ‘14243’
    use HelperProperties;

    protected $meta = [];

    public function resetMetadata() {
    unset($this->meta);
    $this->meta = [];
    }

    public function isMetadataPresent($name) {
    return (array_key_exists($name, $this->meta)) ? TRUE : FALSE;
    }

    public function assignJson($json) {
    $this->meta = array_merge(HelperArrayHelper::fromJson($json, TRUE), $this->meta);
    }

    public function assignArray(array $array) {
    if (HelperArrayHelper::isAssociative($array)) {
    $this->meta = array_merge($array, $this->meta);
    }
    else
    throw new InvalidArgumentException(“$array must be an associative array.”);
    }

    public function assignObject(stdClass $object) {
    $this->meta = array_merge(get_object_vars($object), $this->meta);
    }

    public function asJson() {
    return json_encode($this->meta);
    }

    public function asArray() {
    return $this->meta;
    }

    public function getId() {
    return $this->meta['_id'];
    }

    public function issetId() {
    return isset($this->meta['_id']);
    }

    public function setId($value) {
    if (!empty($value))
    $this->meta['_id'] = (string)$value;
    else
    throw new Exception(“$id must be a non-empty string.”);
    }

    public function unsetId() {
    if ($this->isMetadataPresent(‘_id’))
    unset($this->meta['_id']);
    }

    public function getRev() {
    return $this->meta['_rev'];
    }

    public function issetRev() {
    return isset($this->meta['_rev']);
    }

    public function setRev($value) {
    $this->meta['_id'] = (string)$value;
    }

    public function unsetRev() {
    if ($this->isMetadataPresent(‘_rev’))
    unset($this->meta['_rev']);

    If you need a new metadata, just add the getter and setter relative to the new property and you don’t need to change asJson(), asArray(), asXml().

    I don’t see a violation of the single responsibility principle, because the asJson() method doesn’t know how to convert the object, in fact it just calls a function that does the job on a generic structure: an array. The function json_encode() has the full responsibility for the conversion.

    • http://www.bitfalls.com/ Bruno Skvorc

      Interesting thoughts, thanks. Maybe use Pastebin or Hastebin next time for code, though

  • Tournas Dimitrios

    This article might give the wrong approach of solving problems. I believe that it doesn’t “print the whole picture” and has left out important pieces.
    Newbies, or developers coming from a different programming language might get a wrong education . We are showing them the wrong route , so to speak.
    What I would like to read (probably as a footnote) is that there is really no reason to reinvent the wheel. Tens of frameworks are doing it in a more flexible way, most importantly , those frameworks are composed by loosely coupled modules which can be used out of the scope of a framework. With Composer it’s just a few clicks to install one of those components .
    A randomly chosen example (from Symfony’s documentation page) :
    http://symfony.com/doc/current/components/serializer.html
    $person = new Person();
    $person->setName(‘foo’);
    $jsonContent = $serializer->serialize($person, ‘json’);
    $xmlContent = $serializer->serialize($person, ‘xml’);
    Looking over the shoulders of “Giants” is the way to go . Fabien (Symfony, Silex) , Taylor Otwell (Laravel) and Matthew Weier O’phinney (Zend Framework) are my favourites . Certainly many more names could be in the list (I had to respect the size of the comment section ).
    Most likely this article has been read by hundreds (if not thousands) [new] developers, it should be made clear to them : “Just because we can doesn’t mean we should”. Learning the right way of coding from the beginning will “pay off down the road”. I have taken “my lesson” , it was really hard for me to “thrown off” practises which I had learned during my “newbie days”.
    Please don’t get me wrong, my intention was to make a constructive criticism . Most articles of this Blog are designated as “high quality” by me, certainly this article is one of those .

    • martynthewolf

      Hi Tournas, author here.

      I appreciate the criticism and pointing me at a new toy to explore. I agree with your premise that just because something can be used doesn’t mean it’s the right tool in all cases, there are many ways to skin a cat, only experience is able to tell you what tool is the most appropriate. Maybe a follow up article that explores some of the more flexible options out there would be a nice idea.

      Definitely food for thought. I’m also pleased that you deem my writing to be of high quality. It means a lot.

      Thanks