3.2. Serialization JSON

3.2.1. JSON Syntax

  • JavaScript Object Notation

  • JSON format is similar to dict notation in Python

  • Differences:

    • Coma , is not allowed after the last element in list or object

    • Fields are enclosed only by double quote " character

    • true and false is always lower-cased

    • Instead of None there is null

    • camelCase is convention, although snake_case is also valid

Example JSON file:

[{"sepalLength": 5.1, "sepalWidth": 3.5, "petalLength": 1.4, "petalWidth": 0.2, "species": "setosa"},
 {"sepalLength": 4.9, "sepalWidth": 3.0, "petalLength": 1.4, "petalWidth": 0.2, "species": "setosa"},
 {"sepalLength": false, "sepalWidth": true, "petalLength": null, "petalWidth": 0.2, "species": null}]

JSON or Python list[dict]?:

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

JSON or Python list[dict]?:

{"mission": "Ares 3",
 "launch_date": "2035-06-29T00:00:00+00:00",
 "destination": "Mars",
 "destination_landing": "2035-11-07T00:00:00+00:00",
 "destination_location": "Acidalia Planitia",
 "crew": [{"astronaut": "Melissa Lewis", "date_of_birth": "1995-07-15"},
          {"astronaut": "Rick Martinez", "date_of_birth": "1996-01-21"},
          {"astronaut": "Alex Vogel", "date_of_birth": "1994-11-15"},
          {"astronaut": "Chris Beck", "date_of_birth": "1999-08-02"},
          {"astronaut": "Beth Johansen", "date_of_birth": "2006-05-09"},
          {"astronaut": "Mark Watney", "date_of_birth": "1994-10-12"}]}

JSON or Python list[dict]?:

[{"firstname": "Jan", "lastname": "Twardowski", "addresses": [
    {"street": "Kamienica Pod św. Janem Kapistranem", "city": "Kraków", "post_code": "31-008", "region": "Małopolskie", "country": "Poland"}]},

 {"firstname": "José", "lastname": "Jiménez", "addresses": [
    {"street": "2101 E NASA Pkwy", "city": "Houston", "post_code": 77058, "region": "Texas", "country": "USA"},
    {"street": "", "city": "Kennedy Space Center", "post_code": 32899, "region": "Florida", "country": "USA"}]},

 {"firstname": "Mark", "lastname": "Watney", "addresses": [
    {"street": "4800 Oak Grove Dr", "city": "Pasadena", "post_code": 91109, "region": "California", "country": "USA"},
    {"street": "2825 E Ave P", "city": "Palmdale", "post_code": 93550, "region": "California", "country": "USA"}]},

 {"firstname": "Иван", "lastname": "Иванович", "addresses": [
    {"street": "", "city": "Космодро́м Байкону́р", "post_code": "", "region": "Кызылординская область", "country": "Қазақстан"},
    {"street": "", "city": "Звёздный городо́к", "post_code": 141160, "region": "Московская область", "country": "Россия"}]},

 {"firstname": "Melissa", "lastname": "Lewis", "addresses": []},

 {"firstname": "Alex", "lastname": "Vogel", "addresses": [
    {"street": "Linder Hoehe", "city": "Köln", "post_code": 51147, "region": "North Rhine-Westphalia", "country": "Germany"}]}]

3.2.2. Mapping to JSON

  • json.dumps(DATA: dict) -> str

  • json.loads(DATA: str) -> dict

Serializing mapping to JSON:

import json


DATA = {'firstname': 'Mark',
        'lastname': 'Watney'}

result = json.dumps(DATA)

type(result)
# <class 'str'>
print(result)
# {"firstname": "Mark", "lastname": "Watney"}

Deserializing mapping from JSON:

import json


DATA = '{"firstname": "Mark", "lastname": "Watney"}'

result = json.loads(DATA)

type(result)
# <class 'dict'>
print(result)
# {'firstname': 'Mark', 'lastname': 'Watney'}

3.2.3. Sequence to JSON

  • json.dumps(data: Sequence[dict]) -> str

  • json.loads(data: str) -> list[dict]

Serializing sequence to JSON:

import json


DATA = [{'firstname': 'Melissa', 'lastname': 'Lewis'},
        {'firstname': 'Rick', 'lastname': 'Martinez'},
        {'firstname': 'Mark', 'lastname': 'Watney'}]

result = json.dumps(DATA)

