Python y JSON forman una pareja natural. Ya sea que estés construyendo una API REST con FastAPI o Django, procesando pipelines de datos, o simplemente leyendo un archivo de configuración, trabajarás con JSON constantemente. La buena noticia: la biblioteca estándar de Python tiene todo lo que necesitas en el módulo json. Sin pip install requerido.

Las cuatro funciones que realmente usas

El módulo json te da cuatro funciones para el trabajo diario:

  • json.loads(str) — analiza una cadena JSON en un objeto Python
  • json.dumps(obj) — convierte un objeto Python en una cadena JSON
  • json.load(file) — analiza JSON directamente desde un objeto archivo
  • json.dump(obj, file) — escribe un objeto Python como JSON en un archivo

La s en loads / dumps significa string (cadena). Las que no tienen la s trabajan con objetos de archivo. Fácil de recordar una vez que conoces la regla.

json.loads() — Analizar una cadena JSON

python
import json

json_string = '{"name": "Alice", "age": 30, "active": true, "score": 98.5}'

user = json.loads(json_string)

print(user["name"])    # Alice
print(user["age"])     # 30
print(user["active"])  # True
print(type(user))      # <class 'dict'>

Observa el mapeo de tipos: JSON true se convierte en Python True, JSON false se convierte en Python False, JSON null se convierte en Python None. Los objetos JSON se convierten en dict de Python, los arrays JSON se convierten en list de Python.

json.dumps() — Serializar a una cadena JSON

python
import json

user = {
    "name": "Bob",
    "age": 25,
    "roles": ["admin", "editor"],
    "active": True,
    "extra": None
}

# Compacto (ideal para transmisión de red)
compact = json.dumps(user)
print(compact)
# {"name": "Bob", "age": 25, "roles": ["admin", "editor"], "active": true, "extra": null}

# Con formato (ideal para logs e inspección humana)
pretty = json.dumps(user, indent=2)
print(pretty)
# {
#   "name": "Bob",
#   "age": 25,
#   "roles": [
#     "admin",
#     "editor"
#   ],
#   "active": true,
#   "extra": null
# }

Observa el mapeo de tipos inverso: Python True → JSON true, Python None → JSON null. Python lo maneja automáticamente.

Leer JSON desde un archivo

Este es probablemente el caso de uso más común — leer un archivo de configuración o datos al arrancar:

python
import json

# Leer y analizar en un solo paso
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

print(config["database"]["host"])  # localhost
print(config["database"]["port"])  # 5432

Siempre especifica encoding="utf-8" al abrir archivos JSON. JSON está especificado como UTF-8 por la RFC 8259, y omitirlo puede causar problemas en Windows donde la codificación predeterminada es a veces cp1252.

Escribir JSON en un archivo

python
import json

results = {
    "timestamp": "2024-01-15T09:30:00Z",
    "total": 1523,
    "processed": 1521,
    "failed": 2,
    "errors": [
        {"id": 42, "reason": "missing field"},
        {"id": 99, "reason": "invalid format"}
    ]
}

with open("results.json", "w", encoding="utf-8") as f:
    json.dump(results, f, indent=2)

print("Resultados guardados en results.json")

Manejar errores correctamente

json.loads() lanza json.JSONDecodeError (una subclase de ValueError) cuando la entrada no es JSON válido. Manéjala siempre al analizar datos que no controlas:

python
import json

def safe_parse(json_str):
    try:
        return json.loads(json_str)
    except json.JSONDecodeError as e:
        print(f"JSON inválido en línea {e.lineno}, columna {e.colno}: {e.msg}")
        return None

data = safe_parse('{"name": "Alice"}')   # funciona bien
bad  = safe_parse('not json at all')     # imprime el error, devuelve None
also_bad = safe_parse('{"key": }')       # imprime el error con información de posición

JSONDecodeError te da la línea y columna exactas donde falló el análisis, lo cual es útil para depurar archivos JSON grandes.

Opciones útiles de dumps()

python
import json

data = {
    "z_key": 1,
    "a_key": 2,
    "price": 9.999999999
}

# Ordenar claves alfabéticamente (genial para salida reproducible / diffs)
print(json.dumps(data, sort_keys=True, indent=2))
# {
#   "a_key": 2,
#   "price": 9.999999999,
#   "z_key": 1
# }

# Asegurar que los caracteres no-ASCII se preserven (por defecto: escapados a \uXXXX)
data2 = {"city": "Münich", "greeting": "こんにちは"}
print(json.dumps(data2, ensure_ascii=False))
# {"city": "Münich", "greeting": "こんにちは"}

# Con ensure_ascii=True (por defecto):
print(json.dumps(data2))
# {"city": "M\u00fcnich", "greeting": "\u3053\u3093\u306b\u3061\u306f"}

ensure_ascii=False es algo que siempre añado al escribir archivos JSON que contienen texto no-ASCII. La versión escapada es JSON técnicamente válido pero mucho más difícil de leer en un editor de texto.

Serializar objetos personalizados

Por defecto, json.dumps() no puede serializar instancias de clases personalizadas ni objetos datetime. Tienes dos opciones: crear una subclase de json.JSONEncoder, o convertir a un dict primero:

python
import json
from datetime import datetime, date

# Opción 1: clase de codificador personalizada
class AppEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        return super().default(obj)

data = {"name": "Alice", "created_at": datetime(2024, 1, 15, 9, 30)}
print(json.dumps(data, cls=AppEncoder, indent=2))
# {
#   "name": "Alice",
#   "created_at": "2024-01-15T09:30:00"
# }

# Opción 2: parámetro default= (más simple para conversiones puntuales)
print(json.dumps(data, default=str, indent=2))  # convierte cualquier desconocido a str

Un patrón práctico: Carga de archivo de configuración

Aquí hay un patrón del mundo real que uso en casi todos mis proyectos Python — un cargador de configuración que lee un archivo de configuración JSON con valores predeterminados razonables:

python
import json
import os
from pathlib import Path

DEFAULTS = {
    "database": {"host": "localhost", "port": 5432},
    "debug": False,
    "log_level": "INFO"
}

def load_config(path="config.json"):
    config = DEFAULTS.copy()

    config_path = Path(path)
    if config_path.exists():
        with open(config_path, "r", encoding="utf-8") as f:
            try:
                user_config = json.load(f)
                # Mezcla profunda: la configuración del usuario sobreescribe los valores por defecto
                for key, value in user_config.items():
                    if isinstance(value, dict) and key in config:
                        config[key].update(value)
                    else:
                        config[key] = value
            except json.JSONDecodeError as e:
                print(f"Advertencia: config.json es inválido ({e.msg}), usando valores por defecto")

    return config

config = load_config()
print(config["database"]["host"])  # localhost (o valor sobreescrito)

Resumen

El módulo json de Python cubre todo lo que necesitas sin ninguna dependencia. Las reglas clave: usa loads()/dumps() para cadenas, load()/dump() para archivos, maneja siempre JSONDecodeError al analizar datos externos, y añade ensure_ascii=False cuando tus datos contienen caracteres no-latinos. Para depurar datos JSON, el Formateador JSON y el Validador JSON pueden ahorrarte mucho tiempo.