▷ JSON en Python — Leer, escribir, parsear y validar 2026

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.

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:

JSONPython
object {...}dict
array [...]list
stringstr
numberint o float
true / falseTrue / False
nullNone

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 tirar UnicodeDecodeError con 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 requests y 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ónFunción
String → Pythonjson.loads(texto)
Python → Stringjson.dumps(obj, indent=2, ensure_ascii=False)
Fichero → Pythonjson.load(f)
Python → Ficherojson.dump(obj, f, indent=2, ensure_ascii=False)
Respuesta de requestsr.json()
Error sintaxisjson.JSONDecodeError
Validar estructuraPydantic BaseModel o @dataclass con __post_init__
FechasA 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.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario