5. JSON Serialization

Format JSON jest podobny do zapisu dict w Python, ale różni się:

  • nie może być przecinka po ostatnim elemencie list
  • zawsze są stosowane podwójne cudzysłowia
  • true i false jest pisane małymi literami
  • zamiast None jest null

5.1. JSON Serialization of simple objects

5.1.1. Serializing to JSON

Code Listing 5.1. Serializing to JSON
import json

DATA = {
    'first_name': 'José',
    'last_name': 'Jiménez'
}

output = json.dumps(DATA)

print(output)
# '{"first_name": "Jos\u00e9", "last_name": "Jim\u00e9nez"}'

5.1.2. Deserializing from JSON

Code Listing 5.2. Deserializing from JSON
import json


DATA = '{"first_name": "José", "last_name": "Jiménez"}'

output = json.loads(DATA)

print(output)
# {
#     'first_name': 'Jos\u00e9',
#     'last_name': 'Jim\u00e9nez'
# }

5.2. Serializing datetime and date

5.2.1. Encoding datetime and date

Code Listing 5.3. Exception during encoding datetime
from datetime import datetime, date
import json


DATA = {
    'email': '[email protected]',
    'date': date(1961, 4, 12),
    'datetime': datetime(1969, 7, 21, 14, 56, 15),
}

output = json.dumps(DATA)
# TypeError: Object of type date is not JSON serializable
Code Listing 5.4. Encoding datetime and date
from datetime import datetime, date
import json


DATA = {
    'email': '[email protected]',
    'date': date(1961, 4, 12),
    'datetime': datetime(1969, 7, 21, 14, 56, 15),
}


def encoder(self, value):

    if isinstance(value, datetime):
        return f'{value:%Y-%m-%dT%H:%M:%S.%fZ}'
    elif isinstance(value, date):
        return f'{value:%Y-%m-%d}'


json.JSONEncoder.default = encoder

output = json.dumps(DATA)

print(output)
# '{"email": "[email protected]", "date": "1961-04-12", "datetime": "1969-07-21T14:56:15.000Z"}'

5.2.2. Decoding datetime and date

Code Listing 5.5. Simple loading returns str not datetime or date
import json


DATA = '{"email": "[email protected]", "date": "1961-04-12", "datetime": "1969-07-21T14:56:15.000Z"}'


output = json.loads(DATA)

print(output)
# {
#     'email': '[email protected]',
#     'date': '1961-04-12',
#     'datetime': '1969-07-21T14:56:15.000Z',
# }
Code Listing 5.6. Decoding datetime and date
from datetime import datetime, timezone
import json


DATA = '{"email": "[email protected]", "date": "1961-04-12", "datetime": "1969-07-21T14:56:15.000Z"}'


def decoder(obj):
    for key, value in obj.items():

        if key == 'datetime':
            dt = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
            obj['datetime'] = dt.replace(tzinfo=timezone.utc)

        elif key == 'date':
            dt = datetime.strptime(value, '%Y-%m-%d')
            obj['date'] = dt.replace(tzinfo=timezone.utc).date()

    return obj


output = json.loads(DATA, object_hook=decoder)

print(output)
# {
#     'email': '[email protected]',
#     'date': datetime.date(1961, 4, 12),
#     'datetime': datetime.datetime(1969, 7, 21, 14, 56, 15, tzinfo=datetime.timezone.utc),
# }

5.3. Serializing objects

5.3.1. Encoding objects

Code Listing 5.7. Encoding objects to JSON
import json
from dataclasses import dataclass


@dataclass
class Address:
    street: str
    city: str
    state: str
    zipcode: str
    country: str


@dataclass
class Contact:
    first_name: str
    last_name: str
    addresses: tuple = ()


