18.6. JSON Object

18.6.1. SetUp

>>> from pprint import pprint
>>> from dataclasses import dataclass
>>> import json

18.6.2. Encode Object

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
>>>
>>> DATA = User('Mark', 'Watney')
>>>
>>> data = vars(DATA)
>>> result = json.dumps(data)
>>>
>>> print(result)
{"firstname": "Mark", "lastname": "Watney"}

18.6.3. Decode Object

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
>>>
>>> DATA = """{
...   "firstname": "Mark",
...   "lastname": "Watney"
... }"""
>>>
>>> data = json.loads(DATA)
>>> result = User(**data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')

18.6.4. Object Encoder

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
>>>
>>> DATA = User('Mark', 'Watney')
>>>
>>>
>>> def encoder(obj):
...     return vars(obj)
>>>
>>>
>>> result = json.dumps(DATA, default=encoder)
>>> print(result)
{"firstname": "Mark", "lastname": "Watney"}

18.6.5. Object Decoder

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
>>>
>>> DATA = """{
...   "firstname": "Mark",
...   "lastname": "Watney"
... }"""
>>>
>>>
>>> def decoder(data):
...     return User(**data)
>>>
>>>
>>> result =  json.loads(DATA, object_hook=decoder)
>>> print(result)
User(firstname='Mark', lastname='Watney')

18.6.6. Encode Object with Relation

>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[Group]
>>>
>>>
>>> DATA = [
...     User('Mark', 'Watney', groups=[
...         Group(gid=1, name='users')]),
...     User('Melissa', 'Lewis', groups=[
...         Group(gid=1, name='users'),
...         Group(gid=2, name='admins')]),
...     User('Rick', 'Martinez', groups=[]),
... ]
>>>
>>>
>>> def encoder(obj):
...     data = {'_clsname': obj.__class__.__name__}
...     return data | vars(obj)
>>>
>>>
>>> result = json.dumps(DATA, default=encoder, indent=2)
>>> print(result)
[
  {
    "_clsname": "User",
    "firstname": "Mark",
    "lastname": "Watney",
    "groups": [
      {
        "_clsname": "Group",
        "gid": 1,
        "name": "users"
      }
    ]
  },
  {
    "_clsname": "User",
    "firstname": "Melissa",
    "lastname": "Lewis",
    "groups": [
      {
        "_clsname": "Group",
        "gid": 1,
        "name": "users"
      },
      {
        "_clsname": "Group",
        "gid": 2,
        "name": "admins"
      }
    ]
  },
  {
    "_clsname": "User",
    "firstname": "Rick",
    "lastname": "Martinez",
    "groups": []
  }
]

18.6.7. Decode

Encoding nested objects with relations to JSON:

>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     role: str
...     groups: list[Group]
>>>
>>>
>>> DATA = (
...     '[{"_clsname":"User","firstname":"Mark","lastname":"Watney","rol'
...     'e":"user","groups":[{"_clsname":"Group","gid":1,"name":"users"}'
...     ']},{"_clsname":"User","firstname":"Melissa","lastname":"Lewis",'
...     '"role":"admin","groups":[{"_clsname":"Group","gid":1,"name":"us'
...     'ers"},{"_clsname":"Group","gid":2,"name":"admins"}]},{"_clsname'
...     '":"User","firstname":"Rick","lastname":"Martinez","role":"guest'
...     '","groups":[]}]'
... )
>>>
>>>
>>> def decoder(obj):
...     clsname = obj.pop('_clsname')
...     cls = globals()[clsname]
...     return cls(**obj)
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>> pprint(result, width=72)
[User(firstname='Mark',
      lastname='Watney',
      role='user',
      groups=[Group(gid=1, name='users')]),
 User(firstname='Melissa',
      lastname='Lewis',
      role='admin',
      groups=[Group(gid=1, name='users'), Group(gid=2, name='admins')]),
 User(firstname='Rick', lastname='Martinez', role='guest', groups=[])]

18.6.8. Use Case - 1

>>> import json
>>> from dataclasses import dataclass, field
>>> from pprint import pprint
>>>
>>>
>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>> @dataclass
... class User:
...     lastname: str
...     firstname: str
...     groups: list[Group]
>>>
>>>
>>> DATA = [
...     User('Mark', 'Watney', groups=[
...         Group(gid=1, name='users')]),
...     User('Melissa', 'Lewis', groups=[
...         Group(gid=1, name='users'),
...         Group(gid=2, name='admins')]),
...     User('Rick', 'Martinez', groups=[]),
... ]
>>> class Encoder(json.JSONEncoder):
...     def default(self, obj):
...         data = vars(obj)
...         data['_clsname'] = obj.__class__.__name__
...         return data
>>>
>>>
>>> result = json.dumps(DATA, cls=Encoder)
>>> pprint(result, width=72)
('[{"lastname": "Mark", "firstname": "Watney", "groups": [{"gid": 1, '
 '"name": "users", "_clsname": "Group"}], "_clsname": "User"}, '
 '{"lastname": "Melissa", "firstname": "Lewis", "groups": [{"gid": 1, '
 '"name": "users", "_clsname": "Group"}, {"gid": 2, "name": "admins", '
 '"_clsname": "Group"}], "_clsname": "User"}, {"lastname": "Rick", '
 '"firstname": "Martinez", "groups": [], "_clsname": "User"}]')
>>> class Decoder(json.JSONDecoder):
...     def __init__(self):
...         super().__init__(object_hook=self.default)
...
...     def default(self, data: dict) -> dict:
...         clsname = data.pop('_clsname')
...         cls = globals()[clsname]
...         return cls(**data)
>>>
>>>
>>> result = json.loads(result, cls=Decoder)
>>> pprint(result, width=72)
[User(lastname='Mark',
      firstname='Watney',
      groups=[Group(gid=1, name='users')]),
 User(lastname='Melissa',
      firstname='Lewis',
      groups=[Group(gid=1, name='users'), Group(gid=2, name='admins')]),
 User(lastname='Rick', firstname='Martinez', groups=[])]

18.6.9. 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 Object Factory
# - Difficulty: medium
# - Lines: 5
# - Minutes: 5

# %% English
# 1. Convert from JSON format to Python using decoder function
# 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 używając dekodera funkcyjnego
# 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ść

# %% Hints
# - `dict.pop()`
# - `globals()`
# - `cls(**obj)`

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

>>> assert type(result)
>>> assert len(result) == 9

>>> classes = (Setosa, Virginica, Versicolor)
>>> assert all(type(row) in classes for row in result)

>>> result[0]
Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9)

