Python
Tutorial Python: Módulo json
Aprende a usar el módulo json en Python para serializar, deserializar y manejar estructuras JSON anidadas con ejemplos prácticos y personalización avanzada.
Aprende Python y certifícateSerialización y deserialización
La serialización es el proceso de convertir estructuras de datos de Python en un formato que puede ser fácilmente almacenado o transmitido, mientras que la deserialización es el proceso inverso. El módulo json
de Python proporciona funciones para realizar estas operaciones con el formato JSON (JavaScript Object Notation), un estándar ligero de intercambio de datos ampliamente utilizado en aplicaciones web y APIs.
Conceptos básicos
JSON es un formato de texto que resulta fácil de leer tanto para humanos como para máquinas. Su sintaxis es similar a la de los diccionarios y listas de Python, lo que facilita la conversión entre ambos formatos.
Para trabajar con JSON en Python, primero debemos importar el módulo:
import json
Serialización (Python → JSON)
La función principal para serializar datos es json.dumps()
(para generar una cadena) o json.dump()
(para escribir directamente a un archivo):
# Creamos un diccionario de Python
usuario = {
"nombre": "Ana García",
"edad": 28,
"activo": True,
"intereses": ["programación", "música", "senderismo"],
"dirección": {
"calle": "Calle Mayor 23",
"ciudad": "Madrid",
"código_postal": "28001"
}
}
# Convertimos a cadena JSON
json_str = json.dumps(usuario)
print(json_str)
El resultado será una cadena JSON válida:
{"nombre": "Ana García", "edad": 28, "activo": true, "intereses": ["programación", "música", "senderismo"], "dirección": {"calle": "Calle Mayor 23", "ciudad": "Madrid", "código_postal": "28001"}}
Observa que los valores booleanos de Python (True
) se han convertido a minúsculas (true
) en JSON, siguiendo el estándar.
Mejorando la legibilidad
Para mejorar la legibilidad del JSON generado, podemos usar el parámetro indent
:
# JSON con formato legible
json_formateado = json.dumps(usuario, indent=4)
print(json_formateado)
Esto producirá:
{
"nombre": "Ana García",
"edad": 28,
"activo": true,
"intereses": [
"programación",
"música",
"senderismo"
],
"dirección": {
"calle": "Calle Mayor 23",
"ciudad": "Madrid",
"código_postal": "28001"
}
}
Guardando JSON en un archivo
Para guardar directamente a un archivo, usamos json.dump()
:
# Guardamos el JSON en un archivo
with open("usuario.json", "w", encoding="utf-8") as archivo:
json.dump(usuario, archivo, indent=4, ensure_ascii=False)
El parámetro ensure_ascii=False
permite que los caracteres no ASCII (como acentos o ñ) se guarden directamente en lugar de convertirse a secuencias de escape Unicode, lo que mejora la legibilidad para textos en español.
Deserialización (JSON → Python)
Para convertir JSON a estructuras de datos de Python, usamos json.loads()
(para cadenas) o json.load()
(para archivos):
# Cadena JSON
json_data = '{"nombre": "Carlos Ruiz", "edad": 35, "habilidades": ["Python", "SQL", "JavaScript"]}'
# Convertimos a un diccionario de Python
datos_python = json.loads(json_data)
# Ahora podemos acceder como a cualquier diccionario
print(f"Nombre: {datos_python['nombre']}")
print(f"Primera habilidad: {datos_python['habilidades'][0]}")
Leyendo JSON desde un archivo
Para leer JSON desde un archivo:
# Leemos el JSON desde un archivo
with open("usuario.json", "r", encoding="utf-8") as archivo:
datos_usuario = json.load(archivo)
print(f"Ciudad: {datos_usuario['dirección']['ciudad']}")
Tipos de datos compatibles
El módulo json
puede serializar los siguientes tipos de datos de Python:
- dict: Se convierte en un objeto JSON
- list, tuple: Se convierten en arrays JSON
- str: Se convierte en una cadena JSON
- int, float: Se convierten en números JSON
- True, False: Se convierten en true/false JSON
- None: Se convierte en null JSON
Manejo de errores comunes
Es importante manejar posibles errores durante la serialización y deserialización:
try:
with open("config.json", "r") as archivo:
configuracion = json.load(archivo)
except FileNotFoundError:
print("El archivo de configuración no existe. Usando valores predeterminados.")
configuracion = {"debug": False, "timeout": 30}
except json.JSONDecodeError:
print("El archivo de configuración contiene JSON inválido.")
configuracion = {"debug": True, "timeout": 60}
Caso práctico: Almacenamiento de configuraciones
Un uso común de JSON es almacenar configuraciones de aplicaciones:
def guardar_configuracion(config):
"""Guarda la configuración de la aplicación en un archivo JSON."""
try:
with open("config.json", "w") as archivo:
json.dump(config, archivo, indent=2)
return True
except Exception as e:
print(f"Error al guardar la configuración: {e}")
return False
def cargar_configuracion():
"""Carga la configuración desde un archivo JSON o usa valores predeterminados."""
config_predeterminada = {
"tema": "claro",
"notificaciones": True,
"idioma": "es",
"volumen": 75
}
try:
with open("config.json", "r") as archivo:
return json.load(archivo)
except (FileNotFoundError, json.JSONDecodeError):
# Si el archivo no existe o está corrupto, guardamos la configuración predeterminada
guardar_configuracion(config_predeterminada)
return config_predeterminada
# Uso
config = cargar_configuracion()
print(f"Tema actual: {config['tema']}")
# Modificamos y guardamos
config["tema"] = "oscuro"
guardar_configuracion(config)
Caso práctico: Consumo de APIs
JSON es el formato estándar para comunicarse con APIs web:
import json
import urllib.request
def obtener_datos_api(url):
"""Obtiene y procesa datos JSON de una API."""
try:
# Realizamos la petición HTTP
with urllib.request.urlopen(url) as respuesta:
# Leemos y decodificamos la respuesta
datos_json = json.loads(respuesta.read().decode())
return datos_json
except urllib.error.URLError as e:
print(f"Error de conexión: {e}")
return None
except json.JSONDecodeError:
print("La respuesta no contiene JSON válido")
return None
# Ejemplo con una API pública que devuelve información sobre países
datos_pais = obtener_datos_api("https://restcountries.com/v3.1/name/spain")
if datos_pais:
# La API devuelve una lista, tomamos el primer elemento
pais = datos_pais[0]
print(f"País: {pais['name']['common']}")
print(f"Capital: {pais['capital'][0]}")
print(f"Población: {pais['population']:,} habitantes")
La serialización y deserialización con el módulo json
son operaciones fundamentales para el intercambio de datos en aplicaciones modernas. Dominar estas técnicas te permitirá trabajar eficientemente con APIs, archivos de configuración y cualquier sistema que requiera persistencia o transmisión de datos estructurados.
Trabajar con estructuras anidadas
Cuando trabajamos con JSON en Python, frecuentemente nos encontramos con estructuras anidadas que contienen múltiples niveles de objetos y arrays. Estas estructuras complejas representan relaciones jerárquicas de datos que son comunes en configuraciones de aplicaciones, respuestas de API y bases de datos documentales.
Navegando por estructuras JSON anidadas
Para acceder a elementos dentro de estructuras anidadas, simplemente encadenamos los operadores de acceso (corchetes para listas y corchetes o punto para diccionarios):
import json
# Estructura JSON anidada compleja
datos_json = '''
{
"aplicacion": "GestorTareas",
"version": "2.5.1",
"usuarios": [
{
"id": 1,
"nombre": "Laura Martínez",
"rol": "administrador",
"proyectos": [
{
"id": 101,
"nombre": "Rediseño web",
"tareas": [
{"id": 1001, "descripcion": "Wireframes", "completada": true},
{"id": 1002, "descripcion": "Diseño UI", "completada": false}
]
},
{
"id": 102,
"nombre": "App móvil",
"tareas": [
{"id": 2001, "descripcion": "Prototipo", "completada": true}
]
}
]
},
{
"id": 2,
"nombre": "Carlos Sánchez",
"rol": "desarrollador",
"proyectos": []
}
]
}
'''
# Convertimos la cadena JSON a un diccionario de Python
datos = json.loads(datos_json)
Ahora podemos navegar por esta estructura para extraer información específica:
# Accedemos a valores anidados
nombre_app = datos["aplicacion"]
print(f"Aplicación: {nombre_app}")
# Accedemos al primer usuario
primer_usuario = datos["usuarios"][0]
print(f"Primer usuario: {primer_usuario['nombre']}")
# Accedemos a una tarea específica del primer proyecto del primer usuario
primera_tarea = datos["usuarios"][0]["proyectos"][0]["tareas"][0]
print(f"Primera tarea: {primera_tarea['descripcion']}")
Manejo seguro de estructuras anidadas
Cuando trabajamos con estructuras anidadas, es común encontrarnos con KeyError o IndexError si intentamos acceder a claves o índices que no existen. Para evitar estos errores, podemos usar métodos más seguros:
# Método 1: Usar el método get() de los diccionarios
segundo_usuario_proyectos = datos["usuarios"][1].get("proyectos", [])
print(f"Proyectos del segundo usuario: {segundo_usuario_proyectos}")
# Método 2: Verificar la existencia antes de acceder
if "usuarios" in datos and len(datos["usuarios"]) > 2:
tercer_usuario = datos["usuarios"][2]
print(f"Tercer usuario: {tercer_usuario['nombre']}")
else:
print("No hay un tercer usuario")
Recorriendo estructuras anidadas
Para procesar todos los elementos de una estructura anidada, podemos usar bucles anidados:
# Recorremos todos los usuarios y sus proyectos
for i, usuario in enumerate(datos["usuarios"]):
print(f"\nUsuario {i+1}: {usuario['nombre']}")
if not usuario["proyectos"]:
print(" No tiene proyectos asignados")
continue
for proyecto in usuario["proyectos"]:
print(f" Proyecto: {proyecto['nombre']}")
for tarea in proyecto["tareas"]:
estado = "✓" if tarea["completada"] else "✗"
print(f" [{estado}] {tarea['descripcion']}")
Modificación de estructuras anidadas
Podemos modificar estructuras JSON anidadas como cualquier otra estructura de datos en Python:
# Añadimos un nuevo proyecto al primer usuario
nuevo_proyecto = {
"id": 103,
"nombre": "Análisis de datos",
"tareas": [
{"id": 3001, "descripcion": "Recopilación de datos", "completada": False}
]
}
datos["usuarios"][0]["proyectos"].append(nuevo_proyecto)
# Modificamos el estado de una tarea existente
datos["usuarios"][0]["proyectos"][0]["tareas"][1]["completada"] = True
# Añadimos un campo a todos los proyectos
for usuario in datos["usuarios"]:
for proyecto in usuario.get("proyectos", []):
proyecto["fecha_inicio"] = "2023-09-15"
Búsqueda en estructuras anidadas
Buscar información específica en estructuras anidadas puede ser complejo. Veamos algunas estrategias:
# Función para buscar una tarea por ID en toda la estructura
def buscar_tarea_por_id(datos, tarea_id):
for usuario in datos["usuarios"]:
for proyecto in usuario.get("proyectos", []):
for tarea in proyecto.get("tareas", []):
if tarea["id"] == tarea_id:
return {
"tarea": tarea,
"proyecto": proyecto["nombre"],
"usuario": usuario["nombre"]
}
return None
# Buscamos la tarea con ID 1002
resultado = buscar_tarea_por_id(datos, 1002)
if resultado:
print(f"\nTarea encontrada: '{resultado['tarea']['descripcion']}'")
print(f"Asignada a: {resultado['usuario']} en el proyecto '{resultado['proyecto']}'")
Filtrado de datos anidados
Podemos usar comprensiones de listas para filtrar datos de estructuras anidadas:
# Obtenemos todas las tareas pendientes de todos los usuarios
tareas_pendientes = [
{
"descripcion": tarea["descripcion"],
"proyecto": proyecto["nombre"],
"usuario": usuario["nombre"]
}
for usuario in datos["usuarios"]
for proyecto in usuario.get("proyectos", [])
for tarea in proyecto.get("tareas", [])
if not tarea["completada"]
]
print("\nTareas pendientes:")
for tarea in tareas_pendientes:
print(f"- {tarea['descripcion']} ({tarea['proyecto']}, {tarea['usuario']})")
Transformación de estructuras anidadas
A veces necesitamos transformar la estructura de nuestros datos JSON:
# Transformamos la estructura para tener un formato diferente
estructura_por_proyectos = {}
for usuario in datos["usuarios"]:
for proyecto in usuario.get("proyectos", []):
# Si el proyecto no existe en nuestro diccionario, lo inicializamos
if proyecto["id"] not in estructura_por_proyectos:
estructura_por_proyectos[proyecto["id"]] = {
"nombre": proyecto["nombre"],
"participantes": [],
"tareas": proyecto.get("tareas", [])
}
# Añadimos el usuario como participante
estructura_por_proyectos[proyecto["id"]]["participantes"].append(usuario["nombre"])
# Convertimos a JSON formateado
json_transformado = json.dumps(estructura_por_proyectos, indent=2)
print(f"\nEstructura transformada:\n{json_transformado}")
Validación de estructuras anidadas
Antes de procesar datos JSON complejos, es recomendable validar su estructura:
def validar_estructura_usuario(usuario):
"""Valida que un usuario tenga la estructura esperada."""
campos_requeridos = ["id", "nombre", "rol"]
for campo in campos_requeridos:
if campo not in usuario:
return False
# Validamos que proyectos sea una lista
if "proyectos" not in usuario or not isinstance(usuario["proyectos"], list):
return False
return True
# Validamos todos los usuarios
usuarios_validos = all(validar_estructura_usuario(usuario) for usuario in datos["usuarios"])
print(f"\n¿Todos los usuarios tienen estructura válida? {usuarios_validos}")
Caso práctico: Configuración de aplicación multinivel
Un uso común de estructuras JSON anidadas es para configuraciones de aplicaciones con múltiples entornos:
# Configuración con valores por defecto y específicos por entorno
config_json = '''
{
"default": {
"database": {
"host": "localhost",
"port": 5432,
"timeout": 30
},
"api": {
"url": "https://api.ejemplo.com",
"key": "default-key",
"rate_limit": 100
},
"logging": {
"level": "INFO",
"file": "app.log"
}
},
"development": {
"database": {
"host": "dev-db",
"user": "dev_user"
},
"logging": {
"level": "DEBUG"
}
},
"production": {
"database": {
"host": "prod-db.ejemplo.com",
"user": "prod_user",
"replicas": ["replica1.ejemplo.com", "replica2.ejemplo.com"]
},
"api": {
"key": "prod-secure-key"
},
"logging": {
"level": "WARNING"
}
}
}
'''
config = json.loads(config_json)
def obtener_configuracion(entorno):
"""Obtiene la configuración combinada para un entorno específico."""
# Comenzamos con la configuración por defecto
config_combinada = config["default"].copy()
# Si el entorno existe, actualizamos con sus valores específicos
if entorno in config:
# Para cada sección en el entorno específico
for seccion, valores in config[entorno].items():
if seccion in config_combinada:
# Actualizamos la sección existente
config_combinada[seccion].update(valores)
else:
# Añadimos la sección completa
config_combinada[seccion] = valores
return config_combinada
# Obtenemos la configuración para desarrollo
config_dev = obtener_configuracion("development")
print(f"\nConfiguración de desarrollo:")
print(f"Host BD: {config_dev['database']['host']}")
print(f"Nivel de log: {config_dev['logging']['level']}")
print(f"URL API: {config_dev['api']['url']}")
# Obtenemos la configuración para producción
config_prod = obtener_configuracion("production")
print(f"\nConfiguración de producción:")
print(f"Host BD: {config_prod['database']['host']}")
print(f"Réplicas BD: {', '.join(config_prod['database']['replicas'])}")
print(f"Clave API: {config_prod['api']['key']}")
Trabajar con estructuras JSON anidadas en Python es sencillo gracias a la similitud entre la sintaxis de JSON y las estructuras de datos nativas de Python. Con las técnicas mostradas, podrás navegar, modificar y transformar incluso las estructuras más complejas de manera eficiente.
Personalización de codificación/decodificación
El módulo json
de Python ofrece mecanismos de personalización que permiten controlar con precisión cómo se serializan y deserializan los objetos. Esto resulta especialmente útil cuando trabajamos con tipos de datos que no son directamente compatibles con JSON o cuando necesitamos formatos específicos para nuestros datos.
Personalizando la serialización con parámetros
Antes de explorar soluciones avanzadas, veamos los parámetros básicos que ofrece json.dumps()
para personalizar la salida:
import json
datos = {
"nombre": "María López",
"edad": 32,
"coordenadas": (40.4168, -3.7038), # Una tupla
"idiomas": ["español", "inglés", "francés"]
}
# Personalización básica
json_personalizado = json.dumps(
datos,
indent=2, # Indentación para legibilidad
sort_keys=True, # Ordenar claves alfabéticamente
ensure_ascii=False, # Permitir caracteres no ASCII directamente
separators=(',', ': ') # Personalizar separadores
)
print(json_personalizado)
Manejando tipos de datos no compatibles
El problema más común al serializar es encontrarse con tipos de datos que JSON no soporta nativamente, como fechas, conjuntos, tuplas o clases personalizadas. Para estos casos, podemos usar el parámetro default
:
from datetime import datetime, date
import decimal
datos_complejos = {
"fecha_actual": datetime.now(),
"fecha_nacimiento": date(1990, 5, 15),
"precio": decimal.Decimal("19.99"),
"categorías": {"Python", "JSON", "Serialización"} # Un conjunto (set)
}
# Función para convertir tipos no compatibles
def convertidor_personalizado(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
elif isinstance(obj, set):
return list(obj)
# Si no sabemos manejar el tipo, lanzamos un error
raise TypeError(f"El objeto de tipo {type(obj)} no es serializable a JSON")
# Serialización con convertidor personalizado
try:
json_resultado = json.dumps(
datos_complejos,
default=convertidor_personalizado,
indent=2
)
print(json_resultado)
except TypeError as e:
print(f"Error de serialización: {e}")
Creando un codificador personalizado
Para casos más complejos, podemos crear una subclase de json.JSONEncoder
que maneje múltiples tipos personalizados:
class EncodificadorAvanzado(json.JSONEncoder):
def default(self, obj):
# Manejamos fechas y horas
if isinstance(obj, datetime):
return {
"__tipo__": "datetime",
"valor": obj.isoformat()
}
elif isinstance(obj, date):
return {
"__tipo__": "date",
"valor": obj.isoformat()
}
# Manejamos números decimales
elif isinstance(obj, decimal.Decimal):
return {
"__tipo__": "decimal",
"valor": str(obj)
}
# Manejamos conjuntos
elif isinstance(obj, set):
return {
"__tipo__": "set",
"valor": list(obj)
}
# Manejamos tuplas (preservando que es una tupla)
elif isinstance(obj, tuple):
return {
"__tipo__": "tuple",
"valor": list(obj)
}
# Delegamos a la implementación padre para tipos estándar
return super().default(obj)
# Usamos nuestro codificador personalizado
json_avanzado = json.dumps(
datos_complejos,
cls=EncodificadorAvanzado,
indent=2
)
print(json_avanzado)
Este enfoque no solo convierte los tipos no compatibles, sino que también preserva información sobre el tipo original mediante metadatos, lo que facilita la deserialización posterior.
Personalizando la deserialización
Para deserializar los objetos personalizados, necesitamos un decodificador personalizado que interprete los metadatos que añadimos durante la serialización:
class DecodificadorAvanzado(json.JSONDecoder):
def __init__(self, *args, **kwargs):
# Configuramos el decodificador con un hook para objetos
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, obj):
# Verificamos si el objeto tiene nuestro marcador de tipo
if "__tipo__" not in obj:
return obj
tipo = obj["__tipo__"]
valor = obj["valor"]
# Reconstruimos el objeto según su tipo
if tipo == "datetime":
return datetime.fromisoformat(valor)
elif tipo == "date":
return date.fromisoformat(valor)
elif tipo == "decimal":
return decimal.Decimal(valor)
elif tipo == "set":
return set(valor)
elif tipo == "tuple":
return tuple(valor)
# Si no reconocemos el tipo, devolvemos el objeto sin modificar
return obj
# Deserializamos usando nuestro decodificador personalizado
datos_reconstruidos = json.loads(json_avanzado, cls=DecodificadorAvanzado)
# Verificamos que los tipos se hayan reconstruido correctamente
print(f"Fecha actual: {datos_reconstruidos['fecha_actual']} (tipo: {type(datos_reconstruidos['fecha_actual']).__name__})")
print(f"Categorías: {datos_reconstruidos['categorías']} (tipo: {type(datos_reconstruidos['categorías']).__name__})")
Usando el parámetro object_hook
Una alternativa más sencilla para personalizar la deserialización es usar directamente el parámetro object_hook
:
def reconstruir_objetos(obj):
# Verificamos si hay marcadores de tipo en el objeto
if "__tipo__" in obj:
if obj["__tipo__"] == "datetime":
return datetime.fromisoformat(obj["valor"])
elif obj["__tipo__"] == "set":
return set(obj["valor"])
# ... otros tipos
return obj
# Deserializamos con el hook
datos_simples = json.loads(json_avanzado, object_hook=reconstruir_objetos)
Serializando objetos personalizados
Uno de los casos más comunes es necesitar serializar instancias de clases personalizadas. Veamos cómo hacerlo:
class Producto:
def __init__(self, id, nombre, precio, stock=0):
self.id = id
self.nombre = nombre
self.precio = precio
self.stock = stock
def __repr__(self):
return f"Producto({self.id}, {self.nombre}, {self.precio}, {self.stock})"
# Método 1: Añadir método to_json a la clase
class ProductoSerializable(Producto):
def to_json(self):
return {
"id": self.id,
"nombre": self.nombre,
"precio": float(self.precio),
"stock": self.stock
}
# Método 2: Usar un codificador personalizado
class CodificadorProducto(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Producto):
return {
"__tipo__": "Producto",
"id": obj.id,
"nombre": obj.nombre,
"precio": float(obj.precio) if isinstance(obj.precio, decimal.Decimal) else obj.precio,
"stock": obj.stock
}
return super().default(obj)
# Creamos algunos productos
productos = [
ProductoSerializable(1, "Teclado mecánico", decimal.Decimal("89.99"), 15),
ProductoSerializable(2, "Ratón inalámbrico", decimal.Decimal("45.50"), 8)
]
# Método 1: Usando el método to_json
json_productos1 = json.dumps([p.to_json() for p in productos], indent=2)
# Método 2: Usando el codificador personalizado
json_productos2 = json.dumps(productos, cls=CodificadorProducto, indent=2)
print("Método 1 (to_json):")
print(json_productos1)
print("\nMétodo 2 (codificador personalizado):")
print(json_productos2)
Deserializando a objetos personalizados
Para reconstruir objetos personalizados durante la deserialización:
def decodificador_producto(obj):
if "__tipo__" in obj and obj["__tipo__"] == "Producto":
return Producto(
id=obj["id"],
nombre=obj["nombre"],
precio=decimal.Decimal(str(obj["precio"])),
stock=obj["stock"]
)
return obj
# Deserializamos a objetos Producto
productos_reconstruidos = json.loads(json_productos2, object_hook=decodificador_producto)
# Verificamos que son instancias de Producto
for p in productos_reconstruidos:
print(f"{p} (tipo: {type(p).__name__})")
Caso práctico: Configuración con tipos personalizados
Un caso de uso común es almacenar configuraciones con tipos de datos especiales:
from pathlib import Path
import re
class ConfiguracionApp:
def __init__(self):
self.ruta_datos = Path("/var/data")
self.patrones_busqueda = [
re.compile(r"^\d{4}-\d{2}-\d{2}$"), # Formato fecha
re.compile(r"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$") # Email
]
self.fecha_actualizacion = datetime.now()
self.limites_api = {
"basico": 1000,
"premium": 10000
}
class CodificadorConfig(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, ConfiguracionApp):
# Convertimos el objeto completo a un diccionario
return {
"__tipo__": "ConfiguracionApp",
"ruta_datos": str(obj.ruta_datos),
"patrones_busqueda": [
{"patron": p.pattern} for p in obj.patrones_busqueda
],
"fecha_actualizacion": obj.fecha_actualizacion.isoformat(),
"limites_api": obj.limites_api
}
elif isinstance(obj, Path):
return {"__tipo__": "Path", "ruta": str(obj)}
elif isinstance(obj, re.Pattern):
return {"__tipo__": "Regex", "patron": obj.pattern}
elif isinstance(obj, datetime):
return {"__tipo__": "datetime", "valor": obj.isoformat()}
return super().default(obj)
def decodificador_config(obj):
if "__tipo__" not in obj:
return obj
if obj["__tipo__"] == "ConfiguracionApp":
config = ConfiguracionApp()
config.ruta_datos = Path(obj["ruta_datos"])
config.patrones_busqueda = [
re.compile(item["patron"]) for item in obj["patrones_busqueda"]
]
config.fecha_actualizacion = datetime.fromisoformat(obj["fecha_actualizacion"])
config.limites_api = obj["limites_api"]
return config
elif obj["__tipo__"] == "Path":
return Path(obj["ruta"])
elif obj["__tipo__"] == "Regex":
return re.compile(obj["patron"])
elif obj["__tipo__"] == "datetime":
return datetime.fromisoformat(obj["valor"])
return obj
# Creamos y guardamos la configuración
config = ConfiguracionApp()
json_config = json.dumps(config, cls=CodificadorConfig, indent=2)
with open("config_app.json", "w") as f:
f.write(json_config)
# Cargamos la configuración
with open("config_app.json", "r") as f:
config_cargada = json.loads(f.read(), object_hook=decodificador_config)
# Verificamos que se ha reconstruido correctamente
print(f"Ruta de datos: {config_cargada.ruta_datos} (tipo: {type(config_cargada.ruta_datos).__name__})")
print(f"Primer patrón: {config_cargada.patrones_busqueda[0].pattern}")
print(f"Fecha actualización: {config_cargada.fecha_actualizacion}")
Personalizando la serialización con dataclasses
Las dataclasses de Python (disponibles desde Python 3.7) facilitan la serialización:
from dataclasses import dataclass, asdict, field
from typing import List, Dict, Optional
@dataclass
class Usuario:
id: int
nombre: str
email: str
activo: bool = True
roles: List[str] = field(default_factory=list)
preferencias: Dict[str, str] = field(default_factory=dict)
ultima_conexion: Optional[datetime] = None
# Creamos un usuario
usuario = Usuario(
id=1,
nombre="Elena García",
email="elena@ejemplo.com",
roles=["editor", "revisor"],
preferencias={"tema": "oscuro", "notificaciones": "diarias"},
ultima_conexion=datetime.now()
)
# Convertimos a diccionario y luego a JSON
# (necesitamos un encoder personalizado para el campo datetime)
usuario_dict = asdict(usuario)
json_usuario = json.dumps(
usuario_dict,
default=lambda o: o.isoformat() if isinstance(o, datetime) else o,
indent=2
)
print(json_usuario)
La personalización de la codificación y decodificación JSON en Python te permite trabajar con cualquier tipo de dato, manteniendo la integridad de la información y facilitando la interoperabilidad entre sistemas. Estas técnicas son fundamentales para desarrollar aplicaciones robustas que necesitan persistir o transmitir datos complejos.
JSON y diccionarios Python
Los diccionarios en Python y los objetos JSON comparten una estructura similar basada en pares clave-valor, lo que crea una relación natural entre ambos. Esta similitud hace que la conversión entre diccionarios Python y JSON sea particularmente fluida, convirtiéndose en uno de los casos de uso más comunes del módulo json
.
Similitudes estructurales
Un diccionario Python se parece mucho a un objeto JSON:
# Diccionario Python
usuario_dict = {
"nombre": "Javier Pérez",
"edad": 34,
"activo": True,
"habilidades": ["Python", "SQL", "Docker"]
}
# Equivalente en JSON
"""
{
"nombre": "Javier Pérez",
"edad": 34,
"activo": true,
"habilidades": ["Python", "SQL", "Docker"]
}
"""
Esta similitud estructural facilita el trabajo con APIs, configuraciones y almacenamiento de datos estructurados.
Diferencias clave entre diccionarios y JSON
A pesar de sus similitudes, existen algunas diferencias importantes que debemos tener en cuenta:
- Tipos de claves: Los diccionarios Python pueden usar cualquier objeto inmutable como clave (strings, números, tuplas), mientras que JSON solo permite strings.
- Tipos de datos: JSON tiene un conjunto más limitado de tipos de datos que Python.
- Sintaxis: Los diccionarios Python usan
True
/False
/None
, mientras que JSON usatrue
/false
/null
.
import json
# Diccionario con claves no string
dict_python = {
"texto": "valor",
42: "número como clave",
(1, 2): "tupla como clave",
True: "booleano como clave"
}
# Esto generará un error
try:
json_str = json.dumps(dict_python)
except TypeError as e:
print(f"Error: {e}")
# Salida: Error: keys must be str, int, float, bool or None, not tuple
Conversión automática de tipos
El módulo json
realiza conversiones automáticas entre tipos de Python y JSON:
import json
# Mapeo de tipos Python → JSON
tipos_python = {
"string": "texto",
"int": 42,
"float": 3.14,
"bool_true": True,
"bool_false": False,
"none": None,
"lista": [1, 2, 3],
"dict": {"clave": "valor"}
}
json_str = json.dumps(tipos_python, indent=2)
print(f"Python → JSON:\n{json_str}")
# Mapeo de tipos JSON → Python
python_obj = json.loads(json_str)
print("\nJSON → Python:")
for clave, valor in python_obj.items():
print(f"{clave}: {valor} (tipo: {type(valor).__name__})")
Normalización de diccionarios para JSON
Para asegurar que un diccionario sea serializable a JSON, podemos crear una función de normalización:
def normalizar_para_json(diccionario):
"""Convierte un diccionario con claves no string a uno compatible con JSON."""
resultado = {}
for clave, valor in diccionario.items():
# Convertimos la clave a string
clave_str = str(clave)
# Procesamos el valor recursivamente si es necesario
if isinstance(valor, dict):
valor = normalizar_para_json(valor)
elif isinstance(valor, (list, tuple)):
valor = [
normalizar_para_json(item) if isinstance(item, dict) else item
for item in valor
]
resultado[clave_str] = valor
return resultado
# Diccionario con claves problemáticas
dict_complejo = {
"normal": "valor",
42: "número",
(1, 2): "tupla",
True: "booleano",
"anidado": {
5: "cinco",
"lista": [{"x": 1}, {(1, 2): "tupla en lista"}]
}
}
# Normalizamos y convertimos a JSON
dict_normalizado = normalizar_para_json(dict_complejo)
json_normalizado = json.dumps(dict_normalizado, indent=2)
print(json_normalizado)
Preservando tipos de claves
Si necesitamos preservar información sobre los tipos originales de las claves para una posterior reconstrucción:
def codificar_con_tipos(diccionario):
"""Codifica un diccionario preservando información de tipos de claves."""
resultado = {"__tipo__": "dict_tipado", "items": []}
for clave, valor in diccionario.items():
tipo_clave = type(clave).__name__
clave_str = str(clave)
# Procesamos el valor recursivamente si es necesario
if isinstance(valor, dict):
valor_procesado = codificar_con_tipos(valor)
else:
valor_procesado = valor
resultado["items"].append({
"key_type": tipo_clave,
"key": clave_str,
"value": valor_procesado
})
return resultado
# Decodificamos recuperando los tipos originales
def decodificar_con_tipos(obj):
if isinstance(obj, dict) and obj.get("__tipo__") == "dict_tipado":
resultado = {}
for item in obj["items"]:
tipo_clave = item["key_type"]
clave_str = item["key"]
valor = item["value"]
# Convertimos la clave al tipo original
if tipo_clave == "int":
clave = int(clave_str)
elif tipo_clave == "float":
clave = float(clave_str)
elif tipo_clave == "bool":
clave = clave_str.lower() == "true"
elif tipo_clave == "tuple":
# Asumimos que la tupla se guardó como string de su representación
clave = eval(clave_str)
else:
clave = clave_str
# Procesamos el valor recursivamente si es necesario
if isinstance(valor, dict) and valor.get("__tipo__") == "dict_tipado":
valor = decodificar_con_tipos(valor)
resultado[clave] = valor
return resultado
return obj
# Ejemplo de uso
dict_original = {
"texto": "valor",
42: "número",
(1, 2): "tupla",
True: "booleano"
}
# Codificamos preservando tipos
dict_codificado = codificar_con_tipos(dict_original)
json_tipado = json.dumps(dict_codificado, indent=2)
# Decodificamos recuperando tipos
dict_recuperado = decodificar_con_tipos(json.loads(json_tipado))
print("Diccionario recuperado:")
for k, v in dict_recuperado.items():
print(f"{k} ({type(k).__name__}): {v}")
Uso de diccionarios como configuración
Un caso de uso común es utilizar diccionarios para gestionar configuraciones de aplicaciones:
import json
import os
class ConfigManager:
def __init__(self, config_file="config.json"):
self.config_file = config_file
self.config = self._load_config()
def _load_config(self):
"""Carga la configuración desde un archivo JSON o usa valores predeterminados."""
default_config = {
"app": {
"name": "MiAplicación",
"version": "1.0.0",
"debug": False
},
"database": {
"host": "localhost",
"port": 5432,
"user": "usuario",
"password": "contraseña"
},
"features": {
"dark_mode": True,
"auto_save": True,
"save_interval": 300 # segundos
}
}
try:
if os.path.exists(self.config_file):
with open(self.config_file, "r", encoding="utf-8") as f:
user_config = json.load(f)
# Actualizamos recursivamente la configuración predeterminada
return self._deep_update(default_config, user_config)
else:
# Si no existe el archivo, lo creamos con la configuración predeterminada
self.save_config(default_config)
return default_config
except Exception as e:
print(f"Error al cargar la configuración: {e}")
return default_config
def _deep_update(self, original, update):
"""Actualiza recursivamente un diccionario con otro."""
for key, value in update.items():
if key in original and isinstance(original[key], dict) and isinstance(value, dict):
self._deep_update(original[key], value)
else:
original[key] = value
return original
def save_config(self, config=None):
"""Guarda la configuración en un archivo JSON."""
if config is None:
config = self.config
try:
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error al guardar la configuración: {e}")
return False
def get(self, section, key, default=None):
"""Obtiene un valor de configuración específico."""
try:
return self.config[section][key]
except KeyError:
return default
def set(self, section, key, value):
"""Establece un valor de configuración específico."""
if section not in self.config:
self.config[section] = {}
self.config[section][key] = value
return self.save_config()
# Ejemplo de uso
config = ConfigManager()
# Obtenemos valores
app_name = config.get("app", "name")
debug_mode = config.get("app", "debug")
print(f"Aplicación: {app_name}, Modo debug: {debug_mode}")
# Modificamos valores
config.set("app", "debug", True)
config.set("features", "auto_save", False)
# Añadimos una nueva sección
config.set("ui", "theme", "dark")
config.set("ui", "font_size", 14)
Diccionarios anidados y acceso por puntos
A veces es más conveniente acceder a diccionarios anidados usando notación de puntos en lugar de corchetes:
class DotDict:
"""Clase que permite acceder a diccionarios anidados usando notación de puntos."""
def __init__(self, data):
for key, value in data.items():
if isinstance(value, dict):
setattr(self, key, DotDict(value))
else:
setattr(self, key, value)
def to_dict(self):
"""Convierte de nuevo a un diccionario estándar."""
result = {}
for key, value in self.__dict__.items():
if isinstance(value, DotDict):
result[key] = value.to_dict()
else:
result[key] = value
return result
# Cargamos un JSON y lo convertimos a un objeto con acceso por puntos
with open("config.json", "r") as f:
config_data = json.load(f)
config = DotDict(config_data)
# Ahora podemos acceder así:
print(f"Nombre: {config.app.name}")
print(f"Host DB: {config.database.host}")
print(f"Modo oscuro: {config.features.dark_mode}")
# Y convertir de nuevo a diccionario para guardar
updated_dict = config.to_dict()
with open("config_updated.json", "w") as f:
json.dump(updated_dict, f, indent=2)
Caso práctico: Almacenamiento de preferencias de usuario
Un ejemplo práctico de uso de JSON y diccionarios es almacenar preferencias de usuario:
import json
import os
from datetime import datetime
class UserPreferences:
def __init__(self, user_id):
self.user_id = user_id
self.prefs_file = f"user_{user_id}_prefs.json"
self.preferences = self._load_preferences()
def _load_preferences(self):
"""Carga las preferencias del usuario desde un archivo JSON."""
default_prefs = {
"user_id": self.user_id,
"last_updated": datetime.now().isoformat(),
"ui": {
"theme": "light",
"font_size": 12,
"notifications": True
},
"content": {
"language": "es",
"items_per_page": 20,
"show_thumbnails": True
},
"privacy": {
"share_data": False,
"allow_cookies": True
},
"recent_searches": [],
"favorite_items": []
}
if not os.path.exists(self.prefs_file):
return default_prefs
try:
with open(self.prefs_file, "r", encoding="utf-8") as f:
user_prefs = json.load(f)
return user_prefs
except Exception as e:
print(f"Error al cargar preferencias: {e}")
return default_prefs
def save(self):
"""Guarda las preferencias actuales en el archivo JSON."""
self.preferences["last_updated"] = datetime.now().isoformat()
try:
with open(self.prefs_file, "w", encoding="utf-8") as f:
json.dump(self.preferences, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error al guardar preferencias: {e}")
return False
def update_ui(self, **kwargs):
"""Actualiza preferencias de interfaz de usuario."""
for key, value in kwargs.items():
if key in self.preferences["ui"]:
self.preferences["ui"][key] = value
return self.save()
def add_recent_search(self, search_term):
"""Añade un término de búsqueda a la lista de búsquedas recientes."""
# Evitamos duplicados y mantenemos solo las 10 búsquedas más recientes
if search_term in self.preferences["recent_searches"]:
self.preferences["recent_searches"].remove(search_term)
self.preferences["recent_searches"].insert(0, search_term)
self.preferences["recent_searches"] = self.preferences["recent_searches"][:10]
return self.save()
def add_favorite(self, item_id):
"""Añade un elemento a favoritos."""
if item_id not in self.preferences["favorite_items"]:
self.preferences["favorite_items"].append(item_id)
return self.save()
return True
def remove_favorite(self, item_id):
"""Elimina un elemento de favoritos."""
if item_id in self.preferences["favorite_items"]:
self.preferences["favorite_items"].remove(item_id)
return self.save()
return True
# Ejemplo de uso
user_prefs = UserPreferences(user_id=12345)
# Actualizamos preferencias de UI
user_prefs.update_ui(theme="dark", font_size=14)
# Añadimos búsquedas recientes
user_prefs.add_recent_search("python json tutorial")
user_prefs.add_recent_search("mejores prácticas python")
# Añadimos favoritos
user_prefs.add_favorite("article_789")
user_prefs.add_favorite("video_456")
print(f"Preferencias actualizadas: {user_prefs.preferences}")
Validación de esquemas de diccionarios
Cuando trabajamos con datos JSON que deben seguir una estructura específica, es útil validar que los diccionarios cumplan con el esquema esperado:
def validar_esquema(diccionario, esquema, ruta=""):
"""Valida que un diccionario cumpla con un esquema definido."""
errores = []
# Verificamos campos requeridos
for campo, config in esquema.items():
campo_ruta = f"{ruta}.{campo}" if ruta else campo
# Verificamos si el campo existe
if campo not in diccionario:
if config.get("requerido", False):
errores.append(f"Campo requerido '{campo_ruta}' no encontrado")
continue
valor = diccionario[campo]
tipo_esperado = config.get("tipo")
# Verificamos el tipo
if tipo_esperado and not isinstance(valor, tipo_esperado):
errores.append(
f"Campo '{campo_ruta}' debe ser de tipo {tipo_esperado.__name__}, "
f"pero es {type(valor).__name__}"
)
# Verificamos esquema anidado
if "esquema" in config and isinstance(valor, dict):
errores.extend(validar_esquema(valor, config["esquema"], campo_ruta))
# Verificamos lista de objetos
if "items_esquema" in config and isinstance(valor, list):
for i, item in enumerate(valor):
if isinstance(item, dict):
errores.extend(validar_esquema(
item, config["items_esquema"], f"{campo_ruta}[{i}]"
))
return errores
# Definimos un esquema para validar
esquema_usuario = {
"id": {"tipo": int, "requerido": True},
"nombre": {"tipo": str, "requerido": True},
"email": {"tipo": str, "requerido": True},
"edad": {"tipo": int},
"direccion": {
"tipo": dict,
"esquema": {
"calle": {"tipo": str, "requerido": True},
"ciudad": {"tipo": str, "requerido": True},
"codigo_postal": {"tipo": str}
}
},
"telefonos": {
"tipo": list,
"items_esquema": {
"tipo": {"tipo": str, "requerido": True},
"numero": {"tipo": str, "requerido": True}
}
}
}
# Datos a validar
datos_usuario = {
"id": 1001,
"nombre": "Roberto Gómez",
"email": "roberto@ejemplo.com",
"direccion": {
"calle": "Av. Principal 123",
"ciudad": "Barcelona"
},
"telefonos": [
{"tipo": "casa", "numero": "555-1234"},
{"tipo": "movil", "numero": "666-5678"}
]
}
# Validamos
errores = validar_esquema(datos_usuario, esquema_usuario)
if errores:
print("Errores de validación:")
for error in errores:
print(f"- {error}")
else:
print("Los datos son válidos según el esquema")
La estrecha relación entre diccionarios Python y JSON facilita enormemente el trabajo con datos estructurados, configuraciones y APIs. Aprovechando esta sinergia natural, podemos crear aplicaciones robustas que manejen datos complejos de manera eficiente y mantenible.
Otras lecciones de Python
Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Python
Introducción
Instalación Y Creación De Proyecto
Introducción
Tema 2: Tipos De Datos, Variables Y Operadores
Introducción
Instalación De Python
Introducción
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Estructuras Control Iterativo
Sintaxis
Estructuras Control Condicional
Sintaxis
Testing Con Pytest
Sintaxis
Listas
Estructuras De Datos
Tuplas
Estructuras De Datos
Diccionarios
Estructuras De Datos
Conjuntos
Estructuras De Datos
Comprehensions
Estructuras De Datos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Mixins Y Herencia Múltiple
Programación Orientada A Objetos
Métodos Especiales (Dunder Methods)
Programación Orientada A Objetos
Composición De Clases
Programación Orientada A Objetos
Funciones Lambda
Programación Funcional
Aplicación Parcial
Programación Funcional
Entrada Y Salida, Manejo De Archivos
Programación Funcional
Decoradores
Programación Funcional
Generadores
Programación Funcional
Paradigma Funcional
Programación Funcional
Composición De Funciones
Programación Funcional
Funciones Orden Superior Map Y Filter
Programación Funcional
Funciones Auxiliares
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Archivos Comprimidos
Entrada Y Salida Io
Entrada Y Salida Avanzada
Entrada Y Salida Io
Archivos Temporales
Entrada Y Salida Io
Contexto With
Entrada Y Salida Io
Módulo Csv
Biblioteca Estándar
Módulo Json
Biblioteca Estándar
Módulo Datetime
Biblioteca Estándar
Módulo Math
Biblioteca Estándar
Módulo Os
Biblioteca Estándar
Módulo Re
Biblioteca Estándar
Módulo Random
Biblioteca Estándar
Módulo Time
Biblioteca Estándar
Módulo Collections
Biblioteca Estándar
Módulo Sys
Biblioteca Estándar
Módulo Statistics
Biblioteca Estándar
Módulo Pickle
Biblioteca Estándar
Módulo Pathlib
Biblioteca Estándar
Importar Módulos Y Paquetes
Paquetes Y Módulos
Crear Módulos Y Paquetes
Paquetes Y Módulos
Entornos Virtuales (Virtualenv, Venv)
Entorno Y Dependencias
Gestión De Dependencias (Pip, Requirements.txt)
Entorno Y Dependencias
Python-dotenv Y Variables De Entorno
Entorno Y Dependencias
Acceso A Datos Con Mysql, Pymongo Y Pandas
Acceso A Bases De Datos
Acceso A Mongodb Con Pymongo
Acceso A Bases De Datos
Acceso A Mysql Con Mysql Connector
Acceso A Bases De Datos
Novedades Python 3.13
Características Modernas
Operador Walrus
Características Modernas
Pattern Matching
Características Modernas
Instalación Beautiful Soup
Web Scraping
Sintaxis General De Beautiful Soup
Web Scraping
Tipos De Selectores
Web Scraping
Web Scraping De Html
Web Scraping
Web Scraping Para Ciencia De Datos
Web Scraping
Autenticación Y Acceso A Recursos Protegidos
Web Scraping
Combinación De Selenium Con Beautiful Soup
Web Scraping
Ejercicios de programación de Python
Evalúa tus conocimientos de esta lección Módulo json con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Módulo math
Reto herencia
Excepciones
Introducción a Python
Reto variables
Funciones Python
Reto funciones
Módulo datetime
Reto acumulación
Reto estructuras condicionales
Polimorfismo
Módulo os
Reto métodos dunder
Diccionarios
Reto clases y objetos
Reto operadores
Operadores
Estructuras de control
Funciones lambda
Reto diccionarios
Reto función lambda
Encapsulación
Reto coleciones
Reto funciones auxiliares
Crear módulos y paquetes
Módulo datetime
Excepciones
Operadores
Diccionarios
Reto map, filter
Reto tuplas
Proyecto gestor de tareas CRUD
Tuplas
Variables
Tipos de datos
Conjuntos
Reto mixins
Módulo csv
Módulo json
Herencia
Análisis de datos de ventas con Pandas
Reto fechas y tiempo
Reto estructuras de iteración
Funciones
Reto comprehensions
Variables
Reto serialización
Módulo csv
Reto polimorfismo
Polimorfismo
Clases y objetos
Reto encapsulación
Estructuras de control
Importar módulos y paquetes
Módulo math
Funciones lambda
Reto excepciones
Listas
Reto archivos
Encapsulación
Reto conjuntos
Clases y objetos
Instalación de Python y creación de proyecto
Reto listas
Tipos de datos
Crear módulos y paquetes
Tuplas
Herencia
Reto acceso a sistema
Proyecto sintaxis calculadora
Importar módulos y paquetes
Clases y objetos
Módulo os
Listas
Conjuntos
Reto tipos de datos
Reto matemáticas
Módulo json
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la serialización y deserialización de datos con el módulo json.
- Navegar, modificar y transformar estructuras JSON anidadas en Python.
- Personalizar la codificación y decodificación de tipos de datos no compatibles con JSON.
- Entender la relación entre diccionarios Python y objetos JSON, incluyendo diferencias y conversiones.
- Aplicar técnicas para validar, almacenar y gestionar configuraciones y preferencias usando JSON y diccionarios.