json - JavaScript Object Notation

Scopo: Codifica oggetti Python come stringhe JSON, e decodifica stringhe JSON in oggetti Python.

Il modulo json fornisce una API simile a pickle per convertire oggetti Python in memoria in una rappresentazione serializzata nota come JavaScript Object Notation (JSON). A differenza di pickle, JSON ha il vantaggio di avere implementazioni in molti linguaggi (specialmente JavaScript). Esso è usato largamente per comunicare tra web server e client nelle API REST, ma è anche utile per altre necessità di comunicazione tra applicazioni.

Codificare e Decodificare Tipi di Dato Semplici

Il codificatore nella modalità predefinita è in grado di riconoscere gli oggetti nativi Python (str, int, float, list, tuple e dict).

# json_simple_types.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATI:', repr(data))

data_string = json.dumps(data)
print('JSON:', data_string)

I valori sono codificati in modo che superficialmente ricorda l'output di repr().

$ python3 json_simple_types.py

DATI: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]

La codifica e la successiva decodifica potrebbe non restituire lo stesso tipo di oggetto.

# json_simple_types_decode.py
import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATI        :', data)

data_string = json.dumps(data)
print('CODIFICATI  :', data_string)

decoded = json.loads(data_string)
print('DECODIFICATI:', decoded)

print('ORIGINALI   :', type(data[0]['b']))
print('DECODIFICATI:', type(decoded[0]['b']))

In particolare, le tuple diventano liste.

$ python3 json_simple_types_decode.py

DATI        : [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
CODIFICATI  : [{"a": "A", "c": 3.0, "b": [2, 4]}]
DECODIFICATI: [{'a': 'A', 'c': 3.0, 'b': [2, 4]}]
ORIGINALI   : <class 'tuple'>
DECODIFICATI: <class 'list'>

Output Comprensibile all'Umano contro Output Compatto

Un altro vantaggio di JSON su pickle è che i risultati sono leggibili dall'umano. La funzione dumps() accetta diversi argomenti per rendere l'output ancora più gradevole. Ad esempio il flag sort_keys dice al codificatore di stampare le chiavi di un dizionario ordinate, invece che in ordine casuale.

# json_sort_keys.py
import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATI:', repr(data))

unsorted = json.dumps(data)
print('JSON    :', json.dumps(data))
print('ORDINATO:', json.dumps(data, sort_keys=True))

first = json.dumps(data, sort_keys=True)
second = json.dumps(data, sort_keys=True)

print('CORRISPONDENZA NON ORDINATA:', unsorted == first)
print('CORRISPONDENZA ORDINATA    :', first == second)

L'ordinamento facilita la scansione a occhio dei risultati, e rende anche possibile confrontare l'output di JSON nei test.

$ python3 json_sort_keys.py

DATI: [{'b': (2, 4), 'a': 'A', 'c': 3.0}]
JSON    : [{"b": [2, 4], "a": "A", "c": 3.0}]
ORDINATO: [{"a": "A", "b": [2, 4], "c": 3.0}]
CORRISPONDENZA NON ORDINATA: False
CORRISPONDENZA ORDINATA    : True

Per strutture dati profondamente nidificate, si specifica un valore per l'indentazione: indent in modo che l'output venga formattato piacevolmente.

# json_indent.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATI:', repr(data))

print('NORMALE  :', json.dumps(data, sort_keys=True))
print('INDENTATO:', json.dumps(data, sort_keys=True, indent=2))

Quando il valore di indentazione non è negativo, l'output assomiglia di più a quello di pprint, con gli spazi iniziali per ogni livello della struttura dati corrispondenti al livello di indentazione.

$ python3 json_indent.py

DATI: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]
NORMALE  : [{"a": "A", "b": [2, 4], "c": 3.0}]
INDENTATO: [
  {
    "a": "A",
    "b": [
      2,
      4
    ],
    "c": 3.0
  }
]

Un output particolareggiato come questo tuttavia aumenta il numero di byte necessari per trasmettere la stessa mole di dati, pertanto non è indicato per un uso in ambiente di produzione. In effetti è possibile aggiustare le impostazioni per separare i dati nell'output codificato per renderli ancora più compatti che nella modalità predefinita.

# json_compact_encoding.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
print('DATI:', repr(data))

