jsonplus 0.8.0
Custom datatypes (like datetime) serialization to/from JSON.
License: MIT
Keywords: json custom python type serialization deserialization datetime set

JSON+
=====

.. image:: https://img.shields.io/pypi/v/jsonplus.svg
    :target: https://pypi.python.org/pypi/jsonplus

.. image:: https://img.shields.io/pypi/l/jsonplus.svg
    :target: https://pypi.python.org/pypi/jsonplus

.. image:: https://img.shields.io/pypi/wheel/jsonplus.svg
    :target: https://pypi.python.org/pypi/jsonplus

.. image:: https://img.shields.io/pypi/pyversions/jsonplus.svg
    :target: https://pypi.python.org/pypi/jsonplus

.. image:: https://api.travis-ci.org/randomir/jsonplus.svg?branch=master
    :target: https://travis-ci.org/randomir/jsonplus


Serialization of Python types to JSON that "just works".

Forget errors like::

    TypeError: datetime.datetime(...) is not JSON serializable

In addition to (de-)serialization of basic types (provided by simplejson_), jsonplus_
provides support for **exact** (de-)serialization of other commonly used types, like:
``tuple``/``namedtuple``, ``set``/``frozenset``, ``complex``/``decimal.Decimal``/``fractions.Fraction``,
and ``datetime``/``date``/``time``/``timedelta``.

If the exact representation of types is not your cup of tea, and all you wish
for is the ``json.dumps`` to work on your data structure with non-basic types,
accepting the loss of "type-precision" along the way, than you can use the
**compatibility** mode (thread-local ``jsonplus.prefer_compat()``, or
per-call override ``jsonplus.dumps(..., exact=False)``).

.. _simplejson: https://simplejson.readthedocs.io/en/latest/#encoders-and-decoders
.. _jsonplus: https://pypi.python.org/pypi/jsonplus/


Installation
------------

``jsonplus`` is available as a Python package. To install it, simply type::

    $ pip install jsonplus


Usage
-----

You can treat ``jsonplus`` as a friendly *drop-in* replacement for ``json``/``simplejson``.

.. code-block:: python

    >>> import jsonplus as json

    >>> x = json.loads('{"a":1,"b":2}')
    >>> y = json.dumps(x, indent=4)
    >>> z = json.pretty(x)


Examples
--------

Let's start with that beloved ``datetime``.

.. code-block:: python

    >>> import jsonplus as json

    >>> from datetime import datetime
    >>> json.dumps({
    ...     "x": [4,3],
    ...     "t": datetime.now()
    ... })
    '{"x":[4,3],"t":{"__class__":"datetime","__value__":"2013-09-06T23:38:55.819791"}}'
    
    >>> json.loads(_)
    {u'x': [4, 3], u't': datetime.datetime(2013, 9, 6, 23, 38, 55, 819791)}

Similarly for other ``datetime.*`` types, like ``timedelta``, ``date``, and ``time``:

.. code-block:: python

    >>> from datetime import timedelta, date, time
    >>> print(json.pretty({"dt": timedelta(0, 1234567, 123), "d": date.today(), "t": datetime.now().time()}))
    {
        "d": {
            "__class__": "date",
            "__value__": "2013-09-22"
        },
        "dt": {
            "__class__": "timedelta",
            "__value__": {
                "days": 14,
                "microseconds": 123,
                "seconds": 24967
            }
        },
        "t": {
            "__class__": "time",
            "__value__": "23:33:16.335360"
        }
    }

Also, ``set`` and ``complex``:

.. code-block:: python

    >>> json.dumps([set(range(3)), 1+2j])
    '[{"__class__":"set","__value__":[0,1,2]},{"__class__":"complex","__value__":{"real":1.0,"imag":2.0}}]'
    
    >>> json.loads(_)
    [set([0, 1, 2]), (1+2j)]

``tuple`` and ``namedtuple`` are also preserved:

.. code-block:: python

    >>> from collections import namedtuple
    >>> Point = namedtuple('Point', ['x', 'y'])

    >>> data = json.pretty({"vect": (1, 2, 3), "dot": Point(3, 4)})
    >>> print(data)
    {
        "dot": {
            "__class__": "namedtuple",
            "__value__": {
                "fields": [
                    "x",
                    "y"
                ],
                "name": "Point",
                "values": [
                    3,
                    4
                ]
            }
        },
        "vect": {
            "__class__": "tuple",
            "__value__": [
                1,
                2,
                3
            ]
        }
    }

    >>> json.loads(data)
    {'vect': (1, 2, 3), 'dot': Point(x=3, y=4)}


Compatibility mode
------------------

All types supported in the exact mode are also supported in the compatibility
mode. JSON representation differs, however.

In the exact mode, *type* and *value* are encoded with ``JSON Object``'s
``__class__`` and ``__value__`` keys, while in the compatibility mode, 
**values are "rounded off" to the closest JSON type**.

For example, ``tuple`` and ``set`` are represented with ``JSON Array``, and
``namedtuple`` is coded as a plain ``JSON Object``. ``Decimal`` is
represented as ``JSON Number`` with arbitrary precision (which is lost if
decoded as ``float``).

To switch between the **exact** and **compatibility** modes, use the 
(thread-local) functions ``prefer_exact()`` and ``prefer_compat()``, or call
``dumps(..., exact=False)``:

.. code-block:: python

    >>> import jsonplus as json

    >>> json.prefer_compat()
    # or:
    >>> json.prefer(json.COMPAT)
    # per-instance override:
    >>> json.dumps(obj, exact=False)

    # to go back to (default) exact coding:
    >>> json.prefer_exact()

The above ``tuple``/``namedtuple``/``datetime`` examples run in the compatibility 
coding mode result with:

.. code-block:: python

    >>> json.prefer_compat()

    >>> print(json.pretty({"vect": (1, 2, 3), "dot": Point(3, 4)}))
    {
        "point": {
            "x": 3,
            "y": 4
        },
        "vector": [
            1,
            2,
            3
        ]
    }

    >>> json.dumps({"now": datetime.now()})
    '{"now":"2017-01-26T00:37:40.293963"}'

So, to be able to properly decode values in the compatibility mode, some 
additional context will have to be provided to the decoder.


Adding user types
-----------------

Support for user/custom types can easily be added with `@jsonplus.encoder` and
`@jsonplus.decoder` decorators.

For example, to enable serialization of your type named ``mytype`` in exact mode
(to add compat-mode serialization, append ``exact=False`` in decorator):

.. code-block:: python

    @jsonplus.encoder('mytype')
    def mytype_exact_encoder(myobj):
        return myobj.to_json()

.. code-block:: python

    @jsonplus.decoder('mytype')
    def mytype_decoder(value):
        return mytype(value, reconstruct=True, ...)

If detection of object class is more complex than a simple classname comparison,
if you need to use a predicate function, simply add ``predicate=...`` to the ``encoder``
decorator. For example:

.. code-block:: python

    @jsonplus.encoder('BaseClass', lambda obj: isinstance(obj, BaseClass))
    def all_derived_classes_encoder(derived):
        return derived.base_encoder()