8.5. JSON Decoder¶
8.5.1. Problem¶
Problem with
date
,datetime
,time
,timedelta
Python does not decode values automatically
8.5.2. SetUp¶
>>> from pprint import pprint
>>> from datetime import date
>>> import json
8.5.3. Problem¶
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "1994-10-12"
... }"""
>>>
>>> result = json.loads(DATA)
>>> print(result)
{'firstname': 'Mark',
'lastname': 'Watney',
'birthday': '1994-10-12'}
8.5.4. Function Decoder¶
This works for simple (flat) data
This works for nested data structures
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "1994-10-12"
... }"""
>>>
>>>
>>> def decoder(data: dict) -> dict:
... for field, value in data.items():
... if field == 'birthday':
... data[field] = date.fromisoformat(value)
... return data
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> print(result)
{'firstname': 'Mark',
'lastname': 'Watney',
'birthday': datetime.date(1994, 10, 12)}
8.5.5. Context Dependency Injection¶
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "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 == 'birthday':
... data[field] = date.fromisoformat(value)
... return data
>>>
>>>
>>> result = json.loads(DATA, cls=Decoder)
>>> print(result)
{'firstname': 'Mark',
'lastname': 'Watney',
'birthday': datetime.date(1994, 10, 12)}
8.5.6. Use Case - 0x01¶
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",
... "birthday": "1994-10-12",
... "launch": "1969-07-21T02:56:15",
... "landing": "12:30:00",
... "flight_time": 15552000,
... "duration": 13
... }"""
>>>
>>>
>>> def decoder(obj: dict) -> dict:
... obj['birthday'] = date.fromisoformat(obj['birthday'])
... 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',
'birthday': 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)}
8.5.7. Use Case - 0x02¶
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",
... "birthday": "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 | {
... 'birthday': date.fromisoformat(obj['birthday']),
... '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',
'birthday': 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)}
8.5.8. Use Case - 0x03¶
>>> from datetime import datetime, date, time, timedelta
>>> import json
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "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 'birthday':
... 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',
'birthday': 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)}
8.5.9. Use Case - 0x04¶
>>> from datetime import datetime, date, time, timedelta
>>> import json
>>> from pprint import pprint
>>>
>>>
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "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 birthday(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',
'birthday': 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)}
8.5.10. Use Case - 0x05¶
>>> from datetime import datetime, date, time, timedelta
>>> import json
>>>
>>>
>>> DATA = """{
... "firstname": "Mark",
... "lastname": "Watney",
... "birthday": "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 = {
... 'birthday': 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',
'birthday': 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)}
8.5.11. Assignments¶
"""
* Assignment: JSON Decoder Function
* Complexity: easy
* Lines of code: 8 lines
* Time: 5 min
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ść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> 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', 'born': datetime.date(1995, 7, 15)},
{'name': 'Rick Martinez', 'born': datetime.date(1996, 1, 21)},
{'name': 'Alex Vogel', 'born': datetime.date(1994, 11, 15)},
{'name': 'Chris Beck', 'born': datetime.date(1999, 8, 2)},
{'name': 'Beth Johanssen', 'born': datetime.date(2006, 5, 9)},
{'name': 'Mark Watney', 'born': 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", "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"}]}"""
# JSON decoder
def decoder(data: dict) -> dict:
...
# JSON decoded DATA
# type: dict[str, str|list|datetime]
result = ...