Rób to, co możesz, tym, co posiadasz, i tam, gdzie jesteś.
Theodore Roosevelt
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.