JSON <=> PHP8+ objects serialization / deserialization library

Related tags

Miscellaneous pjson
Overview

A simple library for JSON to PHP Objects conversions

Often times, we interact with an API, or data source that returns JSON. PHP only offers the possibility to deserialize that json into an array or objects of type stdClass.

This library helps deserializing JSON into actual objects of custom defined classes. It does so by using PHP8's attributes on class properties.

Examples

Simple serialization of a class

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class Schedule
{
    use JsonSerialize;

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

(new Schedule(1, 2))->toJson();

Would yield the following:

{
    "start": 1,
    "end": 2
}

And then the reverse can be achieved via:

Schedule::fromJsonString('{"start":1,"end":2}');

Which would return an instance of class Schedule with the properties set according to the JSON.

Custom Names

The previous example can be made to use custom names in JSON instead of just the property name:

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class Schedule
{
    use JsonSerialize;

    #[Json('begin')]
    protected int $start;

    #[Json('stop')]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

(new Schedule(1, 2))->toJson();

would yield

{
    "begin": 1,
    "stop": 2
}

And deserializing with those new property names works just as before:

dump(Schedule::fromJsonString('{"begin":1,"stop":2}'));

// ^ Schedule^ {#345
//   #start: 1
//   #end: 2
// }

Private / Protected

The visibility of a property does not matter. A private or protected property can be serialized / unserialized as well (see previous examples).

Property Path

Sometimes the json format isn't exactly the PHP version we want to use. Say for example that the JSON we received for the previous schedule examples were to look like:

{
    "data": {
        "start": 1,
        "end": 2
    }
}

By declaring our class json attributes as follows, we can still read those properties direclty into our class:

class Schedule
{
    use JsonSerialize;

    #[Json(['data', 'start'])]
    protected int $start;

    #[Json(['data', 'end'])]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

Recursive serialize / deserialize

If we are working with a json structure that's a bit more complex, we will want to have properties be classes that can also be properly deserialized into.

{
    "saturday": {
        "start": 0,
        "end": 2
    },
    "sunday": {
        "start": 0,
        "end": 7
    }
}

The following 2 PHP classes could work well with this:

class Schedule
{
    use JsonSerialize;

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;
}

class Weekend
{
    use JsonSerialize;

    #[Json('saturday')]
    protected Schedule $sat;
    #[Json('sunday')]
    protected Schedule $sun;
}

Arrays

When working with an array of items where each item should be of a given class, we need to tell pjson about the target type:

{
    "days": [
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 7
        }
    ]
}

With Schedule still defined as before, we'd define a week like:

class Week
{
    use JsonSerialize;

    #[Json(type: Schedule::class)]
    protected array $days;
}

This would also work with a map if the json were like:

{
    "days": {
        "monday": {
            "start": 0,
            "end": 2
        },
        "wednesday": {
            "start": 0,
            "end": 2
        }
    }
}

And the resulting PHP object would be:

Week^ {#353
  #days: array:2 [
    "monday" => Schedule^ {#344
      #start: 0
      #end: 2
    }
    "wednesday" => Schedule^ {#343
      #start: 0
      #end: 2
    }
  ]
}

Polymorphic deserialization

Say you have 2 classes that extend a base class. You might receive those as part of a collection and don't know ahead of time if you'll be dealing with one or the other. For example:

abstract class CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected $id;

    #[Json]
    protected string $type;
}

class CatalogCategory extends CatalogObject
{
    use JsonSerialize;

    #[Json('parent_category_id')]
    protected string $parentCategoryId;
}

class CatalogItem extends CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected string $name;
}

You can implement the fromJsonData(array $array) : static on CatalogObject to discriminate based on the received data and return the correct serialization:

abstract class CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected $id;

    #[Json]
    protected string $type;

    public static function fromJsonData($jd): static
    {
        $t = $jd['type'];

        return match ($t) {
            'category' => CatalogCategory::fromJsonData($jd),
            'item' => CatalogItem::fromJsonData($jd),
        };
    }
}

