json — Notación de objetos JavaScript

Propósito:Codifica objetos de Python como cadenas JSON y decodifica cadenas JSON en objetos de Python.

El módulo json proporciona una interfaz de programación similar a pickle para convertir objetos Python en memoria a una representación serializada conocida como notación de objetos JavaScript (JSON). A diferencia de pickle, JSON tiene el beneficio de tener implementaciones en muchos idiomas (especialmente JavaScript). Es el más utilizado para la comunicación entre el servidor web y cliente en una interfaz de programación REST, pero también es útil para otras necesidades de comunicación entre aplicaciones.

Codificar y decodificar tipos de datos simples

El codificador entiende los tipos nativos de Python por defecto (str, int, float, list, tuple, y dict).

json_simple_types.py
import json

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

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

Los valores se codifican de una manera superficialmente similar a la salida de repr() de Python.

$ python3 json_simple_types.py

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

La codificación, luego la decodificación puede no dar exactamente el mismo tipo de objeto.

json_simple_types_decode.py
import json

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

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

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

print('ORIGINAL:', type(data[0]['b']))
print('DECODED :', type(decoded[0]['b']))

En particular, las tuplas se convierten en listas.

$ python3 json_simple_types_decode.py

DATA   : [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
ENCODED: [{"a": "A", "b": [2, 4], "c": 3.0}]
DECODED: [{'a': 'A', 'b': [2, 4], 'c': 3.0}]
ORIGINAL: <class 'tuple'>
DECODED : <class 'list'>

Salida de consumo humano vs salida compacta

Otro beneficio de JSON sobre pickle es que los resultados son legible por humanos. La función dumps() acepta varios argumentos para hacer la salida aún mejor. Por ejemplo, la bandera sort_keys le dice al codificador que envíe las claves de un diccionario ordenadas, en lugar de ordenadas al azar.

json_sort_keys.py
import json

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

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

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

print('UNSORTED MATCH:', unsorted == first)
print('SORTED MATCH  :', first == second)

La clasificación hace que sea más fácil escanear los resultados a ojo, y también hace posible comparar la salida JSON en las pruebas.

$ python3 json_sort_keys.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
JSON: [{"a": "A", "b": [2, 4], "c": 3.0}]
SORT: [{"a": "A", "b": [2, 4], "c": 3.0}]
UNSORTED MATCH: True
SORTED MATCH  : True

Para estructuras de datos altamente anidadas, especifica un valor para indent para que la salida está mejor formateada.

json_indent.py
import json

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

print('NORMAL:', json.dumps(data, sort_keys=True))
print('INDENT:', json.dumps(data, sort_keys=True, indent=2))

Cuando la sangría es un entero no negativo, la salida es más parecida a la de pprint, con espacios iniciales para cada nivel de la estructura de datos que coinciden con el nivel de sangría.

$ python3 json_indent.py

DATA: [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
NORMAL: [{"a": "A", "b": [2, 4], "c": 3.0}]
INDENT: [
  {
    "a": "A",
    "b": [
      2,
      4
    ],
    "c": 3.0
  }
]

La salida detallada como ésta aumenta el número de bytes necesarios para transmitir la misma cantidad de datos, sin embargo, no está destinada a ser utilizada en un entorno de producción. De hecho, es posible ajustar la configuración para separar los datos en la salida codificada para que sea aún más compacta que la configuración predeterminada.

json_compact_encoding.py
import json

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

El argumento separators de dumps() debe ser una tupla que contiene las cadenas para separar elementos en una lista y claves de valores en un diccionario. El valor predeterminado es (',', ':'). Al eliminar el espacio en blanco, se produce una salida más compacta.

$ python3 json_compact_encoding.py

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

Codificar diccionarios

El formato JSON espera que las llaves de un diccionario sean cadenas. Al tratar de codificar un diccionario con tipos que no son de cadena, las llaves producen un TypeError. Una forma de evitar esa limitación es decir al codificador que omita las claves que no sean de cadena usando el argumento skipkeys:

json_skipkeys.py
import json

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

print('First attempt')
try:
    print(json.dumps(data))
except TypeError as err:
    print('ERROR:', err)

print()
print('Second attempt')
print(json.dumps(data, skipkeys=True))

En lugar de elevar una excepción, la clave que no es de cadena se ignora.

$ python3 json_skipkeys.py

First attempt
ERROR: keys must be a string

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

Trabajar con tipos personalizados

Todos los ejemplos hasta ahora han usado tipos incorporados de Python porque son soportados por json de forma nativa. Es común necesitar codificar también clases personalizadas, y hay dos formas de hacerlo.

Dada esta clase para codificar:

json_myobj.py

class MyObj:

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

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

La forma simple de codificar una instancia MyObj es definir una función para convertir un tipo desconocido a un tipo conocido. No necesita hacer la codificación, por lo que simplemente debe convertir un objeto a otro.

json_dump_default.py
import json
import json_myobj

obj = json_myobj.MyObj('instance value goes here')

print('First attempt')
try:
    print(json.dumps(obj))
except TypeError as err:
    print('ERROR:', err)


def convert_to_builtin_type(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


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

En convert_to_builtin_type(), instancias de clases no reconocidas por json se convierten en diccionarios con suficiente información para volver a crear el objeto si un programa tiene acceso a los módulos de Python necesarios.

$ python3 json_dump_default.py

First attempt
ERROR: Object of type 'MyObj' is not JSON serializable

With default
default( <MyObj(instance value goes here)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "instance
value goes here"}

Para decodificar los resultados y crear una instancia MyObj(), usa el argumento object_hook de loads() para vincular al decodificador para que la clase pueda importarse desde el módulo y usarse para crear la instancia.

El object_hook se llama para cada diccionario decodificado del flujo de datos entrante, brindando la oportunidad de convertir el diccionario a otro tipo de objeto. La función de gancho debe devolver el objeto que la aplicación que llama debe recibir en lugar del diccionario.

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('MODULE:', module.__name__)
        class_ = getattr(module, class_name)
        print('CLASS:', class_)
        args = {
            key: value
            for key, value in d.items()
        }
        print('INSTANCE ARGS:', args)
        inst = class_(**args)
    else:
        inst = d
    return inst


encoded_object = '''
    [{"s": "instance value goes here",
      "__module__": "json_myobj", "__class__": "MyObj"}]
    '''

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

Dado que json convierte los valores de cadena a objetos Unicode, estos necesitan volver a codificarse como cadenas ASCII antes de que puedan usarse como argumentos palabra clave del constructor de la clase.

$ python3 json_load_object_hook.py

MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
[<MyObj(instance value goes here)>]

Hay ganchos similares disponibles para los tipos integrados enteros (parse_int), números de coma flotante (parse_float), y constantes (parse_constant).

Clases de codificador y decodificador

Además de las funciones de conveniencia ya cubiertas, el módulo json proporciona clases para la codificación y decodificación. Usando las clases directamente da acceso a interfaces de programación adicionales para personalizar su comportamiento.

El JSONEncoder utiliza una interfaz iterable para producir «fragmentos» de datos codificados, lo que facilita la escritura en archivos o sockets de red sin tener que representar una estructura completa de datos en 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('PART:', part)

La salida se genera en unidades lógicas, en lugar de estar basada en cualquier valor de tamaño.

$ python3 json_encoder_iterable.py

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

El método encode() es básicamente equivalente a ''.join(encoder.iterencode()), con una comprobación de error adicional por adelantado.

Para codificar objetos arbitrarios, anula el método default() con una implementación similar a la utilizada en 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('internal data')
print(obj)
print(MyEncoder().encode(obj))

La salida es la misma que la implementación anterior.

$ python3 json_encoder_default.py

<MyObj(internal data)>
default( <MyObj(internal data)> )
{"__class__": "MyObj", "__module__": "json_myobj", "s": "internal
data"}

Decodificar texto, luego convertir el diccionario en un objeto toma una un poco más de trabajo para configurar que la implementación anterior, pero no mucho.

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('MODULE:', module.__name__)
            class_ = getattr(module, class_name)
            print('CLASS:', class_)
            args = {
                key: value
                for key, value in d.items()
            }
            print('INSTANCE ARGS:', args)
            inst = class_(**args)
        else:
            inst = d
        return inst


encoded_object = '''
[{"s": "instance value goes here",
  "__module__": "json_myobj", "__class__": "MyObj"}]
'''

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

Y la salida es la misma que en el ejemplo anterior.

$ python3 json_decoder_object_hook.py

MODULE: json_myobj
CLASS: <class 'json_myobj.MyObj'>
INSTANCE ARGS: {'s': 'instance value goes here'}
[<MyObj(instance value goes here)>]

Trabajar con flujos y archivos

Todos los ejemplos hasta ahora han asumido que la versión codificada de la estructura de datos completa podría mantenerse en la memoria al mismo tiempo. Con grandes estructuras de datos, puede ser preferible escribir la codificación directamente a un objeto similar a un archivo. Las funciones de conveniencia load() y dump() aceptan referencias a un objeto similar a un archivo utilizar para leer o escribir.

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 identificador de archivo normal funcionaría de la misma manera que el búfer StringIO utilizado en este ejemplo.

$ python3 json_dump_file.py

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

Aunque no está optimizada para leer solo parte de los datos a la vez, la función load() ofrece la ventaja de encapsular la lógica de generar objetos del flujo de entrada.

json_load_file.py
import io
import json

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

Al igual que para dump(), cualquier objeto similar a un archivo se puede pasar a load().

$ python3 json_load_file.py

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

Flujos de datos mixtos

JSONDecoder incluye raw_decode(), un método para decodificar una estructura de datos seguida de más datos, como los datos JSON con texto al final. El valor de retorno es el objeto creado por la decodificación de los datos de entrada, y un índice en esos datos que indica dónde se detuvo la decodificación.

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 = 'This text is not JSON.'

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

print('Object              :', obj)
print('End of parsed input :', end)
print('Remaining text      :', repr(remaining))

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

Desafortunadamente, esto solo funciona si el objeto aparece al principio de la entrada.

$ python3 json_mixed_data.py

JSON first:
Object              : [{'a': 'A', 'c': 3.0, 'b': [2, 4]}]
End of parsed input : 35
Remaining text      : ' This text is not JSON.'

JSON embedded:
ERROR: Expecting value: line 1 column 1 (char 0)

JSON en la línea de comando

El módulo json.tool implementa un programa de línea de comandos para re formatear datos JSON para que sean más fácil de leer.

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

El archivo de entrada example.json contiene un mapeo con las llaves fuera de orden alfabético. El primer ejemplo a continuación muestra los datos re formateando en orden, y el segundo ejemplo usa --sort-keys para ordenar las llaves de mapeo antes de imprimir la salida.

$ python3 -m json.tool example.json

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

$ python3 -m json.tool --sort-keys example.json

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

Ver también