18.5. JSON Decoder

18.5.1. Problem

  • Problem with date, datetime, time, timedelta

  • Python does not decode values automatically

18.5.2. SetUp

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

18.5.3. Problem

>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12"
... }"""
>>>
>>> result = json.loads(DATA)
>>> print(result)   
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': '1994-10-12'}

18.5.4. Function Decoder

  • This works for simple (flat) data

  • This works for nested data structures

>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12"
... }"""
>>>
>>>
>>> def decoder(data: dict) -> dict:
...     for field, value in data.items():
...         if field == 'birthdate':
...             data[field] = date.fromisoformat(value)
...     return data
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> print(result)   
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12)}

18.5.5. Context Dependency Injection

>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12"
... }"""
>>>
>>>
>>> class Decoder(json.JSONDecoder):
...     def __init__(self):
...         super().__init__(object_hook=self.default)
...
...     def default(self, data: dict) -> dict:
...         for field, value in data.items():
...             if field == 'birthdate':
...                 data[field] = date.fromisoformat(value)
...         return data
>>>
>>>
>>> result = json.loads(DATA, cls=Decoder)
>>> print(result)   
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12)}

18.5.6. Use Case - 1

  • This works for simple (flat) data

  • This won't work for nested data structures

>>> import json
>>> from datetime import datetime, date, time, timedelta
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12",
...     "launch": "1969-07-21T02:56:15",
...     "landing": "12:30:00",
...     "flight_time": 15552000,
...     "duration": 13
... }"""
>>>
>>>
>>> def decoder(obj: dict) -> dict:
...     obj['birthdate'] = date.fromisoformat(obj['birthdate'])
...     obj['launch'] = datetime.fromisoformat(obj['launch'])
...     obj['landing'] = time.fromisoformat(obj['landing'])
...     obj['flight_time'] = timedelta(seconds=obj['flight_time'])
...     obj['duration'] = timedelta(days=obj['duration'])
...     return obj
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> pprint(result, sort_dicts=False)
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12),
 'launch': datetime.datetime(1969, 7, 21, 2, 56, 15),
 'landing': datetime.time(12, 30),
 'flight_time': datetime.timedelta(days=180),
 'duration': datetime.timedelta(days=13)}

18.5.7. Use Case - 2

  • This works for simple (flat) data

  • This won't work for nested data structures

>>> import json
>>> from datetime import datetime, date, time, timedelta
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12",
...     "launch": "1969-07-21T02:56:15",
...     "landing": "12:30:00",
...     "flight_time": 15552000,
...     "duration": 13
... }"""
>>>
>>>
>>> def decoder(obj: dict) -> dict:
...     return obj | {
...         'birthdate': date.fromisoformat(obj['birthdate']),
...         'launch': datetime.fromisoformat(obj['launch']),
...         'landing': time.fromisoformat(obj['landing']),
...         'flight_time': timedelta(seconds=obj['flight_time']),
...         'duration': timedelta(days=obj['duration']),
...     }
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> pprint(result, sort_dicts=False)
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12),
 'launch': datetime.datetime(1969, 7, 21, 2, 56, 15),
 'landing': datetime.time(12, 30),
 'flight_time': datetime.timedelta(days=180),
 'duration': datetime.timedelta(days=13)}

18.5.8. Use Case - 3

>>> from datetime import datetime, date, time, timedelta
>>> import json
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12",
...     "launch": "1969-07-21T02:56:15",
...     "landing": "12:30:00",
...     "flight_time": 15552000,
...     "duration": 13
... }"""
>>>
>>>
>>> def decoder(x):
...     for key, value in x.items():
...         match key:
...             case 'birthdate':
...                 x[key] = date.fromisoformat(value)
...             case 'launch':
...                 x[key] = datetime.fromisoformat(value)
...             case 'landing':
...                 x[key] = time.fromisoformat(value)
...             case 'flight_time' | 'mission_time':
...                 x[key] = timedelta(seconds=float(value))
...             case 'duration':
...                 x[key] = timedelta(days=int(value))
...     return x
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> pprint(result, sort_dicts=False)
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12),
 'launch': datetime.datetime(1969, 7, 21, 2, 56, 15),
 'landing': datetime.time(12, 30),
 'flight_time': datetime.timedelta(days=180),
 'duration': datetime.timedelta(days=13)}

18.5.9. Use Case - 4