>>> result[1]
Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2)
"""

import json
from dataclasses import dataclass

FILE = r'_temporary.json'

DATA = (
    '[{"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_widt'
    'h":1.9,"species":"virginica"},{"sepal_length":5.1,"sepal_width":3.5,"'
    'petal_length":1.4,"petal_width":0.2,"species":"setosa"},{"sepal_lengt'
    'h":5.7,"sepal_width":2.8,"petal_length":4.1,"petal_width":1.3,"specie'
    's":"versicolor"},{"sepal_length":6.3,"sepal_width":2.9,"petal_length"'
    ':5.6,"petal_width":1.8,"species":"virginica"},{"sepal_length":6.4,"se'
    'pal_width":3.2,"petal_length":4.5,"petal_width":1.5,"species":"versic'
    'olor"},{"sepal_length":4.7,"sepal_width":3.2,"petal_length":1.3,"peta'
    'l_width":0.2,"species":"setosa"},{"sepal_length":7.0,"sepal_width":3.'
    '2,"petal_length":4.7,"petal_width":1.4,"species":"versicolor"},{"sepa'
    'l_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_l'
    'ength":1.4,"petal_width":0.2,"species":"setosa"}]'
)

@dataclass
class Iris:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float


class Setosa(Iris):
    pass


class Virginica(Iris):
    pass


class Versicolor(Iris):
    pass


# JSON decoded DATA
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 Object Dataclass
# - Difficulty: easy
# - Lines: 15
# - Minutes: 13

# %% English
# 1. Variable `DATA` is a JSON downloaded from https://api.github.com/users
# 3. Using `dataclass` 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. Zmienna `DATA` to JSON pobrany z https://api.github.com/users
# 3. Używając `dataclass` 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ść

# %% Hints
# - `User(**obj)`

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

>>> 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='myuser',
     id=1,
     node_id='MDQ6VXNlcjE=',
     avatar_url='https://avatars.githubusercontent.com/u/1?v=4',
     gravatar_id='',
     url='https://api.github.com/users/myuser',
     html_url='https://github.com/myuser',
     followers_url='https://api.github.com/users/myuser/followers',
     following_url='https://api.github.com/users/myuser/following',
     gists_url='https://api.github.com/users/myuser/gists{/gist_id}',
     starred_url='https://api.github.com/users/myuser/starred',
     subscriptions_url='https://api.github.com/users/myuser/subscriptions',
     organizations_url='https://api.github.com/users/myuser/orgs',
     repos_url='https://api.github.com/users/myuser/repos',
     events_url='https://api.github.com/users/myuser/events{/privacy}',
     received_events_url='https://api.github.com/users/myuser',
     type='User',
     site_admin=False)
"""
import json
from dataclasses import dataclass