type(result)
# <class 'str'>
print(result)
# [{"firstname": "Melissa", "lastname": "Lewis"},
#  {"firstname": "Rick", "lastname": "Martinez"},
#  {"firstname": "Mark", "lastname": "Watney"}]
import json


DATA = '[{"firstname": "Melissa", "lastname": "Lewis"}, {"firstname": "Rick", "lastname": "Martinez"}, {"firstname": "Mark", "lastname": "Watney"}]'

result = json.loads(DATA)

type(result)
# <class 'list'>
print(result)
# [{'firstname': 'Melissa', 'lastname': 'Lewis'},
#  {'firstname': 'Rick', 'lastname': 'Martinez'},
#  {'firstname': 'Mark', 'lastname': 'Watney'}]
import json

DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
        (5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor')]

json.dumps(DATA)
# [["Sepal length", "Sepal width", "Petal length", "Petal width", "Species"],
#  [5.8, 2.7, 5.1, 1.9, "virginica"],
#  [5.1, 3.5, 1.4, 0.2, "setosa"],
#  [5.7, 2.8, 4.1, 1.3, "versicolor"]]
import json
from pprint import pprint


DATA = '[["Sepal length", "Sepal width", "Petal length", "Petal width", "Species"], [5.8, 2.7, 5.1, 1.9, "virginica"], [5.1, 3.5, 1.4, 0.2, "setosa"], [5.7, 2.8, 4.1, 1.3, "versicolor"]]'

json.loads(DATA)
# [['Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'],
#  [5.8, 2.7, 5.1, 1.9, 'virginica'],
#  [5.1, 3.5, 1.4, 0.2, 'setosa'],
#  [5.7, 2.8, 4.1, 1.3, 'versicolor']]

3.2.4. Write JSON File

  • json.dump(data: dict, file: TextIOWrapper) -> None

  • file extension .json

Serialize to JSON:

import json

FILE = r'_temporary.json'

DATA = {'firstname': 'Mark',
        'lastname': 'Watney'}

with open(FILE, mode='w') as file:
    json.dump(DATA, file)


print(open(FILE).read())
# {"firstname": "Mark", "lastname": "Watney"}

3.2.5. Read JSON File

  • json.load(file: TextIOWrapper) -> dict

  • file extension .json

Serialize to JSON:

import json


FILE = r'_temporary.json'
DATA = '{"firstname": "Mark", "lastname": "Watney"}'
open(FILE, mode='w').write(DATA)


with open(FILE) as file:
    result = json.load(file)


type(result)
# <class 'dict'>
print(result)
# {'firstname': 'Mark', 'lastname': 'Watney'}

3.2.6. Datetime to JSON

  • problem with date, datetime, time

Exception during encoding datetime:

from datetime import date
import json

DATA = {'firstname': 'Mark',
        'lastname': 'Watney',
        'date_of_birth': date(1994, 10, 12)}

result = json.dumps(DATA)
# Traceback (most recent call last):
# TypeError: Object of type date is not JSON serializable

from datetime import date
import json

DATA = {'firstname': 'Mark',
        'lastname': 'Watney',
        'date_of_birth': date(1994, 10, 12)}
from datetime import date
import json

DATA = {'firstname': 'Mark',
        'lastname': 'Watney',
        'date_of_birth': date(1994, 10, 12)}

json.JSONEncoder.default = lambda self, date: date.isoformat()
result = json.dumps(DATA)

type(result)
# <class 'str'>
print(result)
# {"firstname": "Mark", "lastname": "Watney", "date_of_birth": "1994-10-12"}

Encoder will be used, when standard procedure fails:

from datetime import date
import json

DATA = {'firstname': 'Mark',
        'lastname': 'Watney',
        'date_of_birth': date(1994, 10, 12),
        'first_mission': datetime(1969, 7, 21, 2, 56, 15)}


class MyEncoder(json.JSONEncoder):
    def default(self, value):
        if isinstance(value, datetime):
            return value.strftime('%Y-%m-%dT%H:%M:%S.%f+00:00')
        if isinstance(value, date):
            return value.strftime('%Y-%m-%d')


result = json.dumps(DATA, cls=MyEncoder)

type(result)
# <class 'str'>
print(result)
# {"firstname": "Mark",
#  "lastname": "Watney",
#  "date_of_birth": "1994-10-12",
#  "first_mission": "1969-07-21T02:56:15.000000+00:00"}

3.2.7. JSON to Datetime

Simple loading returns str not datetime or date:

import json


DATA = '{"firstname": "Mark", "lastname": "Watney", "date_of_birth": "1994-10-12"}'

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

Simple loading returns str not datetime or date:

import json
from datetime import date


DATA = '{"firstname": "Mark", "lastname": "Watney", "date_of_birth": "1994-10-12"}'

def mydecoder(data: dict) -> dict:
    for field, value in data.items():
        if field == 'date_of_birth':
            data[field] = date.fromisoformat(value)
    return data

result = json.loads(DATA, object_hook=mydecoder)

type(result)
# <class 'dict'>
print(result)
# {'firstname': 'Mark', 'lastname': 'Watney', 'date_of_birth': datetime.date(1994, 10, 12)}

Decoding datetime and date:

from datetime import datetime, timezone
import json


DATA = '{"name": "Jan Twardowski", "date": "1961-04-12", "datetime": "1969-07-21T02:56:15.000Z"}'


class MyDecoder(json.JSONDecoder):
    def __init__(self):
        super().__init__(object_hook=self.default)

    def default(self, result: dict) -> dict:
        for field, value in result.items():
            if field in ['date', 'date_of_birth']:
                value = datetime.strptime(value, '%Y-%m-%d').date()
            if field in ['datetime']:
                value = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc)
            result[field] = value
        return result


result = json.loads(DATA, cls=MyDecoder)
pprint(result)
# {'name': 'Jan Twardowski',
#  'date': datetime.date(1961, 4, 12),
#  'datetime': datetime.datetime(1969, 7, 21, 2, 56, 15, tzinfo=datetime.timezone.utc)}
from datetime import datetime, date, timezone
import json

FILE = '_temporary.json'

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


class MyEncoder(json.JSONEncoder):
    def default(self, value: datetime) -> str:
        return value.isoformat()


class MyDecoder(json.JSONDecoder):
    date_of_birth: date
    launch_date: datetime
    destination_landing: datetime

    def __init__(self) -> None:
        super().__init__(object_hook=lambda data: {
                field: getattr(self, method)(value)
                for field, value in data.items()
                if (method := self.__annotations__.get(field, str).__name__)})

    def datetime(self, value: str) -> date:
        return datetime.fromisoformat(value)

    def date(self, value: str) -> date:
        return datetime.fromisoformat(value).date()

    def str(self, value: str) -> str:
        return value


result = json.dumps(DATA, cls=MyEncoder)
type(result)
# <class 'str'>
print(result)
# {"mission": "Ares 3",
#  "launch_date": "2035-06-29T00:00:00+00:00",
#  "destination": "Mars",
#  "destination_landing": "2035-11-07T00:00:00+00:00",
#  "destination_location": "Acidalia Planitia",
#  "crew": [{"astronaut": "Melissa Lewis", "date_of_birth": "1995-07-15"},
#           {"astronaut": "Rick Martinez", "date_of_birth": "1996-01-21"},
#           {"astronaut": "Alex Vogel", "date_of_birth": "1994-11-15"},
#           {"astronaut": "Chris Beck", "date_of_birth": "1999-08-02"},
#           {"astronaut": "Beth Johansen", "date_of_birth": "2006-05-09"},
#           {"astronaut": "Mark Watney", "date_of_birth": "1994-10-12"}]}


result = json.loads(result, cls=MyDecoder)
type(result)
# <class 'dict'>
print(result)
# {'mission': 'Ares 3',
#  'launch_date': datetime.datetime(2035, 6, 29, 0, 0, tzinfo=datetime.timezone.utc),
#  'destination': 'Mars',
#  'destination_landing': datetime.datetime(2035, 11, 7, 0, 0, tzinfo=datetime.timezone.utc),
#  'destination_location': 'Acidalia Planitia',
#  'crew': [{'astronaut': 'Melissa Lewis', 'date_of_birth': datetime.date(1995, 7, 15)},
#           {'astronaut': 'Rick Martinez', 'date_of_birth': datetime.date(1996, 1, 21)},
#           {'astronaut': 'Alex Vogel', 'date_of_birth': datetime.date(1994, 11, 15)},
#           {'astronaut': 'Chris Beck', 'date_of_birth': datetime.date(1999, 8, 2)},
#           {'astronaut': 'Beth Johansen', 'date_of_birth': datetime.date(2006, 5, 9)},
#           {'astronaut': 'Mark Watney', 'date_of_birth': datetime.date(1994, 10, 12)}]}

3.2.8. Python Object to JSON

Encoding nested objects with relations to JSON:

import json
from dataclasses import dataclass


@dataclass
class Mission:
    year: int
    name: str


@dataclass
class Astronaut:
    name: str
    missions: list[Mission]



CREW = [
    Astronaut('Melissa Lewis', []),
    Astronaut('Mark Watney', missions=[
            Mission(2035, 'Ares 3')]),
    Astronaut('Jan Twardowski', missions=[
            Mission(1969, 'Apollo 18'),
            Mission(2024, 'Artemis 3')]),
]


class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        result = obj.__dict__
        result['__class_name__'] = obj.__class__.__name__
        return result


result = json.dumps(CREW, cls=MyEncoder, sort_keys=True, indent=2)
print(type(result))
# <class 'str'>
print(result)
# [
#   {
#     "__class_name__": "Astronaut",
#     "missions": [],
#     "name": "Melissa Lewis"
#   },
#   {
#     "__class_name__": "Astronaut",
#     "missions": [
#       {
#         "__class_name__": "Mission",
#         "name": "Ares 3",
#         "year": 2035
#       }
#     ],
#     "name": "Mark Watney"
#   },
#   {
#     "__class_name__": "Astronaut",
#     "missions": [
#       {
#         "__class_name__": "Mission",
#         "name": "Apollo 18",
#         "year": 1969
#       },
#       {
#         "__class_name__": "Mission",
#         "name": "Artemis 3",
#         "year": 2024
#       }
#     ],
#     "name": "Jan Twardowski"
#   }
# ]

3.2.9. JSON to Python Object

Encoding nested objects with relations to JSON:

from dataclasses import dataclass
import json


DATA = """[
    {"__class_name__": "Astronaut", "name": "Melissa Lewis", "missions": []},
    {"__class_name__": "Astronaut", "name": "Mark Watney", "missions": [{"__class_name__": "Mission", "name": "Ares 3", "year": 2035}]},
    {"__class_name__": "Astronaut", "name": "Jan Twardowski", "missions": [
        {"__class_name__": "Mission", "name": "Apollo 18", "year": 1969},
        {"__class_name__": "Mission", "name": "Artemis 3", "year": 2024}]}]"""


@dataclass
class Mission:
    year: int
    name: str


@dataclass
class Astronaut:
    name: str
    missions: list[Mission]


class MyDecoder(json.JSONDecoder):
    def __init__(self):
        super().__init__(object_hook=self.default)

    def default(self, obj):
        class_name = obj.pop('__class_name__')
        cls = globals()[class_name]
        return cls(**obj)


result = json.loads(DATA, cls=MyDecoder)
print(type(result))
# <class 'list'>
print(result)
# [Astronaut(name='Melissa Lewis', missions=[]),
#  Astronaut(name='Mark Watney', missions=[
#       Mission(year=2035, name='Ares 3')]),
#  Astronaut(name='Jan Twardowski', missions=[
#       Mission(year=1969, name='Apollo 18'),
#       Mission(year=2024, name='Artemis 3')])]

3.2.10. Pretty Printing JSON

  • JSON can be minified to save space for network transmission

  • It is not very readable

Minified JSON file:

