Saltar al contenido principal

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
JSONobjectarraystringnumbertrue / falsenull
Pythondictliststrint / floatTrue / FalseNone

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()​

Lectura de archivo JSON
import json

with open("config.json", "r", encoding="utf-8") as f:
datos = json.load(f)

print(datos["equipo"])
en este ejemplo
  • 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()​

Lectura desde string
import json

texto = '{"ciudad":"Sevilla","temperatura":28}'
datos = json.loads(texto)
print(datos["ciudad"])
Cuándo usar 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)​

Criteriojson.load(...)json.loads(...)
EntradaArchivoCadena (str)
Uso típicoPersistencia en discoDatos recibidos en memoria
SalidaObjeto PythonObjeto 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()​

Guardar JSON en disco
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)
en este ejemplo
  • json.dump(obj, f) escribe directamente en el fichero.
  • indent=4 mejora legibilidad para humanos.
  • ensure_ascii=False conserva acentos y eñes sin escapar.

🟦 Conversión a texto con json.dumps()​

Generar string JSON
import json

datos = {"mensaje": "Sistema listo", "ok": True}
texto_json = json.dumps(datos, indent=2, ensure_ascii=False)
print(texto_json)
Cuándo usar dumps()
  • Para enviar JSON por red.
  • Para logs, depuración o pruebas.
  • Para guardar temporalmente en memoria antes de escribir.

🟨 Comparativa (dump vs dumps)​

Criteriojson.dump(...)json.dumps(...)
SalidaArchivoCadena (str)
Uso típicoPersistir datos en discoSerializar 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:

Navegación segura con diccionarios vacíos
# 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​

CriterioAcceso DirectoAcceso Seguro
Ejemplod["a"]["b"]d.get("a", {}).get("b")
Si falta la claveError (KeyError)Devuelve None (o el valor indicado)
Por qué usar {} 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.

Lectura defensiva
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)​

Validar forma del JSON
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ónQué compruebaEjemplo
EstructuraForma del dato (dict/list, claves mínimas)Existe network y es dict
ContenidoReglas de negocio y rangostimeout > 0, entorno válido

Primero asegura estructura, después valida contenido.

Regla práctica

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.

Error típico de serialización
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​

Conversión personalizada
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)
en este ejemplo
  • default=convertir se ejecuta solo cuando json encuentra 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 TypeError para detectar el problema temprano.

🟨 Conversión de varios tipos en el mismo punto​

default para datetime y set
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.

Pipeline JSON básico
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​

buenas prácticas
  • Usa siempre with open(...) y encoding="utf-8".
  • Usa indent para que el archivo sea mantenible.
  • Usa ensure_ascii=False si hay texto en español.
  • Valida estructura y tipos antes de procesar.
  • Maneja JSONDecodeError al leer datos externos.
  • Evita pickle para 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 app
  • eventos_api.json -> eventos recibidos desde API externa
  • usuarios_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 tipo en INFO, WARN o ERROR.

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:

  • id
  • nombre
  • email
  • rol

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 None si el archivo no existe.
  • Devuelva None si el JSON está corrupto.
  • Muestre un mensaje claro en cada caso.

Objetivo: introducir una utilidad reutilizable para producción.