WARNING: Make sure that each of the subclasses directly use JsonSerialize. Otherwise when they call ::fromJsonData, they would call the parent on CatalogObject leading to infinite recursion.

With this in place, we can do:

$jsonCat = '{"type": "category", "id": "123", "parent_category_id": "456"}';
$c = CatalogObject::fromJsonString($jsonCat);
$this->assertEquals(CatalogCategory::class, get_class($c));

$jsonItem = '{"type": "item", "id": "123", "name": "Sandals"}';
$c = CatalogObject::fromJsonString($jsonItem);
$this->assertEquals(CatalogItem::class, get_class($c));

Lists

If you're dealing with a list of things to deserialize, you can call MyClass::listFromJsonString($json) or MyClass::listfromJsonData($array). For example:

Schedule::listFromJsonString('[
    {
        "schedule_start": 1,
        "schedule_end": 2
    },
    {
        "schedule_start": 11,
        "schedule_end": 22
    },
    {
        "schedule_start": 111,
        "schedule_end": 222
    }
]');

yields the same as

[
    new Schedule(1, 2),
    new Schedule(11, 22),
    new Schedule(111, 222),
];

Initial path

Somteimes the JSON you care about will be nested under a property but you don't want / need to model the outer layer. For this you can pass a $path to the deserializing methods:

Schedule::fromJsonString('{
    "data": {
        "schedule_start": 1,
        "schedule_end": 2
    }
}', path: 'data');

Schedule::fromJsonString('{
    "data": {
        "main": {
            "schedule_start": 1,
            "schedule_end": 2
        }
    }
}', path: ['data', 'main']);

Enums

Backed enums are supported out of the box in PHP 8.1

class Widget
{
    use JsonSerialize;

    #[Json]
    public Status $status;
}

enum Status : string
{
    case ON = 'ON';
    case OFF = 'OFF';
}
$w = new Widget;
$w->status = Status::ON;

$w->toJson(); // {"status": "ON"}

And regular enums can be supported via the JsonSerialize trait or the JsonDataSerializable interface

class Widget
{
    use JsonSerialize;

    #[Json]
    public Size $size;
}

enum Size
{
    use JsonSerialize;

    case BIG;
    case SMALL;

    public static function fromJsonData($d, array|string $path = []): static
    {
        return match ($d) {
            'BIG' => self::BIG,
            'SMALL' => self::SMALL,
            'big' => self::BIG,
            'small' => self::SMALL,
        };
    }

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

$w = new Widget;
$w->size = Size::BIG;

$w->toJson(); // {"status": "big"}

Scalar <=> Class

In some cases, you might want a scalar value to become a PHP object once deserialized and vice-versa. For example, a BigInt class could hold an int as a string and represent it as a string when serialized to JSON:

class Stats
{
    use JsonSerialize;

    #[Json]
    public BigInt $count;
}

class BigInt implements JsonDataSerializable
{
    public function __construct(
        protected string $value,
    ) {
    }

    public static function fromJsonData($jd, array|string $path = []) : static
    {
        return new BigInt($jd);
    }

    public function toJsonData()
    {
        return $this->value;
    }
}

$stats = new Stats;
$stats->count = new BigInt("123456789876543234567898765432345678976543234567876543212345678765432");
$stats->toJson(); // {"count":"123456789876543234567898765432345678976543234567876543212345678765432"}

Use with PHPStan

Using this library, you may have properties that don't appear to be read from or written to anywhere in your code, but are purely used for JSON serialization. PHPStan will complain about these issues, but you can help PHPStan understand that this is expected behavior by adding this library's extension in your phpstan.neon.

includes:
  - vendor/square/pjson/extension.neon
Comments
  • support for enums

    support for enums

    This is definitely a weird trick but ... since the methods are in a trait and never in an interface. It's possible to override them with different type requirements.

    So an enum can do fromJsonData(string $data) instead of fromJsonData(array $data)

    opened by khepin 1
  • Proposal for integration with Laravel casts

    Proposal for integration with Laravel casts

    This shows how we could provide an integration for casting to and from json with actual PHP objects for Laravel's Eloquent models.

    Looking for feedback.

    @mattgrande @bezhermoso @szainmehdi

    opened by khepin 0
  • Fix serializing when path has integer bits

    Fix serializing when path has integer bits

    Deserializing #[Json('list', 0, 'name')] was already fine but the issue was when re-serializing this.

    Using stdClass meant it was impossible to set something at the property 0. By switching internally to an array, we can add properties and / or integer offsets correctly.

    @mattgrande @bezhermoso @szainmehdi

    opened by khepin 0
  • Allow passing in flags and depth parameters to json_encode() and json_decode()

    Allow passing in flags and depth parameters to json_encode() and json_decode()

    Allow passing flags and depth params to toJson(), toJsonList(), fromJsonString(), and listFromJsonString() methods.

    Felt like this was missing. A sample use case might be to pass in JSON_PRETTY_PRINT for readability.

    (new Schedule(1, 2))->toJson(flags: JSON_PRETTY_PRINT);
    

    Default JSON_THROW_ON_ERROR flag for JSON serialization This was being done for deserialization but not for serialization.

    Added tests for the above

    Added PHPStan Extension Depending on way a class was set up, PHPStan would complain about properties being written to but never read from.

    class Item
    {
        public function __construct(
            #[Json]
            private string $id,
        ) {}
    }
    

    This perfectly valid use case would throw a PHPStan error like this:

    Example PHPStan Error:
     ------ --------------------------------------------------------------------------------------------
      Line   src/Item.php
     ------ --------------------------------------------------------------------------------------------
      10     Property Item@src/Item.php:10::$id is never read, only written.
             💡 See: https://phpstan.org/developing-extensions/always-read-written-properties
     ------ --------------------------------------------------------------------------------------------
    

    Allowing developers to simply add the PHPStan extension defined in this package to their phpstan.neon file helps eliminate these issues and allows us to expand on deeper PHPStan integration in the future.

    opened by szainmehdi 0
  • Whole bunch of updates

    Whole bunch of updates

    • Tests for private vars
    • Tests for readonly vars
    • Type can be declared in the attribute instead of the property
    • Works with hashmaps

    @mattgrande @bezhermoso @szainmehdi

    opened by khepin 0
  • Default teams added

    Default teams added

    :wave: Hi there!

    I've added the following default team permissions to make it easier for other Squares to find and interact with this repo:

    If these settings aren't right, feel free to change or remove them!

    opened by squareup-github-settings[bot] 0
  • [Feature] Allow dot-notation for JSON paths

    [Feature] Allow dot-notation for JSON paths

    Using the example definition in the README, it would be great (and familiar to Laravel users) to define nested JSON mappings using dot-notation.

    class Schedule
    {
        use JsonSerialize;
    
        #[Json('data.start')]
        protected int $start;
    
        #[Json('data.end')]
        protected int $end;
    
        public function __construct(int $start, int $end)
        {
            $this->start = $start;
            $this->end = $end;
        }
    }
    
    opened by szainmehdi 4
Releases(v0.2.0)
Owner
Square
Square
Opis Closure - a library that aims to overcome PHP's limitations regarding closure serialization

Opis Closure is a library that aims to overcome PHP's limitations regarding closure serialization by providing a wrapper that will make all closures serializable.

Opis 2.4k Dec 18, 2022
Allows generate class files parse from json and map json to php object, including multi-level and complex objects;

nixihz/php-object Allows generate class files parse from json and map json to php object, including multi-level and complex objects; Installation You

zhixin 2 Sep 9, 2022
A pure PHP implementation of the MessagePack serialization format / msgpack.org[PHP]

msgpack.php A pure PHP implementation of the MessagePack serialization format. Features Fully compliant with the latest MessagePack specification, inc

Eugene Leonovich 368 Dec 19, 2022
Dubbox now means Dubbo eXtensions, and it adds features like RESTful remoting, Kyro/FST serialization, etc to the Dubbo service framework.

Dubbox now means Dubbo eXtensions. If you know java, javax and dubbo, you know what dubbox is :) Dubbox adds features like RESTful remoting, Kyro/FST