DATA = (
    '[{"login":"myuser","id":1,"node_id":"MDQ6VXNlcjE=","avatar_url":"http'
    's://avatars.githubusercontent.com/u/1?v=4","gravatar_id":"","url":"ht'
    'tps://api.github.com/users/myuser","html_url":"https://github.com/myu'
    'ser","followers_url":"https://api.github.com/users/myuser/followers",'
    '"following_url":"https://api.github.com/users/myuser/following","gist'
    's_url":"https://api.github.com/users/myuser/gists{/gist_id}","starred'
    '_url":"https://api.github.com/users/myuser/starred","subscriptions_ur'
    'l":"https://api.github.com/users/myuser/subscriptions","organizations'
    '_url":"https://api.github.com/users/myuser/orgs","repos_url":"https:/'
    '/api.github.com/users/myuser/repos","events_url":"https://api.github.'
    'com/users/myuser/events{/privacy}","received_events_url":"https://api'
    '.github.com/users/myuser","type":"User","site_admin":false},{"login":'
    '"defunkt","id":2,"node_id":"MDQ6VXNlcjI=","avatar_url":"https://avata'
    'rs.githubusercontent.com/u/2?v=4","gravatar_id":"","url":"https://api'
    '.github.com/users/defunkt","html_url":"https://github.com/defunkt","f'
    'ollowers_url":"https://api.github.com/users/defunkt/followers","follo'
    'wing_url":"https://api.github.com/users/defunkt/following","gists_url'
    '":"https://api.github.com/users/defunkt/gists{/gist_id}","starred_url'
    '":"https://api.github.com/users/defunkt/starred","subscriptions_url":'
    '"https://api.github.com/users/defunkt/subscriptions","organizations_u'
    'rl":"https://api.github.com/users/defunkt/orgs","repos_url":"https://'
    'api.github.com/users/defunkt/repos","events_url":"https://api.github.'
    'com/users/defunkt/events{/privacy}","received_events_url":"https://ap'
    'i.github.com/users/defunkt","type":"User","site_admin":false},{"login'
    '":"pjhyett","id":3,"node_id":"MDQ6VXNlcjM=","avatar_url":"https://ava'
    'tars.githubusercontent.com/u/3?v=4","gravatar_id":"","url":"https://a'
    'pi.github.com/users/pjhyett","html_url":"https://github.com/pjhyett",'
    '"followers_url":"https://api.github.com/users/pjhyett/followers","fol'
    'lowing_url":"https://api.github.com/users/pjhyett/following","gists_u'
    'rl":"https://api.github.com/users/pjhyett/gists{/gist_id}","starred_u'
    'rl":"https://api.github.com/users/pjhyett/starred","subscriptions_url'
    '":"https://api.github.com/users/pjhyett/subscriptions","organizations'
    '_url":"https://api.github.com/users/pjhyett/orgs","repos_url":"https:'
    '//api.github.com/users/pjhyett/repos","events_url":"https://api.githu'
    'b.com/users/pjhyett/events{/privacy}","received_events_url":"https://'
    'api.github.com/users/pjhyett","type":"User","site_admin":false}]')


@dataclass
class User:
    pass

# JSON decoded DATA
result = ...