$ DATA='https://raw.githubusercontent.com/AstroMatt/book-python/master/_data/json/iris.json'
$ curl $DATA
[{"sepalLength":5.1,"sepalWidth":3.5,"petalLength":1.4,"petalWidth":0.2,"species":"setosa"},{"sepalLength":4.9,"sepalWidth":3,"petalLength":1.4,"petalWidth":0.2,"species":"setosa"},{"sepalLength":4.7,"sepalWidth":3.2,"petalLength":1.3,"petalWidth":0.2,"species":"setosa"},{"sepalLength":4.6,"sepalWidth":3.1,"petalLength":1.5,"petalWidth":0.2,"species":"setosa"},{"sepalLength":5,"sepalWidth":3.6,"petalLength":1.4,"petalWidth":0.2,"species":"setosa"},{"sepalLength":5.4,"sepalWidth":3.9,"petalLength":1.7,"petalWidth":0.4,"species":"setosa"},{"sepalLength":4.6,"sepalWidth":3.4,"petalLength":1.4,"petalWidth":0.3,"species":"setosa"},{"sepalLength":5,"sepalWidth":3.4,"petalLength":1.5,"petalWidth":0.2,"species":"setosa"},{"sepalLength":4.4,"sepalWidth":2.9,"petalLength":1.4,"petalWidth":0.2,"species":"setosa"},{"sepalLength":4.9,"sepalWidth":3.1,"petalLength":1.5,"petalWidth":0.1,"species":"setosa"},{"sepalLength":7,"sepalWidth":3.2,"petalLength":4.7,"petalWidth":1.4,"species":"versicolor"},{"sepalLength":6.4,"sepalWidth":3.2,"petalLength":4.5,"petalWidth":1.5,"species":"versicolor"},{"sepalLength":6.9,"sepalWidth":3.1,"petalLength":4.9,"petalWidth":1.5,"species":"versicolor"},{"sepalLength":5.5,"sepalWidth":2.3,"petalLength":4,"petalWidth":1.3,"species":"versicolor"},{"sepalLength":6.5,"sepalWidth":2.8,"petalLength":4.6,"petalWidth":1.5,"species":"versicolor"},{"sepalLength":5.7,"sepalWidth":2.8,"petalLength":4.5,"petalWidth":1.3,"species":"versicolor"},{"sepalLength":6.3,"sepalWidth":3.3,"petalLength":4.7,"petalWidth":1.6,"species":"versicolor"},{"sepalLength":4.9,"sepalWidth":2.4,"petalLength":3.3,"petalWidth":1,"species":"versicolor"},{"sepalLength":6.6,"sepalWidth":2.9,"petalLength":4.6,"petalWidth":1.3,"species":"versicolor"},{"sepalLength":5.2,"sepalWidth":2.7,"petalLength":3.9,"petalWidth":1.4,"species":"versicolor"},{"sepalLength":6.3,"sepalWidth":3.3,"petalLength":6,"petalWidth":2.5,"species":"virginica"},{"sepalLength":5.8,"sepalWidth":2.7,"petalLength":5.1,"petalWidth":1.9,"species":"virginica"},{"sepalLength":7.1,"sepalWidth":3,"petalLength":5.9,"petalWidth":2.1,"species":"virginica"},{"sepalLength":6.3,"sepalWidth":2.9,"petalLength":5.6,"petalWidth":1.8,"species":"virginica"},{"sepalLength":6.5,"sepalWidth":3,"petalLength":5.8,"petalWidth":2.2,"species":"virginica"},{"sepalLength":7.6,"sepalWidth":3,"petalLength":6.6,"petalWidth":2.1,"species":"virginica"},{"sepalLength":4.9,"sepalWidth":2.5,"petalLength":4.5,"petalWidth":1.7,"species":"virginica"},{"sepalLength":7.3,"sepalWidth":2.9,"petalLength":6.3,"petalWidth":1.8,"species":"virginica"},{"sepalLength":6.7,"sepalWidth":2.5,"petalLength":5.8,"petalWidth":1.8,"species":"virginica"},{"sepalLength":7.2,"sepalWidth":3.6,"petalLength":6.1,"petalWidth":2.5,"species":"virginica"}]

Pretty Printing JSON:

$ DATA='https://raw.githubusercontent.com/AstroMatt/book-python/master/_data/json/iris.json'
$ curl $DATA |python -m json.tool
[
    {
        "petalLength": 1.4,
        "petalWidth": 0.2,
        "sepalLength": 5.1,
        "sepalWidth": 3.5,
        "species": "setosa"
    },
    {
        "petalLength": 1.4,
        "petalWidth": 0.2,
        "sepalLength": 4.9,
        "sepalWidth": 3,
        "species": "setosa"
    },
...

json.tool checks JSON syntax validity:

$ echo '{"sepalLength":5.1,"sepalWidth":3.5,}' | python -m json.tool
Expecting property name enclosed in double quotes: line 1 column 37 (char 36)

3.2.11. Assignments

Code 3.26. Solution
"""
* Assignment: Serialization JSON Dump
* Complexity: easy
* Lines of code: 4 lines
* Time: 8 min

English:
    1. Use data from "Given" section (see below)
    2. Extract from input a header and data
    3. Create `result: list[dict]`
        a. key - name from the header
        b. value - measurement or species
    4. Write structure to file `iris_serialize.json` in JSON format
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Z danych wydziel nagłówek i pomiary
    3. Wygeneruj `result: list[dict]`
        a. klucz - nazwa z nagłówka
        b. wartość - wyniki pomiarów lub gatunek
    4. Zapisz strukturę do pliku `iris_serialize.json` w formacie JSON
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> type(result)
    <class 'list'>
    >>> len(result) > 0
    True
    >>> all(type(row) is dict
    ...     for row in result)
    True
    >>> from os import remove
    >>> result = open(FILE).read()
    >>> remove(FILE)
    >>> print(result)  # doctest: +NORMALIZE_WHITESPACE
    [{"Sepal length": 5.8, "Sepal width": 2.7, "Petal length": 5.1, "Petal width": 1.9, "Species": "virginica"},
     {"Sepal length": 5.1, "Sepal width": 3.5, "Petal length": 1.4, "Petal width": 0.2, "Species": "setosa"},
     {"Sepal length": 5.7, "Sepal width": 2.8, "Petal length": 4.1, "Petal width": 1.3, "Species": "versicolor"},
     {"Sepal length": 6.3, "Sepal width": 2.9, "Petal length": 5.6, "Petal width": 1.8, "Species": "virginica"},
     {"Sepal length": 6.4, "Sepal width": 3.2, "Petal length": 4.5, "Petal width": 1.5, "Species": "versicolor"},
     {"Sepal length": 4.7, "Sepal width": 3.2, "Petal length": 1.3, "Petal width": 0.2, "Species": "setosa"},
     {"Sepal length": 7.0, "Sepal width": 3.2, "Petal length": 4.7, "Petal width": 1.4, "Species": "versicolor"},
     {"Sepal length": 7.6, "Sepal width": 3.0, "Petal length": 6.6, "Petal width": 2.1, "Species": "virginica"},
     {"Sepal length": 4.9, "Sepal width": 3.0, "Petal length": 1.4, "Petal width": 0.2, "Species": "setosa"}]
"""


# Given
import json

FILE = '_temporary.json'

DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
        (5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor'),
        (6.3, 2.9, 5.6, 1.8, 'virginica'),
        (6.4, 3.2, 4.5, 1.5, 'versicolor'),
        (4.7, 3.2, 1.3, 0.2, 'setosa'),
        (7.0, 3.2, 4.7, 1.4, 'versicolor'),
        (7.6, 3.0, 6.6, 2.1, 'virginica'),
        (4.9, 3.0, 1.4, 0.2, 'setosa')]


Code 3.27. Solution
"""
* Assignment: Serialization JSON Load
* Complexity: easy
* Lines of code: 4 lines
* Time: 8 min

English:
    1. Use data from "Given" section (see below)
    2. Read data from `FILE`
    3. Convert data to `result: list[tuple]`
    4. Do not add header as a first line
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Odczytaj dane z pliku `FILE`
    3. Przekonwertuj dane do `result: list[tuple]`
    4. Nie dodawaj nagłówka jako pierwsza linia
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> type(result)
    <class 'list'>
    >>> len(result) > 0
    True
    >>> all(type(row) is tuple
    ...     for row in result)
    True
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [(5.8, 2.7, 5.1, 1.9, 'virginica'),
     (5.1, 3.5, 1.4, 0.2, 'setosa'),
     (5.7, 2.8, 4.1, 1.3, 'versicolor'),
     (6.3, 2.9, 5.6, 1.8, 'virginica'),
     (6.4, 3.2, 4.5, 1.5, 'versicolor'),
     (4.7, 3.2, 1.3, 0.2, 'setosa'),
     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
     (7.6, 3.0, 6.6, 2.1, 'virginica'),
     (4.9, 3.0, 1.4, 0.2, 'setosa')]
     >>> from os import remove
     >>> remove(FILE)
"""


import json

FILE = r'_temporary.json'

DATA = """[{"Sepal length": 5.8, "Sepal width": 2.7, "Petal length": 5.1, "Petal width": 1.9, "Species": "virginica"},
{"Sepal length": 5.1, "Sepal width": 3.5, "Petal length": 1.4, "Petal width": 0.2, "Species": "setosa"},
{"Sepal length": 5.7, "Sepal width": 2.8, "Petal length": 4.1, "Petal width": 1.3, "Species": "versicolor"},
{"Sepal length": 6.3, "Sepal width": 2.9, "Petal length": 5.6, "Petal width": 1.8, "Species": "virginica"},
{"Sepal length": 6.4, "Sepal width": 3.2, "Petal length": 4.5, "Petal width": 1.5, "Species": "versicolor"},
{"Sepal length": 4.7, "Sepal width": 3.2, "Petal length": 1.3, "Petal width": 0.2, "Species": "setosa"},
{"Sepal length": 7.0, "Sepal width": 3.2, "Petal length": 4.7, "Petal width": 1.4, "Species": "versicolor"},
{"Sepal length": 7.6, "Sepal width": 3.0, "Petal length": 6.6, "Petal width": 2.1, "Species": "virginica"},
{"Sepal length": 4.9, "Sepal width": 3.0, "Petal length": 1.4, "Petal width": 0.2, "Species": "setosa"}]"""

with open(FILE, mode='w') as file:
    file.write(DATA)

result = list()


Code 3.28. Solution
"""
* Assignment: Serialization JSON Datetime
* Complexity: easy
* Lines of code: 15 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. Save data to file in JSON format
    3. Read data from file
    4. Recreate data structure
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Zapisz dane do pliku w formacie JSON
    3. Odczytaj dane z pliku
    4. Odtwórz strukturę danych
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isclass
    >>> isclass(Encoder)
    True
    >>> isclass(Decoder)
    True
    >>> issubclass(Encoder, json.JSONEncoder)
    True
    >>> issubclass(Decoder, json.JSONDecoder)
    True
    >>> with open(FILE, mode='w') as file:
    ...     json.dump(DATA, file, cls=Encoder)
    >>> with open(FILE, mode='r') as file:
    ...     result = json.load(file, cls=Decoder)
    >>> from os import remove
    >>> remove(FILE)
    >>> type(result)
    <class 'dict'>
    >>> len(result) > 0
    True
    >>> all(type(key) is str
    ...     and type(value) in (str, datetime, list)
    ...     for key, value in result.items())
    True
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    {'mission': 'Ares 3',
     'launch_date': datetime.datetime(2035, 6, 29, 0, 0, tzinfo=datetime.timezone.utc),
     'destination': 'Mars',
     'destination_landing': datetime.datetime(2035, 11, 7, 0, 0, tzinfo=datetime.timezone.utc),
     'destination_location': 'Acidalia Planitia',
     'crew': [{'astronaut': 'Melissa Lewis', 'date_of_birth': datetime.date(1995, 7, 15)},
              {'astronaut': 'Rick Martinez', 'date_of_birth': datetime.date(1996, 1, 21)},
              {'astronaut': 'Alex Vogel', 'date_of_birth': datetime.date(1994, 11, 15)},
              {'astronaut': 'Chris Beck', 'date_of_birth': datetime.date(1999, 8, 2)},
              {'astronaut': 'Beth Johansen', 'date_of_birth': datetime.date(2006, 5, 9)},
              {'astronaut': 'Mark Watney', 'date_of_birth': datetime.date(1994, 10, 12)}]}
"""


# Given
from datetime import datetime, date, timezone
import json

FILE = '_temporary.json'

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


class Encoder(json.JSONEncoder):
    def default(self, value: datetime) -> str:
        return ...


class Decoder(json.JSONDecoder):
    def __init__(self) -> None:
        super().__init__(object_hook=self.default)

    def default(self, data: dict) -> dict:
        ...


Code 3.29. Solution
"""
* Assignment: Serialization JSON Object
* Complexity: medium
* Lines of code: 15 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. Convert from JSON format to Python
    3. Reading file create instances of `Setosa`, `Virginica`, `Versicolor`
       classes based on value in field "species"
    4. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Przekonwertuj dane z JSON do Python
    3. Czytając plik twórz obiekty klas `Setosa`, `Virginica`, `Versicolor`
       w zależności od wartości pola "species"
    4. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> type(result)
    <class 'list'>
    >>> len(result) > 0
    True
    >>> all(type(row) in (Setosa, Virginica, Versicolor)
    ...     for row in result)
    True
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Virginica(sepalLength=5.8, sepalWidth=2.7, petalLength=5.1, petalWidth=1.9),
     Setosa(sepalLength=5.1, sepalWidth=3.5, petalLength=1.4, petalWidth=0.2),
     Versicolor(sepalLength=5.7, sepalWidth=2.8, petalLength=4.1, petalWidth=1.3),
     Virginica(sepalLength=6.3, sepalWidth=2.9, petalLength=5.6, petalWidth=1.8),
     Versicolor(sepalLength=6.4, sepalWidth=3.2, petalLength=4.5, petalWidth=1.5),
     Setosa(sepalLength=4.7, sepalWidth=3.2, petalLength=1.3, petalWidth=0.2),
     Versicolor(sepalLength=7.0, sepalWidth=3.2, petalLength=4.7, petalWidth=1.4),
     Virginica(sepalLength=7.6, sepalWidth=3.0, petalLength=6.6, petalWidth=2.1),
     Setosa(sepalLength=4.9, sepalWidth=3.0, petalLength=1.4, petalWidth=0.2)]
"""


# Given
import json
from dataclasses import dataclass


FILE = r'_temporary.json'
DATA = """
    [{"sepalLength": 5.8, "sepalWidth": 2.7, "petalLength": 5.1, "petalWidth": 1.9, "species": "virginica"},
     {"sepalLength": 5.1, "sepalWidth": 3.5, "petalLength": 1.4, "petalWidth": 0.2, "species": "setosa"},
     {"sepalLength": 5.7, "sepalWidth": 2.8, "petalLength": 4.1, "petalWidth": 1.3, "species": "versicolor"},
     {"sepalLength": 6.3, "sepalWidth": 2.9, "petalLength": 5.6, "petalWidth": 1.8, "species": "virginica"},
     {"sepalLength": 6.4, "sepalWidth": 3.2, "petalLength": 4.5, "petalWidth": 1.5, "species": "versicolor"},
     {"sepalLength": 4.7, "sepalWidth": 3.2, "petalLength": 1.3, "petalWidth": 0.2, "species": "setosa"},
     {"sepalLength": 7.0, "sepalWidth": 3.2, "petalLength": 4.7, "petalWidth": 1.4, "species": "versicolor"},
     {"sepalLength": 7.6, "sepalWidth": 3.0, "petalLength": 6.6, "petalWidth": 2.1, "species": "virginica"},
     {"sepalLength": 4.9, "sepalWidth": 3.0, "petalLength": 1.4, "petalWidth": 0.2, "species": "setosa"}]"""


@dataclass
class Iris:
    sepalLength: float
    sepalWidth: float
    petalLength: float
    petalWidth: float


class Setosa(Iris):
    pass


class Virginica(Iris):
    pass


class Versicolor(Iris):
    pass


result: list


Code 3.30. Solution
"""
* Assignment: Serialization JSON HTTP
* Complexity: hard
* Lines of code: 15 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    1. Use `requests` library (requires installation)
    2. Download data from https://api.github.com/users
    3. Model data as class `User`
    4. Iterate over records and create instances of this class
    5. Collect all instances to one list
    6. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    1. Użyj biblioteki `requests` (wymagana instalacja)
    2. Pobierz dane z https://api.github.com/users
    3. Zamodeluj dane za pomocą klasy `User`
    4. Iterując po rekordach twórz instancje tej klasy
    5. Zbierz wszystkie instancje do jednej listy
    6. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> type(result)
    <class 'list'>
    >>> len(result) > 0
    True
    >>> all(type(row) is User
    ...     for row in result)
    True
    >>> result[0]  # doctest: +NORMALIZE_WHITESPACE
    User(login='mojombo',
         id=1,
         url='https://api.github.com/users/mojombo',
         node_id='MDQ6VXNlcjE=',
         avatar_url='https://avatars.githubusercontent.com/u/1?v=4',
         gravatar_id='',
         html_url='https://github.com/mojombo',
         followers_url='https://api.github.com/users/mojombo/followers',
         following_url='https://api.github.com/users/mojombo/following{/other_user}',
         gists_url='https://api.github.com/users/mojombo/gists{/gist_id}',
         starred_url='https://api.github.com/users/mojombo/starred{/owner}{/repo}',
         subscriptions_url='https://api.github.com/users/mojombo/subscriptions',
         organizations_url='https://api.github.com/users/mojombo/orgs',
         repos_url='https://api.github.com/users/mojombo/repos',
         events_url='https://api.github.com/users/mojombo/events{/privacy}',
         received_events_url='https://api.github.com/users/mojombo/received_events',
         type='User',
         site_admin=False)
"""


# Given
from dataclasses import dataclass
import requests


class User:
    pass


DATA = requests.get('https://api.github.com/users').json()