JSON JavaScript Object Notation to lekki i prosty format zapisu danych chętnie wykorzystywany do ich wymiany. Jego forma i czytelność jest przystępna zarówno dla ludzi jak i maszyn, co więcej jest on obsługiwany pomiędzy różnymi językami programowania. Czynniki te ewidentnie zadecydowały o jego popularności.
Standard JSON został szczegółowo opisany w krótkim dokumentach:
W języku Python obsługa standardu JSON jest zaimplementowana w module json w
ramach standardowej biblioteki. Oficjalna dokumentacja modułu, json — JSON
encoder and decoder, stanowi
najlepsze źródło wiedzy i przykładów stosowania.
Współpracę Python’a z obsługą danych w formacie JSON i prostotę obsługi tak zakodowanych, danych - składających się z typów podstawowych - możemy prześledzić na prostym kodu znajdującym się poniżej:
import json
from pprint import pprint
if __name__ == '__main__':
original = {'imię': 'Jan',
'nazwisko': 'Nowak',
'adres': { 'miasto': 'Łódź',
'ulica': 'Piotrkowska',
'numer_domu': 17,
'numer_lokalu': None} }
dump = json.dumps(original, sort_keys=True, indent=4)
loaded = json.loads(dump)
print("Print dumped original of type (%s)" % (type(dump), ) )
print(dump)
print("Print loaded dump: (%s)" % (type(loaded), ) )
pprint(loaded)
Zakodowane w formacie utf-8 dane, umieszczone w słowniku original,
migrujemy do formatu JSON po czym ponownie przywracamy. Zwróć uwagę na kilka
istotnych rzeczy widocznych na po uruchomieniu skryptu:
print wyświetla zakodowane dane,Dane w kroku Print dumped original of type (<class "str">):
{
"adres": {
"miasto": "\u0141\u00f3d\u017a",
"numer_domu": 17,
"numer_lokalu": null,
"ulica": "Piotrkowska"
},
"imi\u0119": "Jan",
"nazwisko": "Nowak"
}
Dane w kroku Print loaded dump: (<class "dict">):
{'adres': {'miasto': 'Łódź',
'numer_domu': 17,
'numer_lokalu': None,
'ulica': 'Piotrkowska'},
'imię': 'Jan',
'nazwisko': 'Nowak'}
Poruszanie się w obrębie typów prostych nie wymaga dodatkowych kroków. Sytuacja nieznacznie komplikuje się, gdy zamierzamy pracować z obiektami.
Domyślny koder i dekoder zawarty w pakiecie json przystosowany jest do
radzenia sobie z podstawowymi typami danych. Umożliwia jednakże swoją rozbudowę
dzięki czemu z jego pomocą można zapisać w formacie JSON dowolny obiekt, czy
typ danych.
Dla przykładu posłużymy się dwiema klasami. Person reprezentuje osobę
dokonującą transakcję, natomiast Transaction opisuję dokonywaną między
osobami transakcję. Jak już wiesz kodowanie i dekodowanie typów prostych jak
str, czy float jest domyślnie zaimplementowane w pakiecie json. Problem
jaki wystąpi przy kodowaniu poniższego kodu związany jest z obsługą obiektów
Person, oraz datetime.datetime.
from dataclasses import dataclass
import datetime
import json
from typing import Any
@dataclass(frozen=True)
class Person():
name: str
surename: str
email: str
@dataclass(frozen=True)
class Transaction():
""" Object holding paylaod data. """
sender: Person
recipient: Person
data: datetime.datetime
amount: float
Wykonując poniższy kod otrzymamy wyjątek TypeError: Object of type datetime is
not JSON serializable kodera pakietu json związany z nieobsługiwanym typem
danych.
p1 = Person(name="John", surename="Wick", email="john.wick@email.com")
p2 = Person(name="Bowery", surename="King", email="bowery.king@email.com")
t1 = AdvancedTransaction(sender=p1,
recipient=p2,
data=datetime.datetime.now(),
amount=1.61)
t1_json = json.dumps(t1.__dict__, sort_keys=True) # Exception: Type Error
Poradzenie sobie z tym błędem wymaga od nas implementacji kodera danych
mówiącego w jaki sposób należy przechować nieobsługiwane dotąd klasy. Nowa
implementacja kodera musi dziedziczyć z klasy json.JSONEncoder i definiować
metodę default zwracającą tekst z reprezentacją dodawanych przez nas
obiektów. Przykładowa implementacja:
class JsonEncoder(json.JSONEncoder):
""" Class serializing project specific data into JSON format. """
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S"
def default(self, obj:Any) -> str:
if isinstance(obj, Person):
_j = {}
for k, v in obj.__dict__.items():
_j[k] = v
return {'_type': 'Person',
'value': _j}
elif isinstance(obj, datetime.datetime):
return { "_type": "datetime",
"_format": "%s %s" % (self.DATE_FORMAT, self.TIME_FORMAT),
"value": obj.strftime( "%s %s"% ( self.DATE_FORMAT,
self.TIME_FORMAT)) }
else:
raise ValueError("Not supported object type")
Korzystając z klasy JsonEncoder będziemy w stanie bez trudu przenieść do
formatu JSON sprawiającą nam problem instancję Transaction. By tego dokonać
metodzie json.dumps podajemy atrybut cls:
t1_json = json.dumps(t1.__dict__, cls=JsonEncoder, sort_keys=True, indent=2)
# t1_json =
# {
# "amount": 1.61,
# "data": {
# "_format": "%Y-%m-%d %H:%M:%S",
# "_type": "datetime",
# "value": "2020-11-02 15:17:59"
# },
# "recipient": {
# "_type": "Person",
# "value": {
# "email": "bowery.king@email.com",
# "name": "Bowery",
# "surename": "King"
# }
# },
# "sender": {
# "_type": "Person",
# "value": {
# "email": "john.wick@email.com",
# "name": "John",
# "surename": "Wick"
# }
# }
# }
Teoretycznie możemy na tym poprzestać, jednak uważam, że przydatne będzie
dodanie do naszego kodu możliwości odtworzenia zakodowanych w formacje JSON
obiektów. Jak się domyślasz nie stanie się to auto-magicznie i potrzebne jest
dopisanie do kodu dekodera dziedziczącego po json.JSONDecoder i
implementującego metodę object_hook.
class JsonDecoder(json.JSONDecoder):
""" JSON decoder prepared to handle project specific data. """
def __init__(self, *args, **kwargs):
super(JsonDecoder, self).__init__( object_hook=self.object_hook,
*args)
def object_hook(self, obj):
if "_type" not in obj:
return obj
elif obj["_type"] == "Person":
return Person( name =obj["value"]["name"],
surename=obj["value"]["surename"],
email =obj["value"]["email"])
elif obj["_type"] == "datetime":
return datetime.datetime.strptime( obj['value'], obj['_format'] )
else:
msg = "Unsupported object type '%s'" % obj["_type"]
raise json.JSONDecoderError(msg)
Korzystnie z dekodera sprowadza się do wykonania kodu:
decoder = JsonDecoder()
t1_loaded = decoder.decode(t1_json))
Teraz obiekt t1_loaded będzie posiadał tę samą zawartość co oryginalny obiekt
t1.
Liczę na to, że ta krótka prezentacja uzmysłowiła Ci jak wygodnym formatem jest JSON i dlaczego warto go stosować. Przede wszystkim jest on czytelny zarówno dla maszyn jak i dla ludzi, co czyni go wyjątkowo praktycznym rozwiązaniem.
Jeżeli zamierzasz do tego, żeby żyło Ci się wygodnie, prawdopodobnie nigdy nie będziesz bogaty. Lecz jeśli zmierzasz do tego, by być bogatym, prawdopodobnie będzie Ci w końcu niesamowicie wygodnie. 'Bogaty albo biedny' T. Harv Eker