JSON en Python — Leer, escribir, parsear y validar

JSON es el formato de intercambio de datos por excelencia. APIs, ficheros de configuración, exportaciones, mensajes entre servicios — si trabajas con datos en algún momento te vas a encontrar JSON. Por suerte, manejarlo en Python es de las cosas más fáciles que hay: el módulo json viene en la librería estándar y resuelve el 95% de los casos con cuatro funciones.
En esta entrada te enseño todo lo que necesitas en la práctica: leer y escribir desde fichero, parsear strings, llamar APIs que devuelven JSON, validar lo que recibes, manejar fechas y objetos custom, y los errores típicos que te tiran un script en producción.
Contenido
- 1 La idea en 30 segundos
- 2 Leer JSON desde un fichero
- 3 Escribir JSON a fichero
- 4 Parsear y serializar strings
- 5 JSON desde una API
- 6 Manejar errores al parsear
- 7 Validar la estructura de lo que recibes
- 8 Casos especiales: fechas, decimales, objetos custom
- 9 Casos reales típicos
- 10 Errores típicos al usar JSON
- 11 Resumen
- 12 Tu siguiente paso
La idea en 30 segundos
JSON es un formato de texto que representa datos como objetos (con claves y valores), arrays (listas), números, strings, booleanos y null. En Python, el módulo json traduce entre JSON y los tipos nativos:
| JSON | Python |
|---|---|
object {...} | dict |
array [...] | list |
string | str |
number | int o float |
true / false | True / False |
null | None |
Y dos pares de funciones que cubren prácticamente todo:
json.loads(texto)→ parsea un string JSON a Python.json.dumps(obj)→ serializa un objeto Python a string JSON.json.load(fichero)→ lee de fichero abierto.json.dump(obj, fichero)→ escribe a fichero abierto.
(Truco mnemotécnico: la s final es de “string”. loads/dumps trabajan con strings, load/dump con ficheros.)
Leer JSON desde un fichero
Caso típico: tienes peliculas.json con datos y quieres cargarlos en Python.
{
"peliculas": [
{"titulo": "Inception", "year": 2010, "rating": 8.8},
{"titulo": "Heat", "year": 1995, "rating": 8.3}
]
}
import json
with open("peliculas.json", "r", encoding="utf-8") as f:
datos = json.load(f)
print(datos["peliculas"][0]["titulo"]) # Inception
json.load(f) lee el fichero y te devuelve un diccionario Python con la estructura entera. A partir de ahí, lo trabajas con [], .get(), comprehensions… como cualquier dict y lista normales.
💡 Siempre
encoding="utf-8". Si te ahorras esa línea, en Windows te puede tirarUnicodeDecodeErrorcon tildes y emojis. Con UTF-8 explícito, va siempre.
Escribir JSON a fichero
Lo opuesto. Tienes datos en Python y los quieres guardar:
import json
datos = {
"peliculas": [
{"titulo": "Arrival", "year": 2016, "rating": 8.0},
{"titulo": "Dune", "year": 2021, "rating": 8.0},
]
}
with open("peliculas.json", "w", encoding="utf-8") as f:
json.dump(datos, f, indent=2, ensure_ascii=False)
Dos detalles que cambian la calidad del output:
indent=2— formatea con sangrías de 2 espacios. Sin esto te queda toda la estructura en una sola línea ilegible.ensure_ascii=False— guarda tildes y caracteres no-ASCII tal cual. Sin esto, “Crónica” te queda escapado a"Crónica". Funciona, pero es feísimo de leer.
📥 Llévate el cheatsheet de Python (gratis)
PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.
Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.
Parsear y serializar strings
Si lo que tienes es un string (típico de respuestas HTTP, mensajes en cola, parámetros), no abras un fichero ficticio — usa la versión con s:
import json
texto = '{"nombre": "Ana", "edad": 30, "activo": true}'
datos = json.loads(texto)
# datos = {"nombre": "Ana", "edad": 30, "activo": True}
# y al revés:
de_vuelta = json.dumps(datos, indent=2)
print(de_vuelta)
# {
# "nombre": "Ana",
# "edad": 30,
# "activo": true
# }
JSON desde una API
El caso real más típico. Llamas una API con requests, recibes JSON, lo procesas:
import requests
r = requests.get("https://api.github.com/repos/python/cpython")
r.raise_for_status() # lanza si la API devolvió error
datos = r.json() # ya parsea por ti, no hace falta json.loads()
print(datos["name"]) # cpython
print(datos["stargazers_count"])
r.json() es atajo de json.loads(r.text). Si la API devuelve algo que no es JSON válido, lanza json.JSONDecodeError.
💡 ¿Quieres entender APIs REST a fondo, con
requestsy autenticación? Mira APIs en Python con OpenAI y Claude — el patrón es el mismo, cambia solo el endpoint.
Manejar errores al parsear
JSON malformado es el error más típico. La excepción es json.JSONDecodeError:
import json
texto_malo = '{"nombre": "Ana", "edad": 30,}' # coma de más al final
try:
datos = json.loads(texto_malo)
except json.JSONDecodeError as e:
print(f"JSON inválido: {e.msg} (línea {e.lineno}, columna {e.colno})")
Si lees de fichero, además puedes encontrarte FileNotFoundError. Patrón profesional:
import json
from pathlib import Path
ruta = Path("peliculas.json")
try:
with ruta.open("r", encoding="utf-8") as f:
datos = json.load(f)
except FileNotFoundError:
print(f"No existe el fichero {ruta}")
datos = {}
except json.JSONDecodeError as e:
print(f"JSON corrupto en {ruta}: {e}")
datos = {}Validar la estructura de lo que recibes
Que un JSON sea válido sintácticamente no significa que tenga las claves que esperabas. Si lo recibes de una API o de un usuario, valídalo antes de usarlo:
def es_pelicula_valida(d: dict) -> bool:
return (
isinstance(d, dict)
and "titulo" in d
and "year" in d
and isinstance(d["year"], int)
)
datos = json.loads(respuesta_api)
if not es_pelicula_valida(datos):
raise ValueError("La respuesta no tiene el formato esperado")
Esto funciona pero es manual. Para validación seria, lo profesional es usar Pydantic o dataclasses con __post_init__:
from dataclasses import dataclass
@dataclass
class Pelicula:
titulo: str
year: int
rating: float
def __post_init__(self):
if not isinstance(self.year, int):
raise TypeError("year debe ser int")
if self.year < 1888:
raise ValueError(f"Year inválido: {self.year}")
datos = json.loads('{"titulo": "Heat", "year": 1995, "rating": 8.3}')
p = Pelicula(**datos) # valida al construir
Y si quieres lo realmente profesional, Pydantic te hace todo esto declarativo:
from pydantic import BaseModel
class Pelicula(BaseModel):
titulo: str
year: int
rating: float
p = Pelicula.model_validate_json(respuesta_api_str)
Casos especiales: fechas, decimales, objetos custom
JSON no tiene un tipo para fechas. Las pasas siempre como string:
from datetime import date
import json
datos = {"titulo": "Heat", "estreno": date(1995, 12, 15)}
json.dumps(datos) # ❌ TypeError: Object of type date is not JSON serializable
Soluciones, en orden de calidad:
# 1. Convertir antes de serializar
datos["estreno"] = datos["estreno"].isoformat()
json.dumps(datos)
# {"titulo": "Heat", "estreno": "1995-12-15"}
# 2. Función default custom
def serializar(o):
if isinstance(o, date):
return o.isoformat()
raise TypeError
json.dumps(datos, default=serializar)
Y al leer, la fecha viene como string — la conviertes con date.fromisoformat():
from datetime import date
texto = '{"estreno": "1995-12-15"}'
datos = json.loads(texto)
fecha = date.fromisoformat(datos["estreno"])Para decimales (Decimal), pasa lo mismo: no son nativos en JSON. Mejor convertir a str y reconstruir al leer si te importa la precisión.
Casos reales típicos
Configuración de una app en JSON
import json
with open("config.json", encoding="utf-8") as f:
config = json.load(f)
DB_URL = config["database"]["url"]
DEBUG = config.get("debug", False)
Cachear resultado de una API en disco
import json
import requests
from pathlib import Path
cache = Path("repos.json")
if cache.exists():
datos = json.loads(cache.read_text(encoding="utf-8"))
else:
r = requests.get("https://api.github.com/users/python/repos")
r.raise_for_status()
datos = r.json()
cache.write_text(json.dumps(datos, indent=2), encoding="utf-8")
Convertir lista de dicts a JSON Lines (un objeto por línea)
Útil cuando tienes muchos objetos y los procesas en streaming:
import json
eventos = [{"id": 1}, {"id": 2}, {"id": 3}]
with open("eventos.jsonl", "w", encoding="utf-8") as f:
for evento in eventos:
f.write(json.dumps(evento) + "\n")
Errores típicos al usar JSON
# 1. Olvidar encoding="utf-8" → Unicode rotos en Windows
open("datos.json") # ❌
open("datos.json", encoding="utf-8") # ✓
# 2. ensure_ascii por defecto deja escapados los caracteres no-ASCII
json.dumps({"ciudad": "Cádiz"})
# '{"ciudad": "C\\u00e1diz"}' ← funciona pero ilegible
json.dumps({"ciudad": "Cádiz"}, ensure_ascii=False)
# '{"ciudad": "Cádiz"}' ← legible
# 3. Confundir json.load (fichero) con json.loads (string)
texto = '{"a": 1}'
json.load(texto) # ❌ AttributeError: 'str' object has no attribute 'read'
json.loads(texto) # ✓
# 4. Asumir que JSON tiene tipos que no tiene
json.dumps({"f": datetime.now()}) # ❌ no hay tipo fecha
json.dumps({"d": Decimal("1.50")}) # ❌ no hay tipo Decimal
# 5. Tuplas se serializan como arrays, pero al leerlas vuelven como listas
datos = (1, 2, 3)
texto = json.dumps(datos) # "[1, 2, 3]"
recuperado = json.loads(texto) # [1, 2, 3] (lista, no tupla)
Resumen
| Operación | Función |
|---|---|
| String → Python | json.loads(texto) |
| Python → String | json.dumps(obj, indent=2, ensure_ascii=False) |
| Fichero → Python | json.load(f) |
| Python → Fichero | json.dump(obj, f, indent=2, ensure_ascii=False) |
Respuesta de requests | r.json() |
| Error sintaxis | json.JSONDecodeError |
| Validar estructura | Pydantic BaseModel o @dataclass con __post_init__ |
| Fechas | A string ISO con .isoformat() y date.fromisoformat() |
¿Te ha valido esto?
Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.
Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.
Tu siguiente paso
JSON es la lengua franca del intercambio de datos hoy. Si dominas estas cuatro funciones + el patrón de validación con Pydantic/dataclasses, ya tienes resuelto el 95% de lo que vas a encontrar en proyectos reales. Y la próxima vez que una API te devuelva un JSON con 50 niveles de anidamiento, sabes exactamente cómo abrirla y validarla sin sufrir.
Si quieres aprender Python desde la base hasta consumir APIs, persistir datos y construir aplicaciones reales, en El Pythonista lo enseño paso a paso.
Un abrazo,
Oscar
¿Quieres aprender Python en orden, no a saltos?
Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.
37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía
