GitHunt
CW

cweiske/jsonmapper

Map nested JSON structures onto PHP classes


JsonMapper - map nested JSON structures onto PHP classes


Takes data retrieved from a JSON__ web service and converts them
into nested object and arrays - using your own model classes.

Starting from a base object, it maps JSON data on class properties,
converting them into the correct simple types or objects.

It's a bit like the native SOAP parameter mapping PHP's SoapClient
gives you, but for JSON.
It does not rely on any schema, only your PHP class definitions.

Type detection works by parsing type declarations and @var
docblock annotations of class properties,
as well as type hints in setter methods.

You do not have to modify your model classes by adding JSON specific code;
it works automatically by parsing already-existing docblocks.

This library has no dependencies.

Keywords: deserialization, hydration

__ http://json.org/

.. contents::

============
Pro & contra

Benefits

  • Autocompletion in IDEs
  • It's easy to add comfort methods to data model classes
  • Your JSON API may change, but your models can stay the same - not
    breaking applications that use the model classes.

Drawbacks

  • Model classes need to be written by hand

    Since JsonMapper does not rely on any schema information
    (e.g. from json-schema__), model classes cannot be generated
    automatically.

__ http://json-schema.org/

=====
Usage

Basic usage

#. Install__ netresearch/jsonmapper with composer
#. Create a JsonMapper object instance
#. Call the map or mapArray method, depending on your data

Map a normal object:

.. code:: php

<?php
require 'autoload.php';
$mapper = new JsonMapper();
$contactObject = $mapper->map($jsonContact, new Contact());
// or as classname
$contactObject = $mapper->map($jsonContact, Contact::class);

Map an array of objects:

.. code:: php

<?php
require 'autoload.php';
$mapper = new JsonMapper();
$contactsArray = $mapper->mapArray(
    $jsonContacts, array(), 'Contact'
);

Instead of array() you may also use ArrayObject and derived classes,
as well as classes implementing ArrayAccess.

.. __: #installation

Example

JSON from an address book web service:

.. code:: javascript

{
    "name":"Sheldon Cooper",
    "address": {
        "street": "2311 N. Los Robles Avenue",
        "city": "Pasadena"
    }
}

Your local Contact class:

.. code:: php

<?php
class Contact
{
    /**
     * Full name
     */
    public string $name;

    public ?Address $address;
}

Your local Address class:

.. code:: php

<?php
class Address
{
    public $street;
    public $city;

    public function getGeoCoords()
    {
        //do something with $street and $city
    }
}

Your application code:

.. code:: php

<?php
$json = json_decode(file_get_contents('http://example.org/sheldon.json'));
$mapper = new JsonMapper();
$contact = $mapper->map($json, new Contact());

echo "Geo coordinates for " . $contact->name . ": "
    . var_export($contact->address->getGeoCoords(), true);

Property type mapping

JsonMapper uses several sources to detect the correct type of
a property in the following order:

#. Setter method (set + ucwords($propertyname))

Underscores "_" and hyphens "-" make the next letter uppercase.
Property foo_bar-baz leads to setter method setFooBarBaz.

#. If it has a type hint in the method signature then its type used::

    public function setPerson(Contact $person) {...}

#. The method's docblock is inspected for @param $type annotations::

    /**
     * @param Contact $person Main contact for this application
     */
    public function setPerson($person) {...}

#. If no type could be detected, the plain JSON value is passed
to the setter method.

#. Class property types (since PHP 7.4)::

 public Contact $person;

#. Constructor property promotion types (since PHP 8.0)::

 public function __construct(protected Contact $person) {}

#. @var $type docblock annotation of class properties::

/**
 * @var \my\application\model\Contact
 */
public $person;

The property has to be public to be used directly.
You may also use $bIgnoreVisibility__ to utilize
protected and private properties.

.. __: #prop-bignorevisibility

If no type could be detected, the property gets the plain JSON value set.

If a property can not be found, JsonMapper tries to find the property
in a case-insensitive manner.
A JSON property isempty would then be mapped to a PHP property
isEmpty.

.. note::
You have to provide the fully qualified namespace
for the type to work. Relative class names are evaluated
in the context of the current classes namespace, NOT
respecting any imports that may be present.

  PHP does not provide the imports via Reflection; the comment text only
  contains the literal text of the type.
  For performance reasons JsonMapper does not parse the source code on its
  own to detect and expand any imports.

