json - Serializzatore di JavaScript Object Notation

Scopo Codifica oggetti Python come stringhe JSON, e decodifica stringhe JSON in oggetti Python
Versione Python 2.6

Il modulo json fornisce una API simile a pickle per la conversione in memoria di oggetti Python verso una rappresentazione serializzata nota come "JavaScript Object Notation". Al contrario di pickle, JSON ha il beneficio di avere implementazioni in molti linguaggi (specialmente JavaScript), rendendolo adatto alla comunicazione tra applicazioni. L'uso più ampio di JSON è probabilmente nella comunicazione tra web server e client in una applicazione AJAX, ma non è limitato a quel campo.

Codificare e Decodificare Tipi di Dato Semplici

Il codificatore comprende i tipi nativi di Python in modo predefinito (int, float, list, tuple, dict).

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 un modo molto simile all'output della funzione Python repr()

$ python json_simple_types.py
DATI: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
JSON: [{"a": "A", "c": 3.0, "b": [2, 4]}]

La codifica, quindi la ridecodifica, potrebbe non fornire esattamente lo stesso tipo di oggetto.

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
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'])

Ad esempio, le tuple sono convertite in liste JSON

$ python json_simple_types_decode.py
CODIFICATI  : [{"a": "A", "c": 3.0, "b": [2, 4]}]
DECODIFICATI: [{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]
ORIGINALI   : <type 'tuple'>
DECODIFICATI: <type 'list'>

Usabili dall'utente contro Output Compatto

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

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 'ORDINATI:', json.dumps(data, sort_keys=True)

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

print 'CORRISPONDENZA NON ORDINATI:', unsorted == first
print 'CORRISPONDENZA ORDINATI    :', first == second

L'ordinamento facilita la lettura dei risultati, e rende anche possibile il confronto dell'output di JSON nei test.

$ python json_sort_keys.py
DATI: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
JSON    : [{"a": "A", "c": 3.0, "b": [2, 4]}]
ORDINATI: [{"a": "A", "b": [2, 4], "c": 3.0}]
CORRISPONDENZA NON ORDINATI: False
CORRISPONDENZA ORDINATI    : True

Per strutture di dati altamente nidificate, si vorrà specificare un valore per indent, in modo che l'output venga gradevolmente formattato.

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 indent non è un intero negativo, l'output assomiglia molto più a quello di pprint, con spazi iniziali per ogni livello della struttura dati che corrisponde al livello di indentazione.

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

Un output esteso come questo accresce il numero di byte necessari per trasmettere lo stesso quantitativo di dati, comunque questo non è il tipo di cosa che si voglia necessariamente usare in un ambiente di produzione. Infatti, si vorrànno adattare le impostazioni per la separazione dei dati nell'output codificato per renderlo ancora più compatto rispetto alle impostazioni predefinite.

import json

data = [ { 'a':'A', 'b':(2, 4), 'c':3.0 } ]
print 'DATI:', repr(data)
print 'repr(data)                  :', len(repr(data))
print 'dumps(data)                 :', len(json.dumps(data))
print 'dumps(data, indentazione=2) :', len(json.dumps(data, indent=2))
print 'dumps(data, separatori)     :', len(json.dumps(data, separators=(',',':')))

Il parametro separators per dumps() dovrebbe essere una tuple contenente le stringhe per separare gli elementi in una lista e le chiavi dai valori in un dizionario. Predefinito: (', ', ': '). Eliminando lo spazio, possiamo produrre un output più compatto.

$ python json_compact_encoding.py
DATI: [{'a': 'A', 'c': 3.0, 'b': (2, 4)}]
repr(data)                  : 35
dumps(data)                 : 35
dumps(data, indentazione=2) : 76
dumps(data, separatori)     : 29

Codificare Dizionari

Il formato JSON si aspetta che le chiavi di un dizionario siano stringhe. Se si hanno altri tipi di chiavi nel proprio dizionario, il tentativo di codificare l'oggetto produrrà un TypeError. Un modo di aggirare questo limite è di ignorare le chiavi non stringa usando il parametro skipkeys:

import json

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

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

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

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

$ python json_skipkeys.py
Primo tentativo
ERRORE: key ('d',) is not a string

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

Lavorare con i Propri Tipi

Fino a qui tutti gli esempi hanno usati tipi Python built-in perchè essi sono supportati nativamente da json. Non è raro, naturalmente, che si vogliano codificare anche i propri tipi. Ci sono due modi per fare questo.

Per prima cosa occorre una classe da codificare:

class MyObj(object):
    def __init__(self, s):
        self.s = s
    def __repr__(self):
        return '<MyObj(%s)>' % self.s

Un modo semplice di codificare una istanza di MyObj è di definire una funzione per convertire un tipo sconosciuto in uno conosciuto. Non occorre occuparsi di effettuare la codifica, ma solo di convertire un oggetto in un altro.

import json
import json_myobj

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

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

def convert_to_builtin_type(obj):
    print 'default(', repr(obj), ')'
    # Converte gli oggetti in una 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)

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

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

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

Per decodificare i risultati e creare una istanza di MyObj, occorre agganciarsi al decoder così che si possa importare la classe dal modulo e creare l'istanza. Per farlo si usa il parametro object_hook di loads().

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

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
        class_ = getattr(module, class_name)
        print 'CLASSE:', class_
        args = dict( (key.encode('ascii'), value) for key, value in d.items())
        print 'PARAMETRI ISTANZA:', args
        inst = class_(**args)
    else:
        inst = d
    return inst

encoded_object = '[{"s": "Il valore di 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 i valori stringa in oggetti unicode, occorre ricodificarli come stringhe ASCII prima di usarle come parametri di parola chiave per il costruttore della classe

MODULO: 
CLASSE: 
PARAMETRI ISTANZA: {'s': u'Il valore di istanza va qui'}
[]

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

Classi di Codifica e Decodifica

A parte le funzioni di utilità che sono già state esaminate, il modulo json fornisce classi per la codifica e la decodifica. Usando le classi direttamente, si accede a delle API extra e si possono creare sottoclassi per personalizzarne il comportamento.

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

import json

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

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

Come si può notare, l'output viene generato in unità logiche, invece che esser basato su un qualsiasi valore di dimensione.

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

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

Per codificare oggetti arbitrari, si può sovrascrivere il metodo default() con una implementazione simile a quella usato più sopra in convert_builtin_type().

import json
import json_myobj

class MyEncoder(json.JSONEncoder):

    def default(self, obj):
        print 'default(', repr(obj), ')'
        # Converte gli oggetti in un dizionario della loro rappresentazione
        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 della implementazione precedente.


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

La decodifica del testo, poi la conversione del dizionario in un oggetto richiede un lavoro maggiore da impostare rispetto alla implementazione precedente, ma non troppo.

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
            class_ = getattr(module, class_name)
            print 'CLASSE:', class_
            args = dict( (key.encode('ascii'), value) for key, value in d.items())
            print 'PARAMETRI ISTANZA:', args
            inst = class_(**args)
        else:
            inst = d
        return inst

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

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

E l'ouput è lo stesso dell'esempio precedente.

MODULO: 
CLASSE: 
PARAMETRI ISTANZA: {'s': u'Il valore di istanza va qui'}
[]

Lavorare con Flussi e File

In tutti gli esempi fino a qui si è ritenuto che si sarebbe potuto (e dovuto) mantenere la versione codificata della intera struttura di dati in memoria tutta insieme. Con grandi strutture dati potrebbe essere preferibile scrivere la codifica direttamente verso un oggetto tipo file. Le funzioni di utilità load() e dump() accettano riferimenti ad oggetti tipo file da usare per leggere o scrivere.

import json
import tempfile

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

f = tempfile.NamedTemporaryFile(mode='w+')
json.dump(data, f)
f.flush()

print open(f.name, 'r').read()

Un socket funzionerebbe pressochè allo stesso modo del normale file handle usato qui.

$ python json_dump_file.py
[{"a": "A", "c": 3.0, "b": [2, 4]}]

Sebbene non ottimizzata per leggere solo parti di dati alla volta, la funzione load() offre comunque il vantaggio di incapsulare la logica della generazione di oggetti dal flusso di input.

import json
import tempfile

f = tempfile.NamedTemporaryFile(mode='w+')
f.write('[{"a": "A", "c": 3.0, "b": [2, 4]}]')
f.flush()
f.seek(0)
$ python json_load_file.py
[{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]

Flussi di Dati Misti

JSONDecoder include il metodo raw_decode() per decodificare una struttura di dati seguita da altri dati, tipo dati JSON con testo in coda. Il valore di ritorno è l'oggetto creato decodificando i dati in input, ed un indice all'interno di quei dati che indica quando la decodifica è stata interrotta.

    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 'Prima JSON:'
obj, end, remaining = get_decoded_and_remainder(' '.join([encoded_object, extra_text]))
print 'Oggetto             :', obj
print 'Fine input elaborato:', end
print 'Testo rimanente     :', repr(remaining)

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

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

Prima JSON:
Oggetto             : [{u'a': u'A', u'c': 3.0, u'b': [2, 4]}]
Fine input elaborato: 35
Testo rimanente     : ' Testo NON JSON.'

JSON contenuto:
ERRORE: No JSON object could be decoded

Vedere anche:

json
La documentazione della libreria standard per questo modulo.
http://json.org
JavaScript Object Notation (JSON).
http://code.google.com/p/simplejson/
simplejson, di Bob Ippolito, è la versione di sviluppo esterna mantenuta della libreria json inclusa con Python 2.6 e Python 3.0. Mantiene compatibilità all'indietro con Python 2.4 e Python 2.5
jsonpickle
jsonpickle consente a qualsiasi oggetto Python di essere serializzato in JSON
Persistenza e Scambio Dati