addressbook = [
    Contact(first_name='Matt', last_name='Kowalski', addresses=(
        Address(street='2101 E NASA Pkwy', city='Houston', state='Texas', zipcode='77058', country='USA'),
        Address(street='', city='Kennedy Space Center', zipcode='32899', state='Florida', country='USA'),
        Address(street='4800 Oak Grove Dr', city='Pasadena', zipcode='91109', state='California', country='USA'),
        Address(street='2825 E Ave P', city='Palmdale', state='California', zipcode='93550', country='USA'),
    )),
    Contact(first_name='José', last_name='Jiménez'),
    Contact(first_name='Иван', last_name='Иванович', addresses=()),
]


def encoder(self, obj):
    result = obj.__dict__
    result['__type__'] = obj.__class__.__name__
    return result


json.JSONEncoder.default = encoder
output = json.dumps(addressbook)

print(output)
# [{"first_name": "Matt", "last_name": "Kowalski", "addresses": [{"street": "2101 E NASA Pkwy", "city": "Houston", "state": "Texas", "zipcode": "77058", "country": "USA", "__type__": "Address"}, {"street": "", "city": "Kennedy Space Center", "state": "Florida", "zipcode": "32899", "country": "USA", "__type__": "Address"}, {"street": "4800 Oak Grove Dr", "city": "Pasadena", "state": "California", "zipcode": "91109", "country": "USA", "__type__": "Address"}, {"street": "2825 E Ave P", "city": "Palmdale", "state": "California", "zipcode": "93550", "country": "USA", "__type__": "Address"}], "__type__": "Contact"}, {"first_name": "Jos\u00e9", "last_name": "Jim\u00e9nez", "addresses": [], "__type__": "Contact"}, {"first_name": "\u0418\u0432\u0430\u043d", "last_name": "\u0418\u0432\u0430\u043d\u043e\u0432\u0438\u0447", "addresses": [], "__type__": "Contact"}]

5.3.2. Decoding objects

Code Listing 5.8. Decoding objects from JSON
import json
import sys
from dataclasses import dataclass



DATA = """[{"first_name": "Matt", "last_name": "Kowalski", "addresses": [{"street": "2101 E NASA Pkwy", "city": "Houston", "state": "Texas", "zipcode": "77058", "country": "USA", "__type__": "Address"}, {"street": "Florida", "city": "Kennedy Space Center", "state": "", "zipcode": "32899", "country": "USA", "__type__": "Address"}, {"street": "4800 Oak Grove Dr", "city": "Pasadena", "state": "California", "zipcode": "91109", "country": "USA", "__type__": "Address"}, {"street": "2825 E Ave P", "city": "Palmdale", "state": "California", "zipcode": "93550", "country": "USA", "__type__": "Address"}], "__type__": "Contact"}, {"first_name": "Jos\u00e9", "last_name": "Jim\u00e9nez", "addresses": [], "__type__": "Contact"}, {"first_name": "\u0418\u0432\u0430\u043d", "last_name": "\u0418\u0432\u0430\u043d\u043e\u0432\u0438\u0447", "addresses": [], "__type__": "Contact"}]"""


@dataclass
class Address:
    street: str
    city: str
    state: str
    zipcode: str
    country: str


@dataclass
class Contact:
    first_name: str
    last_name: str
    addresses: tuple = ()


def decoder(obj):
    type = obj.pop('__type__')
    cls = getattr(sys.modules[__name__], type)
    return cls(**obj)


output = json.loads(DATA, object_hook=decoder)

print(output)
# [
#   Contact(first_name='Matt', last_name='Kowalski', addresses=[
#       Address(street='2101 E NASA Pkwy', city='Houston', state='Texas', zipcode='77058', country='USA'),
#       Address(street='', city='Kennedy Space Center', state='Florida', zipcode='32899', country='USA'),
#       Address(street='4800 Oak Grove Dr', city='Pasadena', state='California', zipcode='91109', country='USA'),
#       Address(street='2825 E Ave P', city='Palmdale', state='California', zipcode='93550', country='USA')]),
#   Contact(first_name='José', last_name='Jiménez', addresses=[]),
#   Contact(first_name='Иван', last_name='Иванович', addresses=[])
# ]