Supported type names

  • Simple types

    • string
    • bool, boolean
    • int, integer
    • double, float
    • array
    • object
    • mixed
  • Class names, with and without namespaces

    • Contact - exception will be thrown if the JSON value is null
  • Arrays of simple types and class names:

    • int[]
    • Contact[]
  • Multidimensional arrays:

    • int[][]
    • TreeDeePixel[][][]
  • ArrayObjects of simple types and class names:

    • ContactList[Contact]
    • NumberList[int]
  • Backed enums, with and without namespaces

    • Suit:string|Suit:int - exception will be thrown if the JSON value is not present in the enum
  • Nullable types:

    • int|null or ?int - will be null if the value in JSON is
      null, otherwise it will be an integer
    • Contact|null or ?Contact - will be null if the value in JSON is
      null, otherwise it will be an object of type Contact

ArrayObjects and extending classes are treated as arrays.

Variables without a type or with type mixed will get the
JSON value set directly without any conversion.

See phpdoc's type documentation__ for more information.

__ http://phpdoc.org/docs/latest/references/phpdoc/types.html

Simple type mapping

.. note::
This feature is disabled by default for security reasons since version 5.
See $bStrictObjectTypeChecking__ for details.

.. __: #prop-bstrictobjecttypechecking

When an object shall be created but the JSON contains a simple type
only (e.g. string, float, boolean), this value is passed to
the classes' constructor. Example:

PHP code:

.. code:: php

public DateTime $date;

JSON:

.. code:: js

{"date":"2014-05-15"}

This will result in new DateTime('2014-05-15') being called.

Class map

When variables are defined as objects of abstract classes or interfaces,
JsonMapper would normally try to instantiate those directly and crash.

Using JsonMapper's $classMap property, you can specify which classes
shall get instantiated instead:

.. code:: php

$jm = new JsonMapper();
$jm->classMap['Foo'] = 'Bar';
$jm->map(...);

This would create objects of type Bar when a variable is defined to be
of type Foo.

It is also possible to use a callable in case the actual implementation class
needs to be determined dynamically (for example in case of a union).
The mapped class ('Foo' in the example below) and the Json data are passed as
parameters into the call.

.. code:: php

$mapper = function ($class, $jvalue) {
    // examine $class and $jvalue to figure out what class to use...
    return 'DateTime';
};

$jm = new JsonMapper();
$jm->classMap['Foo'] = $mapper;
$jm->map(...);

Nullables

JsonMapper throws an exception when a JSON property is null,
unless the PHP class property has a nullable type - e.g. Contact|null or ?Contact.

If your API contains many fields that may be null and you do not want
to make all your type definitions nullable, set:

.. code:: php

$jm->bStrictNullTypes = false;

Since version 5.0.0, null values in arrays lead to a JsonMapper_Exception
unless the type is nullable - e.g. array[?string] or array[string|null].

To get the previous behavior back (allowing nulls even when not declared so) set:

.. code:: php

$jm->bStrictNullTypesInArrays = false;

Logging

JsonMapper's setLogger() method supports all PSR-3__ compatible
logger instances.

Events that get logged:

  • JSON data contain a key, but the class does not have a property
    or setter method for it.
  • Neither setter nor property can be set from outside because they
    are protected or private

__ http://www.php-fig.org/psr/psr-3/

Handling invalid or missing data

During development, APIs often change.
To get notified about such changes, JsonMapper can be configured to
throw exceptions in case of either missing or yet unknown data.

Unknown properties

When JsonMapper sees properties in the JSON data that are
not defined in the PHP class, you can let it throw an exception
by setting $bExceptionOnUndefinedProperty:

.. code:: php

$jm = new JsonMapper();
$jm->bExceptionOnUndefinedProperty = true;
$jm->map(...);

You may also choose to handle those properties yourself by setting
a callable__ to $undefinedPropertyHandler:

__ http://php.net/manual/en/language.types.callable.php

.. code:: php

/**
 * Handle undefined properties during JsonMapper::map()
 *
 * @param object $object    Object that is being filled
 * @param string $propName  Name of the unknown JSON property
 * @param mixed  $jsonValue JSON value of the property
 *
 * @return void
 */
function setUndefinedProperty($object, $propName, $jsonValue)
{
    $object->{'UNDEF' . $propName} = $jsonValue;
}

$jm = new JsonMapper();
$jm->undefinedPropertyHandler = 'setUndefinedProperty';
$jm->map(...);

Or if you would let JsonMapper handle the setter for you, you can return a string
from the $undefinedPropertyHandler which will be used as property name.

.. code:: php

/**
 * Handle undefined properties during JsonMapper::map()
 *
 * @param object $object    Object that is being filled
 * @param string $propName  Name of the unknown JSON property
 * @param mixed  $jsonValue JSON value of the property
 *
 * @return void
 */
function fixPropName($object, $propName, $jsonValue)
{
    return ucfirst($propName);
}

$jm = new JsonMapper();
$jm->undefinedPropertyHandler = 'fixPropName';
$jm->map(...);

Missing properties

.. note::
This only works when $bStrictObjectTypeChecking__ stays enabled.

.. __: #prop-bstrictobjecttypechecking

Properties in your PHP classes can be marked as "required" by
putting @required in their docblock:

.. code:: php

/**
 * @var string
 * @required
 */
public $someDatum;

When the JSON data do not contain this property, JsonMapper will throw
a JsonMapper_Exception when $bExceptionOnMissingData is activated:

.. code:: php

$jm = new JsonMapper();
$jm->bExceptionOnMissingData = true;
$jm->map(...);

Option $bRemoveUndefinedAttributes causes JsonMapper to remove properties
from the final object if they have not been in the JSON data:

.. code:: php

$jm = new JsonMapper();
$jm->bRemoveUndefinedAttributes = true;
$jm->map(...);

.. _prop-bignorevisibility:

Private properties and functions

You can allow mapping to private and protected properties and
setter methods by setting $bIgnoreVisibility to true:

.. code:: php

$jm = new JsonMapper();
$jm->bIgnoreVisibility = true;
$jm->map(...);

.. _prop-bstrictobjecttypechecking:

Simple types instead of objects

When a variable's type is a class and JSON data is a simple type
like string, JsonMapper can pass this value to the class' constructor
when configured to do so:

.. code:: php

$jm = new JsonMapper();
$jm->bStrictObjectTypeChecking = false;
$jm->map(...);

This can be used to automatically initialize DateTime objects
from date strings.

Disabling this strict object type checks may lead to problems, though:

  • When a class does not have a constructor or no constructor parameter,
    the value will get lost
  • When the constructor has more than 1 required parameter, it will crash.
  • When the constructor's parameter type does not match the one of the
    data in JSON, it will crash
  • @required properties will not be filled

.. note::
The default value changed from false to true in version 5 to
increase security.

Now you have to opt in if you want to pass simple types to
the class constructor.

Passing arrays to map()

You may wish to pass array data into map() that you got by calling

.. code:: php

json_decode($jsonString, true)

By default, JsonMapper will throw an exception because map() requires
an object as first parameter.
You can circumvent that by setting $bEnforceMapType to false:

.. code:: php

$jm = new JsonMapper();
$jm->bEnforceMapType = false;
$jm->map(...);

Post-mapping callback

JsonMapper is able to call a custom method directly on each object after
mapping it is finished:

.. code:: php

$jm = new JsonMapper();
$jm->postMappingMethod = 'afterMapping';
$jm->map(...);

Now afterMapping() is called on each mapped object
(if the class has that method).

You may pass additional arguments to the post-mapping callback:

.. code:: php

$jm = new JsonMapper();
$jm->postMappingMethod = 'afterMapping';
$jm->postMappingMethodArguments = [23, 'foo'];
$jm->map(...);

.. _installation:

============
Installation

Via Composer from Packagist__::

$ composer require netresearch/jsonmapper

__ https://packagist.org/packages/netresearch/jsonmapper

================
Related software

Alternatives

  • Jackson's data binding__ for Java
  • Johannes Schmitt Serializer__ for PHP
  • metassione__ for PHP
  • Cartographer__ for PHP
  • Data Transfer Object__ for PHP
  • Valinor__ for PHP
  • An equally named JsonMapper__ library that has dependencies.

__ https://fasterxml.github.io/jackson-databind/
__ http://jmsyst.com/libs/serializer
__ https://github.com/drbonzo/metassione
__ https://github.com/jonjomckay/cartographer
__ https://github.com/spatie/data-transfer-object
__ https://github.com/cuyz/valinor
__ https://jsonmapper.net/

================
About JsonMapper

License

JsonMapper is licensed under the OSL 3.0__.

__ http://opensource.org/licenses/osl-3.0

Coding style

JsonMapper follows the PEAR Coding Standards__.

__ http://pear.php.net/manual/en/standards.php

Author

Christian Weiske, cweiske.de

__ mailto:cweiske+jsonmapper@cweiske.de
__ http://cweiske.de/

cweiske/jsonmapper | GitHunt