Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 3 de 30

JSON

Los cuatro tipos de Python que se transforman en otra cosa sin avisar, el precio que sale a 199,89999999999998, y las fechas que JSON no sabe guardar.

⏱ 15 min de lectura

JSON es el formato en el que viaja casi todo lo que hace una API, y tiene fama de trivial: cuatro tipos, dos funciones y a correr. Esa fama es merecida en el 90 % de los casos y es exactamente lo que hace peligroso el 10 % restante, porque los fallos de JSON no dan error: los datos llegan, se convierten en algo parecido pero distinto, y el programa sigue.

Esta lección es ese 10 %.

El mapeo#

JSON tiene seis tipos y Python tiene muchos más, así que la correspondencia es incompleta por definición:

JSONPython al leerPython al escribir
objectdictdict
arraylistlist, tuple
stringstrstr
numberint o floatint, float
true / falseboolbool
nullNoneNone

Dos funciones, y conviene tener clara la diferencia porque los nombres son confusos:

python
json.dumps(objeto)   # a texto      (la s es de string)
json.loads(texto)    # desde texto
json.dump(obj, f)    # a un fichero
json.load(f)         # desde un fichero

Lo que no sobrevive al viaje#

Aquí está lo que importa. Una ida y vuelta por JSON con varios tipos de Python:

Salida en pantalla
tupla          tuple     -> list   [1, 2, 3]
clave entera   dict      -> dict   {'1': 'uno'}
conjunto       set       -> TypeError: Object of type set is not JSON serializable
decimal        Decimal   -> TypeError: Object of type Decimal is not JSON serializable
fecha          date      -> TypeError: Object of type date is not JSON serializable
bytes          bytes     -> TypeError: Object of type bytes is not JSON serializable

Los cuatro últimos fallan ruidosamente, y eso está bien: te enteras.

Los dos primeros son los peligrosos, porque funcionan y cambian tus datos:

Una tupla vuelve como lista. Suele dar igual, hasta que algo compara (1, 2) == [1, 2], que es falso, o intenta usarla como clave de un diccionario.

Una clave entera vuelve como texto. Esta es la que muerde de verdad:

consola
>>> json.loads(json.dumps({1: 'uno'}))
{'1': 'uno'}

Guardas {42: 'Ana'}, lo lees, y datos[42] da KeyError mientras datos['42'] funciona. El motivo es que en JSON las claves solo pueden ser texto, así que Python convierte, y no avisa porque no lo considera un error.

Si tu diccionario tiene claves numéricas y va a viajar por JSON, hay dos opciones: aceptarlo y usar cadenas en los dos lados desde el principio, o mandar una lista de objetos, que es lo que hacen las APIs bien diseñadas.

El dinero#

Este es el problema práctico número uno de JSON y no es culpa de JSON: es que los decimales binarios no pueden representar exactamente 0,1, y lo arrastra cualquier programa que use float.

Salida en pantalla
sumar diez veces 19.99  ->  199.89999999999998   (debería ser 199.90)
8.35 * 3                ->  25.049999999999997   (debería ser 25.05)
round(8.35, 1)          ->  8.3                  (debería ser 8.4)

El tercero es el que más desconcierta: redondear a un decimal da 8,3 y no 8,4, porque el 8.35 guardado es en realidad un pelín menor que 8,35.

En una factura eso son céntimos que no cuadran, y no hay manera de arreglarlo a posteriori. La solución es no usar float para dinero nunca, y el módulo json trae el gancho:

python
import decimal

datos = json.loads(texto, parse_float=decimal.Decimal)
Salida en pantalla
parse_float=Decimal:  8.35 * 3  ->  25.05

Exacto, porque Decimal trabaja en base diez como las personas.

Y para el otro lado, escribir, hay que decirle cómo, porque Decimal no es serializable:

python
json.dumps(datos, default=str)

Las fechas#

JSON no tiene tipo fecha. Punto. Lo que hay es el acuerdo de escribirlas como texto en formato ISO 8601, que es lo que devuelve GitHub:

Salida en pantalla
"created_at": "2012-04-28T02:47:18Z"

Y leerlas es una línea de la biblioteca estándar:

Salida en pantalla
fromisoformat('2012-04-28T02:47:18Z')         -> 2012-04-28 02:47:18+00:00  tz=UTC
fromisoformat('2026-09-22T12:00:00+02:00')    -> 2026-09-22 12:00:00+02:00  tz=UTC+02:00
fromisoformat('2026-09-22')                   -> 2026-09-22 00:00:00  tz=None
fromisoformat('2026-09-22T12:00:00.123456Z')  -> ... .123456+00:00  tz=UTC

Los cuatro formatos, incluido el de la Z al final, que Python solo acepta desde la versión 3.11. Si ves por ahí código con s.replace('Z', '+00:00'), es de antes de eso.

Fíjate en el tercero: una fecha sin hora sale sin zona horaria, y mezclar fechas con y sin zona lanza un TypeError al compararlas. Es un fallo que aparece semanas después.

Para escribir, hay un atajo que casi funciona y conviene no usar:

Salida en pantalla
json.dumps(fecha)                -> TypeError: Object of type datetime is not JSON serializable
json.dumps(fecha, default=str)   -> {"cuando": "2026-09-22 12:00:00+00:00"}
fecha.isoformat()                -> {"cuando": "2026-09-22T12:00:00+00:00"}

Mira la diferencia entre las dos últimas: un espacio en lugar de la T. El default=str llama a str(), que en datetime produce un formato parecido a ISO pero que no lo es. Muchos analizadores lo aceptan, otros no, y el fallo aparece solo con el cliente escrito en otro lenguaje.

La forma correcta es explícita:

python
json.dumps(datos, default=lambda o: o.isoformat() if hasattr(o, 'isoformat') else str(o))

El texto y el español#

python
json.dumps({'nombre': 'Íñigo Ñuño', 'emoji': '🌐'})
Salida en pantalla
{"nombre": "\u00cd\u00f1igo \u00d1u\u00f1o", "emoji": "\ud83c\udf10"}

Legible no es. Eso pasa porque ensure_ascii vale True por defecto, que es una herencia de cuando no se podía dar por hecho que todo fuera UTF-8.

python
json.dumps(datos, ensure_ascii=False)
Salida en pantalla
{"nombre": "Íñigo Ñuño", "emoji": "🌐"}
Salida en pantalla
bytes: 69 con escapes, 45 sin ellos
los dos se leen igual: True

Los dos son JSON válido y los dos se leen igual, así que esto no es una cuestión de corrección: es de legibilidad y de tamaño. Un tercio menos de bytes en texto español, y registros que se pueden leer.

La recomendación para un proyecto en español: ensure_ascii=False siempre, asegurándose de que el fichero o la respuesta van en UTF-8, cosa que en una API es automática.