7.2.4 Ficheros JSON
JSON (JavaScript Object Notation) es un formato de texto usado para intercambiar y almacenar datos estructurados. En Python se integra muy bien porque su estructura se parece a diccionarios y listas.
A diferencia de CSV, JSON permite representar datos anidados (objetos dentro de objetos, listas dentro de objetos, etc.). Esto lo hace más expresivo para aplicaciones reales.
1️⃣ Qué es JSON y cómo se representa en Python
JSON usa tipos básicos. Python los traduce automáticamente al leer y escribir.
| Lenguaje | ||||||
|---|---|---|---|---|---|---|
| JSON | object | array | string | number | true / false | null |
| Python | dict | list | str | int / float | True / False | None |
Ejemplo JSON:
{
"equipo": "Mercurio",
"activo": true,
"miembros": ["Ana", "Luis", "Nora"],
"config": {
"timeout": 30,
"modo": "produccion"
}
}
2️⃣ Lectura de JSON
Al leer JSON en Python hay dos escenarios principales: leer desde archivo o leer desde una cadena en memoria.
🟩 Lectura desde archivo con json.load()
import json
with open("config.json", "r", encoding="utf-8") as f:
datos = json.load(f)
print(datos["equipo"])
json.load(f)recibe un archivo abierto.- Convierte el contenido JSON a estructuras Python (
dict,list, etc.). - Es la opción habitual cuando trabajas con persistencia en disco.
🟦 Lectura desde cadena con json.loads()
import json
texto = '{"ciudad":"Sevilla","temperatura":28}'
datos = json.loads(texto)
print(datos["ciudad"])
loads()- Cuando recibes JSON desde una API o socket.
- Cuando el JSON no está en fichero, sino en una variable de texto.
🟨 Comparativa (load vs loads)
| Criterio | json.load(...) | json.loads(...) |
|---|---|---|
| Entrada | Archivo | Cadena (str) |
| Uso típico | Persistencia en disco | Datos recibidos en memoria |
| Salida | Objeto Python | Objeto Python |
3️⃣ Escritura de JSON
Igual que en lectura, puedes escribir JSON directamente a archivo o convertirlo a texto en memoria.
🟩 Escritura a archivo con json.dump()
import json
config = {
"equipo": "Mercurio",
"activo": True,
"timeout": 30
}
with open("config_salida.json", "w", encoding="utf-8") as f:
json.dump(config, f, indent=4, ensure_ascii=False)
json.dump(obj, f)escribe directamente en el fichero.indent=4mejora legibilidad para humanos.ensure_ascii=Falseconserva acentos y eñes sin escapar.
🟦 Conversión a texto con json.dumps()
import json
datos = {"mensaje": "Sistema listo", "ok": True}
texto_json = json.dumps(datos, indent=2, ensure_ascii=False)
print(texto_json)
dumps()- Para enviar JSON por red.
- Para logs, depuración o pruebas.
- Para guardar temporalmente en memoria antes de escribir.
🟨 Comparativa (dump vs dumps)
| Criterio | json.dump(...) | json.dumps(...) |
|---|---|---|
| Salida | Archivo | Cadena (str) |
| Uso típico | Persistir datos en disco | Serializar en memoria |
Necesita open(...) | Sí | No |
4️⃣ Acceso y navegación por estructuras JSON
En datos reales, un JSON rara vez es plano. Lo habitual es encontrar varios niveles (anidación) y colecciones que obligan a navegar con precisión.
🟩 Visualizando la jerarquía
Imagina este JSON de un personaje:
{
"nombre": "Elora",
"stats": { "fuerza": 15, "magia": 85 },
"inventario": [
{"item": "Poción", "cantidad": 3},
{"item": "Báculo", "cantidad": 1}
]
}
Para llegar a la "Poción", bajamos por la estructura: inventario (lista) -> primer elemento (índice 0) -> item (clave).
🟦 El problema del "Acceso en Cadena"
Si intentas acceder a algo que no existe usando corchetes, Python lanzará un error y detendrá el programa:
# Peligro: Si "stats" no existe, lanza KeyError.
# Si existe pero no tiene "magia", también falla.
magia = datos["stats"]["magia"]
🟨 El truco del "Acceso Seguro" (.get)
Para evitar que el programa "explote", encadenamos .get() proporcionando un diccionario vacío como valor por defecto si la clave intermedia falta:
# Si "stats" no existe, el primer .get devuelve {}.
# El segundo .get busca "magia" en ese {} y devuelve 0.
magia = datos.get("stats", {}).get("magia", 0)
# Para listas, usamos una lista vacía por defecto
items = datos.get("inventario", [])
for i in items:
print(i.get("item"))
🟩 Comparativa de estrategias
| Criterio | Acceso Directo | Acceso Seguro |
|---|---|---|
| Ejemplo | d["a"]["b"] | d.get("a", {}).get("b") |
| Si falta la clave | Error (KeyError) | Devuelve None (o el valor indicado) |
{} en .get()Al navegar por niveles, .get("nivel1", {}) asegura que el siguiente nivel siempre sea un objeto (aunque esté vacío) sobre el que llamar a otro .get(). Si no pusieras el {}, el primer .get devolvería None, y el segundo daría un error fatal: AttributeError: 'NoneType' object has no attribute 'get'.
5️⃣ Validación y manejo de errores
Cuando trabajas con JSON externo (API, exportaciones, ficheros compartidos), debes asumir que puede fallar en tres niveles:
- El archivo no existe.
- El archivo existe pero no contiene JSON válido.
- El JSON es válido, pero no tiene la estructura esperada por tu programa.
Si no controlas esos casos, el flujo se rompe con excepciones o, peor, con errores silenciosos.
import json
try:
with open("entrada.json", "r", encoding="utf-8") as f:
datos = json.load(f)
except FileNotFoundError:
print("No existe el archivo de entrada.")
except json.JSONDecodeError:
print("El archivo no contiene JSON válido.")
🟩 Qué aporta este patrón
- Evita que el programa se detenga por un archivo faltante.
- Evita procesar basura cuando el JSON está corrupto.
- Te permite mostrar mensajes claros para depurar rápido.
🟦 Validación mínima de estructura (después del load)
def validar_config(config):
if not isinstance(config, dict):
return False
if "entorno" not in config or "network" not in config:
return False
if not isinstance(config["network"], dict):
return False
return True
🟨 Validar estructura vs validar contenido
| Tipo de validación | Qué comprueba | Ejemplo |
|---|---|---|
| Estructura | Forma del dato (dict/list, claves mínimas) | Existe network y es dict |
| Contenido | Reglas de negocio y rangos | timeout > 0, entorno válido |
Primero asegura estructura, después valida contenido.
Primero valida "forma" (tipo y claves mínimas), luego valida "contenido" (rangos, formatos, reglas de negocio).
6️⃣ Tipos no soportados y serialización personalizada
JSON no soporta directamente tipos como datetime, set o clases propias.
import json
from datetime import datetime
datos = {"fecha": datetime.now()}
# json.dumps(datos) -> TypeError
🟩 Por qué ocurre este error
json solo sabe serializar tipos estándar JSON (dict, list, str, int, float, bool, None).
Cuando encuentra un objeto fuera de ese conjunto (por ejemplo datetime), necesita que tú le indiques cómo convertirlo.
🟦 Solución con default
import json
from datetime import datetime
def convertir(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Tipo no serializable: {type(obj)}")
datos = {"fecha": datetime(2026, 2, 17, 10, 30)}
texto = json.dumps(datos, default=convertir, ensure_ascii=False)
print(texto)
default=convertirse ejecuta solo cuandojsonencuentra un tipo no soportado.- La función devuelve una versión serializable (en este caso, texto ISO para fecha/hora).
- Si no sabes convertir un tipo, es correcto lanzar
TypeErrorpara detectar el problema temprano.
🟨 Conversión de varios tipos en el mismo punto
import json
from datetime import datetime
def convertir(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, set):
return list(obj)
raise TypeError(f"Tipo no serializable: {type(obj)}")
Esto centraliza la estrategia de serialización y evita parches dispersos por el código.
7️⃣ Ejemplo completo: leer, transformar y guardar
Este pipeline resume el enfoque profesional en JSON: leer, filtrar por criterio y escribir solo lo útil.
import json
with open("usuarios.json", "r", encoding="utf-8") as f:
usuarios = json.load(f)
activos = []
for usuario in usuarios:
if usuario.get("activo") is True:
activos.append({
"id": usuario.get("id"),
"nombre": usuario.get("nombre"),
"rol": usuario.get("rol")
})
with open("usuarios_activos.json", "w", encoding="utf-8") as f:
json.dump(activos, f, indent=4, ensure_ascii=False)
Flujo recomendado: leer -> validar -> transformar -> escribir.
✅ Buenas prácticas recomendadas
- Usa siempre
with open(...)yencoding="utf-8". - Usa
indentpara que el archivo sea mantenible. - Usa
ensure_ascii=Falsesi hay texto en español. - Valida estructura y tipos antes de procesar.
- Maneja
JSONDecodeErroral leer datos externos. - Evita
picklepara intercambio entre sistemas.
🧪 Ejercicios prácticos – Protocolo Hermes
El panel de analítica ya funciona con CSV, pero ahora el equipo necesita configuración y reportes estructurados en JSON para integrarse con el backend.
Tu misión es leer, validar, transformar y generar ficheros JSON listos para producción.
Dispones de estos ficheros: 👉 Descárgalos aquí
config_app.json-> configuración principal de la appeventos_api.json-> eventos recibidos desde API externausuarios_servicio.json-> usuarios y estado de cuenta
🟢 Fase 1 – Lectura y verificación
🟩 Ejercicio 1 – Carga de configuración (config_app.json)
Carga el archivo y muestra:
- Nombre de la aplicación.
- Entorno activo (
dev,qa,prod). - Timeout de red.
Objetivo: practicar lectura con json.load() y acceso por claves.
🟦 Ejercicio 2 – Lectura desde cadena (loads)
Convierte a objeto Python el siguiente texto JSON:
'{"modulo":"auth","reintentos":3,"activo":true}'
Después imprime el nombre del módulo y el número de reintentos.
Objetivo: diferenciar lectura desde archivo y lectura desde string.
🟡 Fase 2 – Validación y limpieza
🟨 Ejercicio 3 – Eventos válidos (eventos_api.json)
Recorre la lista de eventos y conserva solo los que cumplan:
- Tienen
id_evento. - Tienen
timestamp. - Tienen
tipoenINFO,WARNoERROR.
Guarda los válidos en eventos_validos.json.
Objetivo: aplicar validación estructural en JSON.
🟧 Ejercicio 4 – Conteo por severidad
A partir de eventos_validos.json, calcula:
- Total de
INFO - Total de
WARN - Total de
ERROR
Muestra el resultado por pantalla como diccionario.
Objetivo: procesar datos ya normalizados.
🔵 Fase 3 – Transformación y salida
🟥 Ejercicio 5 – Exportación de usuarios activos (usuarios_servicio.json)
Genera usuarios_activos.json con solo usuarios que:
- Tengan
activo = true - Tengan email no vacío
Para cada usuario, conserva solo:
idnombreemailrol
Objetivo: transformar y reducir estructura JSON.
🟫 Ejercicio 6 – Resumen final (resumen_operativo.json)
Crea un JSON resumen con este formato:
{
"total_eventos_validos": 0,
"errores": 0,
"usuarios_activos": 0,
"generado_en": "2026-02-17T10:30:00"
}
La fecha puede generarse con datetime.now().isoformat().
Objetivo: consolidar lectura, cálculo y escritura en un único resultado.
🔴 Fase Final – Robustez
🟪 Ejercicio 7 – Lectura segura con try/except
Implementa una función cargar_json_seguro(ruta) que:
- Devuelva el contenido si el JSON es válido.
- Devuelva
Nonesi el archivo no existe. - Devuelva
Nonesi el JSON está corrupto. - Muestre un mensaje claro en cada caso.
Objetivo: introducir una utilidad reutilizable para producción.