7.4. JSON Encoder

  • Problem with date, datetime, time, timedelta

  • Exception during encoding datetime

  • Encoder will be used, when standard procedure fails

7.4.1. SetUp

>>> from datetime import date
>>> import json

7.4.2. Problem

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> result = json.dumps(DATA)
Traceback (most recent call last):
TypeError: Object of type date is not JSON serializable

7.4.3. Default Function with Lambda

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> json.dumps(DATA, default=lambda x: x.isoformat())
'{"firstname": "Mark", "lastname": "Watney", "born": "1994-10-12"}'

7.4.4. Default Function with If

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> def encoder(x):
...     if type(x) is date:
...         return x.isoformat()
>>>
>>>
>>> json.dumps(DATA, default=encoder)
'{"firstname": "Mark", "lastname": "Watney", "born": "1994-10-12"}'

7.4.5. Default Function with Match

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> def encoder(x):
...     match x:
...         case date() | datetime() | time():
...             return x.isoformat()
...         case timedelta():
...             return x.total_seconds()
>>>
>>>
>>> json.dumps(DATA, default=encoder)
'{"firstname": "Mark", "lastname": "Watney", "born": "1994-10-12"}'

7.4.6. Monkey Patching with Lambda Expression

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> json.JSONEncoder.default = lambda self,x: x.isoformat()
>>>
>>> json.dumps(DATA)
'{"firstname": "Mark", "lastname": "Watney", "born": "1994-10-12"}'

7.4.7. Dependency Injection

>>> DATA = {'firstname': 'Mark',
...         'lastname': 'Watney',
...         'born': date(1994, 10, 12)}
>>>
>>>
>>> class MyEncoder(json.JSONEncoder):
...     def default(self, x):
...         if type(x) is date:
...             return x.isoformat()
>>>
>>>
>>> json.dumps(DATA, cls=MyEncoder)
'{"firstname": "Mark", "lastname": "Watney", "born": "1994-10-12"}'

7.4.8. Use Case - 0x01

>>> from datetime import datetime, date, time, timedelta
>>> import json
>>>
>>>
>>> DATA = {'name': 'Mark Watney',
...         'born': date(1994, 10, 12),
...         'launch': datetime(1969, 7, 21, 2, 56, 15),
...         'landing': time(12, 30),
...         'duration': timedelta(days=13)}
>>>
>>>
>>> class Encoder(json.JSONEncoder):
...     def default(self, value):
...         if type(value) in (datetime, date, time):
...             return value.isoformat()
...         if type(value) is timedelta:
...             return value.total_seconds()
>>>
>>>
>>> result = json.dumps(DATA, cls=Encoder, indent=2)
>>> print(result)
{
  "name": "Mark Watney",
  "born": "1994-10-12",
  "launch": "1969-07-21T02:56:15",
  "landing": "12:30:00",
  "duration": 1123200.0
}

7.4.9. Use Case - 0x02

>>> from datetime import datetime, date, time, timedelta
>>> import json
>>>
>>>
>>> DATA = {'name': 'Mark Watney',
...         'born': date(1994, 10, 12),
...         'launch': datetime(1969, 7, 21, 2, 56, 15),
...         'landing': time(12, 30),
...         'duration': timedelta(days=13)}
>>>
>>>
>>> def encoder(x):
...     match x:
...         case date() | datetime() | time():
...             return x.isoformat()
...         case timedelta():
...             return x.total_seconds()
>>>
>>>
>>> result = json.dumps(DATA, default=encoder, indent=2)
>>> print(result)
{
  "name": "Mark Watney",
  "born": "1994-10-12",
  "launch": "1969-07-21T02:56:15",
  "landing": "12:30:00",
  "duration": 1123200.0
}

7.4.10. Assignments

Code 7.15. Solution
"""
* Assignment: JSON Encoder Function
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

English:
    1. Define `result: str` with JSON encoded `DATA`
    2. Use encoder function
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj `result: str` z zakodowanym `DATA` w JSON
    2. Użyj enkodera funkcyjnego
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction
    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'
    >>> assert type(result) is str, \
    'Variable `result` has invalid type, should be str'
    >>> assert isfunction(encoder), \
    'Encoder must be a function'

    >>> print(result)  # doctest: +NORMALIZE_WHITESPACE
    {"mission": "Ares 3",
     "launch_date": "2035-06-29T00:00:00",
     "destination": "Mars",
     "destination_landing": "2035-11-07T00:00:00",
     "destination_location": "Acidalia Planitia",
     "crew": [{"name": "Melissa Lewis", "born": "1995-07-15"},
              {"name": "Rick Martinez", "born": "1996-01-21"},
              {"name": "Alex Vogel", "born": "1994-11-15"},
              {"name": "Chris Beck", "born": "1999-08-02"},
              {"name": "Beth Johanssen", "born": "2006-05-09"},
              {"name": "Mark Watney", "born": "1994-10-12"}]}
"""

import json
from datetime import date, datetime
from typing import Any


DATA = {
    'mission': 'Ares 3',
    'launch_date': datetime(2035, 6, 29),
    'destination': 'Mars',
    'destination_landing': datetime(2035, 11, 7),
    'destination_location': 'Acidalia Planitia',
    'crew': [
        {'name': 'Melissa Lewis', 'born': date(1995, 7, 15)},
        {'name': 'Rick Martinez', 'born': date(1996, 1, 21)},
        {'name': 'Alex Vogel', 'born': date(1994, 11, 15)},
        {'name': 'Chris Beck', 'born': date(1999, 8, 2)},
        {'name': 'Beth Johanssen', 'born': date(2006, 5, 9)},
        {'name': 'Mark Watney', 'born': date(1994, 10, 12)}]}


# JSON encoder
def encoder(value: Any) -> str:
    ...


# JSON encoded DATA
# type: str
result = ...