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