5.4. Class based encoders and decoders

5.4.1. Class based encoder

Code Listing 5.9. Class based encoder
from datetime import datetime, date
import json


DATA = {
    'email': '[email protected]',
    'date': date(1961, 4, 12),
    'datetime': datetime(1969, 7, 21, 14, 56, 15),
}


class DatetimeEncoder(json.JSONEncoder):
    def default(self, value):

        if isinstance(value, datetime):
            return f'{value:%Y-%m-%dT%H:%M:%S.%fZ}'
        elif isinstance(value, date):
            return f'{value:%Y-%m-%d}'


output = json.dumps(DATA, cls=DatetimeEncoder)

print(output)
# '{"email": "[email protected]", "date": "1961-04-12", "datetime": "1969-07-21T14:56:15.000Z"}'

5.4.2. Class based decoder

Code Listing 5.10. Class based decoder
import datetime
import json


DATA = '{"email": "[email protected]", "date": "1961-04-12", "datetime": "1969-07-21T14:56:15.000Z"}'


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

    def default(self, obj):
        for key, value in obj.items():

            if key == 'datetime':
                dt = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
                obj['datetime'] = dt.replace(tzinfo=datetime.timezone.utc)

            elif key == 'date':
                dt = datetime.datetime.strptime(value, '%Y-%m-%d')
                obj['date'] = dt.date()

        return obj


output = json.loads(DATA, cls=DatetimeDecoder)

print(output)
# {
#     'email': '[email protected]',
#     'date': date(1961, 4, 12),
#     'datetime': datetime(1969, 7, 21, 14, 56, 15, tzinfo=datetime.timezone.utc),
# }

5.5. Assignments

5.5.1. Date serialization

  1. Skopiuj do swojego pliku strukturę danych Code Listing 5.11.
  2. Zapisz ją do pliku json
  3. Wczytaj ją z pliku json jako obiekty Pythona (ten sam efekt co na listingu)
About:
  • Filename: json_datetimes.py
  • Lines of code to write: 10 lines
  • Estimated time of completion: 15 min
The whys and wherefores:
 
  • Serializacja danych
  • Korzystanie z biblioteki JSON
  • Serializowanie zagnieżdżonych dat i dat z czasem
Code Listing 5.11. Sample Python data JSON
from datetime import datetime, date


DATA = {
    "astronaut": {
        "date": date(1961, 4, 12),
        "person": "[email protected]"
    },
    "flight": [
        {"datetime": datetime(1969, 7, 21, 14, 56, 15), "action": "landing"}
    ]
}

5.5.2. Serializing custom class to JSON

  1. Skopiuj do pliku iris.json dane z listingu Code Listing 5.12.
  2. Stwórz klasy Setosa, Virginica, Versicolor
  3. Czytając dane z pliku twórz obiekty powyższych klas w zależności od wyniku pomiaru (pole “species”)
About:
  • Filename: json_objects.py
  • Lines of code to write: 15 lines
  • Estimated time of completion: 20 min
The whys and wherefores:
 
  • Serializacja danych
  • Korzystanie z biblioteki JSON
  • Serializowanie zagnieżdżonych obiektów
Code Listing 5.12. Sample Python data JSON
[
  {"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": 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.0, "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.0, "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.0, "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.0, "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.0, "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.0, "petalWidth": 2.5, "species": "virginica"},
  {"sepalLength": 5.8, "sepalWidth": 2.7, "petalLength": 5.1, "petalWidth": 1.9, "species": "virginica"},
  {"sepalLength": 7.1, "sepalWidth": 3.0, "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.0, "petalLength": 5.8, "petalWidth": 2.2, "species": "virginica"},
  {"sepalLength": 7.6, "sepalWidth": 3.0, "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"}
]