当当 4.9k Dec 27, 2022
Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.DeepEqual().

Deeper Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.De

Joubert RedRat 4 Feb 12, 2022
PHP package to make your objects strict and throw exception when you try to access or set some undefined property in your objects.

?? Yell PHP package to make your objects strict and throw exception when you try to access or set some undefined property in your objects. Requirement

Zeeshan Ahmad 20 Dec 8, 2018
Creating data transfer objects with the power of php objects. No php attributes, no reflection api, and no other under the hook work.

Super Simple DTO Creating data transfer objects with the power of php objects. No php attributes, no reflection api, and no other under the hook work.

Mohammed Manssour 8 Jun 8, 2023
All PHP functions, rewritten to throw exceptions instead of returning false, now for php8

A set of core PHP functions rewritten to throw exceptions instead of returning false when an error is encountered.

TheCodingMachine 106 Nov 21, 2022
DBML parser for PHP8. It's a PHP parser for DBML syntax.

DBML parser written on PHP8 DBML (database markup language) is a simple, readable DSL language designed to define database structures. This page outli

Pavel Buchnev 32 Dec 29, 2022
Validated properties in PHP8.1 and above using attribute rules

PHP Validated Properties Add Rule attributes to your model properties to make sure they are valid. Why this package? When validating external data com

null 23 Oct 18, 2022
Dobren Dragojević 6 Jun 11, 2023
Json-normalizer: Provides generic and vendor-specific normalizers for normalizing JSON documents

json-normalizer Provides generic and vendor-specific normalizers for normalizing JSON documents. Installation Run $ composer require ergebnis/json-nor

null 64 Dec 31, 2022
PDF API. JSON to PDF. PDF Template Management, Visual HTML Template Editor and API to render PDFS by json data

PDF Template Management, Visual HTML Template Editor and API to render PDFS by json data PDF ENGINE VERSION: development: This is a prerelease version

Ajous Solutions 2 Dec 30, 2022
:date: The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects

sabre/vobject The VObject library allows you to easily parse and manipulate iCalendar and vCard objects using PHP. The goal of the VObject library is

sabre.io 532 Dec 25, 2022
Samsui is a factory library for building PHP objects useful for setting up test data in your applications.

#Samsui Samsui is a factory library for building PHP objects useful for setting up test data in your applications. It is mainly inspired by Rosie for

Sam Yong 31 Nov 11, 2020
A PHP 7 value objects helper library.

valueobjects Requirements Requires PHP >= 7.1 Installation Through Composer, obviously: composer require funeralzone/valueobjects Extensions This lib

Funeral Zone 56 Dec 16, 2022
QuidPHP/Main is a PHP library that provides a set of base objects and collections that can be extended to build something more specific.

QuidPHP/Main is a PHP library that provides a set of base objects and collections that can be extended to build something more specific. It is part of the QuidPHP package and can also be used standalone.

QuidPHP 4 Jul 2, 2022
PHP library with basic objects and more for working with Facebook/Metas Conversions API

PHP library with basic objects and more for working with Facebook/Metas Conversions API Installation The easiest way to install this library is by ins

null 5 Dec 5, 2022
A set of classes to create and manipulate HTML objects abstractions

HTMLObject HTMLObject is a set of classes to create and manipulate HTML objects abstractions. Static calls to the classes echo Element::p('text')->cla

Emma Fabre 128 Dec 22, 2022