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.

Kodowanie typów prostych

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:

  • sposób konwersji, kodowania, polskich znaków diaktrycznych,
  • estetykę z jaką zwykły print wyświetla zakodowane dane,
  • typy danych źródłowych, zakodowanych i ponownie odkodowanych.

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.

Kodowanie typów złożonych, własnych

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.

Podsumowanie

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.

Artykuł dodano 2020-02-08