5.4. JSON Object

5.4.1. 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 = vars(obj)
        result['__type__'] = obj.__class__.__name__
        return result


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

5.4.2. JSON to Python Object

Encoding nested objects with relations to JSON:

from dataclasses import dataclass
import json


DATA = """[
    {"__type__": "Astronaut", "name": "Melissa Lewis", "missions": []},
    {"__type__": "Astronaut", "name": "Mark Watney", "missions": [{"__type__": "Mission", "name": "Ares 3", "year": 2035}]},
    {"__type__": "Astronaut", "name": "Jan Twardowski", "missions": [
        {"__type__": "Mission", "name": "Apollo 18", "year": 1969},
        {"__type__": "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):
        clsname = obj.pop('__type__')
        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')])]

5.4.3. Assignments

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

English:
    1. Convert from JSON format to Python
    2. Create instances of `Setosa`, `Virginica`, `Versicolor`
       classes based on value in field "species"
    3. Add instances to `result: list[Setosa|Virginica|Versicolor]`
    4. Run doctests - all must succeed

Polish:
    1. Przekonwertuj dane z JSON do Python
    2. Twórz obiekty klas `Setosa`, `Virginica`, `Versicolor`
       w zależności od wartości pola "species"
    3. Dodawaj instancje do `result: list[Setosa|Virginica|Versicolor]`
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * `dict.pop()`
    * `globals()`
    * Assignment Expression

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> 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)]
"""

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 5.5. Solution
"""
* Assignment: JSON Object Dataclass
* Complexity: easy
* Lines of code: 15 lines
* Time: 13 min

English:
    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. Run doctests - all must succeed

Polish:
    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. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> 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,
         node_id='MDQ6VXNlcjE=',
         avatar_url='https://avatars.githubusercontent.com/u/1?v=4',
         gravatar_id='',
         url='https://api.github.com/users/mojombo',
         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)
"""

import requests


DATA = 'https://raw.githubusercontent.com/AstroMatt/book-python/master/_data/json/github-users.json'
DATA = requests.get(DATA).json()
result: list = []


class User:
    pass