>>> from datetime import datetime, date, time, timedelta
>>> import json
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12",
...     "launch": "1969-07-21T02:56:15",
...     "landing": "12:30:00",
...     "flight_time": 15552000,
...     "duration": 13
... }"""
>>>
>>>
>>> class Decoder(json.JSONDecoder):
...     def __init__(self) -> None:
...         super().__init__(object_hook=lambda data: {
...             field: getattr(self, field)(value)
...             for field, value in data.items()})
...
...     def firstname(self, value: str) -> str:
...         return value
...
...     def lastname(self, value: str) -> str:
...         return value
...
...     def birthdate(self, value: str) -> date:
...         return date.fromisoformat(value)
...
...     def launch(self, value: str) -> datetime:
...         return datetime.fromisoformat(value)
...
...     def landing(self, value: str) -> time:
...         return time.fromisoformat(value)
...
...     def flight_time(self, value: str) -> timedelta:
...         return timedelta(seconds=float(value))
...
...     def duration(self, value: str) -> timedelta:
...         return timedelta(days=int(value))
>>>
>>>
>>> result = json.loads(DATA, cls=Decoder)
>>> pprint(result, sort_dicts=False)
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12),
 'launch': datetime.datetime(1969, 7, 21, 2, 56, 15),
 'landing': datetime.time(12, 30),
 'flight_time': datetime.timedelta(days=180),
 'duration': datetime.timedelta(days=13)}

18.5.10. Use Case - 5

>>> from datetime import datetime, date, time, timedelta
>>> import json
>>>
>>>
>>> DATA = """{
...     "firstname": "Mark",
...     "lastname": "Watney",
...     "birthdate": "1994-10-12",
...     "launch": "1969-07-21T02:56:15",
...     "landing": "12:30:00",
...     "flight_time": 15552000,
...     "duration": 13
... }"""
>>>
>>>
>>> class Decoder(json.JSONDecoder):
...     def __init__(self):
...         super().__init__(object_hook=lambda data: {
...                 field: self.default(field, value)
...                 for field, value in data.items()})
...
...     def default(self, field, value):
...         result = {
...             'birthdate': lambda x: date.fromisoformat(x),
...             'launch': lambda x: datetime.fromisoformat(x),
...             'landing': lambda x: time.fromisoformat(x),
...             'duration': lambda x: timedelta(days=x),
...             'flight_time': lambda x: timedelta(seconds=x),
...         }.get(field, value)
...         return result(value) if callable(result) else result
>>>
>>>
>>> result = json.loads(DATA, cls=Decoder)
>>> print(result)   
{'firstname': 'Mark',
 'lastname': 'Watney',
 'birthdate': datetime.date(1994, 10, 12),
 'launch': datetime.datetime(1969, 7, 21, 2, 56, 15),
 'landing': datetime.time(12, 30),
 'flight_time': datetime.timedelta(days=180),
 'duration': datetime.timedelta(days=13)}

18.5.11. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: JSON Decoder Function
# - Difficulty: easy
# - Lines: 8
# - Minutes: 5

# %% English
# 1. Define `result: dict` with decoded `DATA` from JSON
# 2. Use decoder function
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj `result: dict` z odkodowanym `DATA` z JSON
# 2. Użyj dekodera funkcyjnego
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `json.loads(object_hook=...)`
# - `dict.items()`
# - `datetime.fromisoformat()`
# - `date.fromisoformat()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isfunction
>>> assert isfunction(decoder), \
'Decoder must be a function'

>>> assert type(result) is dict, \
'Result must be a dict'
>>> assert len(result) > 0, \
'Result cannot be empty'
>>> assert all(type(key) is str
...            and type(value) in (str, datetime, list)
...            for key, value in result.items()), \
'All keys must be str and all values must be either str, datetime or list'

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

import json
from datetime import datetime, date


DATA = """
    {"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", "birthdate": "1995-07-15"},
              {"name": "Rick Martinez", "birthdate": "1996-01-21"},
              {"name": "Alex Vogel", "birthdate": "1994-11-15"},
              {"name": "Chris Beck", "birthdate": "1999-08-02"},
              {"name": "Beth Johanssen", "birthdate": "2006-05-09"},
              {"name": "Mark Watney", "birthdate": "1994-10-12"}]}"""


# JSON decoder
def decoder(obj):
    ...


# JSON decoded DATA
# type: dict[str, str|list|datetime]
result = ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: JSON Decoder Class
# - Difficulty: medium
# - Lines: 12
# - Minutes: 5

# %% English
# 1. Define `result: dict` with decoded `DATA` from JSON
# 2. Use decoder class
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj `result: dict` z odkodowanym `DATA` z JSON
# 2. Użyj dekodera klasowego
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `json.loads(object_hook=...)`
# - `dict.items()`
# - `super().__init__(object_hook=...)`
# - `datetime.fromisoformat()`
# - `date.fromisoformat()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass
>>> assert isclass(Decoder), \
'Decoder must be a class'
>>> assert issubclass(Decoder, json.JSONDecoder), \
'Decoder must inherit from `json.JSONDecoder`'

>>> assert type(result) is dict, \
'Result must be a dict'
>>> assert len(result) > 0, \
'Result cannot be empty'
>>> assert all(type(key) is str
...            and type(value) in (str, datetime, list)
...            for key, value in result.items()), \
'All keys must be str and all values must be either str, datetime or list'

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

import json
from datetime import date, datetime


DATA = """
    {"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", "birthdate": "1995-07-15"},
              {"name": "Rick Martinez", "birthdate": "1996-01-21"},
              {"name": "Alex Vogel", "birthdate": "1994-11-15"},
              {"name": "Chris Beck", "birthdate": "1999-08-02"},
              {"name": "Beth Johanssen", "birthdate": "2006-05-09"},
              {"name": "Mark Watney", "birthdate": "1994-10-12"}]}"""


# JSON decoder
class Decoder:
    ...


# JSON decoded DATA
# type: dict[str, str|list|datetime]
result = ...