print('repr(data)             :', len(repr(data)))

plain_dump = json.dumps(data)
print('dumps(data)            :', len(plain_dump))

small_indent = json.dumps(data, indent=2)
print('dumps(data, indent=2)  :', len(small_indent))

with_separators = json.dumps(data, separators=(',', ':'))
print('dumps(data, separators):', len(with_separators))

L'argomento separators per la funzione dumps() dovrebbe essere una tupla che contiene le stringhe per separare gli elementi in una lista e le chiavi dai valori in un dizionario. La modalità predefinita è (', ', ': '). Rimuovendo gli spazi si può produrre un output più compatto.

$ python3 json_compact_encoding.py

DATI: [{'c': 3.0, 'b': (2, 4), 'a': 'A'}]
repr(data)             : 35
dumps(data)            : 35
dumps(data, indent=2)  : 73
dumps(data, separators): 29

Codificare i Dizionari

Il formato JSON si attende che le chiavi di un dizionario siano stringhe. Cercare di codificare un dizionario con chiavi non stringa genera un TypeError. Un modo per circumnavigare questa limitazione è di dire al codificatore di ignorare le chiavi non stringa usando l'argomento skipkeys.

# json_skipkeys.py

import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0, ('d',): 'D tuple'}]

print('Primo tentativo')
try:
    print(json.dumps(data))
except TypeError as err:
    print('ERRORE:', err)

print()
print('Secondo tentativo')
print(json.dumps(data, skipkeys=True))

Piuttosto che sollevare una eccezione, la chiave non stringa, viene ignorata.

$ python3 json_skipkeys.py
Primo tentativo
ERRORE: keys must be a string

Secondo tentativo
[{"c": 3.0, "a": "A", "b": [2, 4]}]

Lavorare con Tipi Personalizzati

Tutti gli esempi fino a qui hanno usato tipi Python built-in visto che questi sono supportati nativamente da json. E' comune la necessità di codificare anche classi personalizzate, e per farlo ci sono due modi.

Si prenda questa classe da codificare.

# json_myobj.py

class MyObj:

    def __init__(self, s):
        self.s = s

    def __repr__(self):
      return '<MyObj({})>'.format(self.s)

Un semplice modo per codificare una istanza di MyObj è di definire una funzione che converta un tipo sconosciuto in uno conosciuto. Non occorre eseguire la codifica, quindi si dovrebbe solo convertire un oggetto in un altro.

# json_dump_default.py

import json
import json_myobj

obj = json_myobj.MyObj('Il valore dell\'istanza va qui')

print('Primo tentativo')
try:
    print(json.dumps(obj))
except TypeError as err:
    print('ERRORE:', err)


def convert_to_builtin_type(obj):
    print('default(', repr(obj), ')')
    # Converte oggetti in un dizionario della loro rappresentazione
    d = {
        '__class__': obj.__class__.__name__,
        '__module__': obj.__module__,
    }
    d.update(obj.__dict__)
    return d

print()
print('Con default')
print(json.dumps(obj, default=convert_to_builtin_type))

Con la funzione convert_to_builtin_type(), le istanze delle classi non riconosciute da json sono convertite in dizionari con informazioni sufficienti per ricreare l'oggetto, se un programma ha accesso ai moduli Python necessari.

$ python3 json_dump_default.py

Primo tentativo
ERRORE: <MyObj(Il valore dell'istanza va qui)> is not JSON serializable

Con default
default( <MyObj(Il valore dell'istanza va qui)> )
{"__module__": "json_myobj", "__class__": "MyObj", "s": "Il valore dell'istanza va qui"}

Per decodificare i risultati e creare una istanza di MyObj(), si usa l'argomento object_hook di loads() per consentire al decodificatore di importare la classe dal modulo e quindi creare l'istanza.

object_hook viene chiamato per ogni dizionario decodificato dal canale di dati in arrivo, fornendo la possibilità di convertire il dizionario in un altro tipo di oggetto. La funzione di aggancio dovrebbe ritornare l'oggetto che l'applicazione chiamante dovrebbe ricevere in luogo del dizionario.

# json_load_object_hook.py

import json


def dict_to_object(d):
    if '__class__' in d:
        class_name = d.pop('__class__')
        module_name = d.pop('__module__')
        module = __import__(module_name)
        print('MODULO:', module.__name__)
        class_ = getattr(module, class_name)
        print('CLASSE:', class_)
        args = {
            key: value
            for key, value in d.items()
        }
        print('ARGOMENTI DELL\'ISTANZA:', args)
        inst = class_(**args)
    else:
        inst = d
    return inst


encoded_object = '''
    [{"s": "Il valore dell'istanza va qui",
      "__module__": "json_myobj", "__class__": "MyObj"}]
    '''

myobj_instance = json.loads(
    encoded_object,
    object_hook=dict_to_object,
)
print(myobj_instance)

Visto che json converte valori stringa in oggetti unicode, essi dovrebbero essere ricodificati in stringhe ASCII prima che possano essere usate come argomenti chiave dal costruttore della classe.

$ python3 json_load_object_hook.py

MODULO: json_myobj
CLASSE: <class 'json_myobj.MyObj'>
ARGOMENTI DELL'ISTANZA: {'s': "Il valore dell'istanza va qui"}
[<MyObj(Il valore dell'istanza va qui)>]

Agganci simili sono disponibili per i tipi di intero built-in (parse_int), numeri a virgola mobile (parse_float) e costanti (parse_constant).

Classi per Codificare e Decodificare

A parte le funzioni di comodo già trattate, il modulo json fornisce classi per codificare e decodificare. L'uso delle classi da accesso diretto a delle API supplementari per personalizzarne il comportamento.

JSONEncoder usa una interfaccia iterabile per produrre "blocchi" di dati codificati, facilitandone la scrittura su file o su socket di rete senza dover rappresentare l'intera struttura dati in memoria.

# json_encoder_iterable.py

import json

encoder = json.JSONEncoder()
data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]

for part in encoder.iterencode(data):
    print('PARTE:', part)

L'output viene generato in unità logiche, piuttosto che basarsi su un qualsiasi valore di dimensione.

$ python3 json_encoder_iterable.py

PARTE: [
PARTE: {
PARTE: "b"
PARTE: :
PARTE: [2
PARTE: , 4
PARTE: ]
PARTE: ,
PARTE: "a"
PARTE: :
PARTE: "A"
PARTE: ,
PARTE: "c"
PARTE: :
PARTE: 3.0
PARTE: }
PARTE: ]

Il metodo encode() è praticamente equivalente a ''.join(encoder.iterencode()), con qualche verifica di errore supplementare all'inizio.

Per codificare oggetti arbitrari, si sovrascrive il metodo default() con una implementazione simile a quella usata in convert_to_builtin_type().

# json_encoder_default.py

import json
import json_myobj


class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        print('default(', repr(obj), ')')
        # Convert objects to a dictionary of their representation
        d = {
            '__class__': obj.__class__.__name__,
            '__module__': obj.__module__,
        }
        d.update(obj.__dict__)
        return d


obj = json_myobj.MyObj('dati interni')
print(obj)
print(MyEncoder().encode(obj))

L'output è lo stesso dell'implementazione precedente.

$ python3 json_encoder_default.py

<MyObj(dati interni)>
default( <MyObj(dati interni)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "dati interni"}

Decodificare il testo, quindi convertire il dizionario in un oggetto necessita di più lavoro di impostazione rispetto alla implementazione precedente, ma non più di tanto.

# json_decoder_object_hook.py

import json


class MyDecoder(json.JSONDecoder):

    def __init__(self):
        json.JSONDecoder.__init__(
            self,
            object_hook=self.dict_to_object,
        )

    def dict_to_object(self, d):
        if '__class__' in d:
            class_name = d.pop('__class__')
            module_name = d.pop('__module__')
            module = __import__(module_name)
            print('MODULO:', module.__name__)
            class_ = getattr(module, class_name)
            print('CLASSE:', class_)
            args = {
                key: value
                for key, value in d.items()
            }
            print('ARGOMENTI DELL\'ISTANZA:', args)
            inst = class_(**args)
        else:
            inst = d
        return inst


encoded_object = '''
[{"s": "I valori dell'instanza vanno qui",
  "__module__": "json_myobj", "__class__": "MyObj"}]
'''

myobj_instance = MyDecoder().decode(encoded_object)
print(myobj_instance)

L'output è lo stesso dell'esempio precedente.

$ python3 json_decoder_object_hook.py
MODULO: json_myobj
CLASSE: <class 'json_myobj.MyObj'>
ARGOMENTI DELL'ISTANZA: {'s': "I valori dell'instanza vanno qui"}
[<MyObj(I valori dell'instanza vanno qui)>]

Lavorare con Flussi e File

Tutti gli esempi fino a qui assumevano che la versione codificata della intera struttura dati dovrebbe stare tutta in memoria. Con strutture dati molto grandi, potrebbe essere preferibile scrivere la codifica direttamente a un oggetto di tipo file. Le funzioni di comodo load() e dump() accettano riferimenti a oggetti di tipo file da usare per lettura e scrittura.

# json_dump_file.py

import io
import json

data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]

f = io.StringIO()
json.dump(data, f)

print(f.getvalue())

Un socket o un normale file handle avrebbero funzionato allo stesso modo del buffer StringIO usato nell'esempio.

$ python3 json_dump_file.py

[{"b": [2, 4], "c": 3.0, "a": "A"}]

Sebbene non ottimizzato per leggere solo parti di dati alla volta, la funzione load() offre comunque il beneficio dell'incapsulare la logica della generazione degli oggetti da un flusso in input.

# json_load_file.py

import io
import json

f = io.StringIO('[{"a": "A", "c": 3.0, "b": [2, 4]}]')
print(json.load(f))

Proprio come per dump(), qualsiasi oggetto di tipo file può essere passato a load().

$ python3 json_load_file.py

[{'b': [2, 4], 'c': 3.0, 'a': 'A'}]

Flussi Dati Mescolati

JSONDecoder include raw_decode(), un metodo per decodificare una struttura dati seguita da ulteriori dati, tipo dati JSON con del testo in coda. Il valore ritornato è l'oggetto creato dalla decodifica dei dati in input, a un indice nei dati che indica dove la decodifica si è interrotta.

# json_mixed_data.py

import json

decoder = json.JSONDecoder()


def get_decoded_and_remainder(input_data):
    obj, end = decoder.raw_decode(input_data)
    remaining = input_data[end:]
    return (obj, end, remaining)


encoded_object = '[{"a": "A", "c": 3.0, "b": [2, 4]}]'
extra_text = 'Questo testo non è JSON.'

print('JSON davanti:')
data = ' '.join([encoded_object, extra_text])
obj, end, remaining = get_decoded_and_remainder(data)

print('Oggetto                   :', obj)
print('Fine dell\'input elaborato:', end)
print('Testo rimanente           :', repr(remaining))

print()
print('JSON incorporato:')
try:
    data = ' '.join([extra_text, encoded_object, extra_text])
    obj, end, remaining = get_decoded_and_remainder(data)
except ValueError as err:
    print('ERRORE:', err)

Sfortunatamente, questo funziona solo se l'oggetto compare all'inizio dell'input.

$ python3 json_mixed_data.py

JSON davanti:
Oggetto                   : [{'b': [2, 4], 'a': 'A', 'c': 3.0}]
Fine dell'input elaborato: 35
Testo rimanente           : ' Questo testo non è JSON.'

JSON incorporato:
ERRORE: Expecting value: line 1 column 1 (char 0)

JSON da Riga di Comando

Il modulo json.tool implementa un programma di riga di comando per riformattare i dati JSON per facilitarne la lettura.

[{"a": "A", "c": 3.0, "b": [2, 4]}]

Il file in input example.json contiene una mappatura con le chiavi non ordinate alfabeticamente. Il primo esempio che segue mostra i dati riformattati ordinati, e il secondo usa --sort-keys per ordinare le chiavi di mappatura prima della stampa.

$ python3 -m json.tool example.json
[
    {
        "a": "A",
        "c": 3.0,
        "b": [
            2,
            4
        ]
    }
]
[ robby: ~/pycode/pymotw-it3.0/dumpscripts ]$ python3 -m json.tool --sort-keys example.json
[
    {
        "a": "A",
        "b": [
            2,
            4
        ],
        "c": 3.0
    }
]

Vedere anche:

json
La documentazione della libreria standard per questo modulo
Note di Portabilità per json
JavaScript Object Notation
La home di JSON, con documentazione e implementazioni in altri linguaggi.
jsonpickle
consente a ogni oggetto Python di essere serializzato in JSON