Estructuras JSON Avanzadas: Patrones y Técnicas Profesionales
Domina patrones avanzados de JSON: normalización, grafos, versionado, polimorfismo y arquitecturas escalables.
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# Estructuras JSON Avanzadas: Patrones y Técnicas Profesionales
Más allá de objetos y arrays simples, JSON puede modelar estructuras complejas. Esta guía avanzada explora patrones, técnicas y arquitecturas para JSON profesional.
Normalización de Datos
Problema: Datos Duplicados
{
"posts": [
{
"id": 1,
"titulo": "Intro a JSON",
"autor": {
"id": 101,
"nombre": "Ana García",
"email": "ana@ejemplo.com"
},
"comentarios": [
{
"id": 1,
"texto": "Excelente post",
"autor": {
"id": 102,
"nombre": "Carlos López",
"email": "carlos@ejemplo.com"
}
}
]
},
{
"id": 2,
"titulo": "JSON Avanzado",
"autor": {
"id": 101,
"nombre": "Ana García",
"email": "ana@ejemplo.com"
}
}
]
}
Problemas:
- Duplicación de datos del autor
- Difícil mantener consistencia
- Mayor tamaño de archivo
Solución: Normalización
{
"entidades": {
"posts": {
"1": {
"id": 1,
"titulo": "Intro a JSON",
"autorId": 101,
"comentarioIds": [1]
},
"2": {
"id": 2,
"titulo": "JSON Avanzado",
"autorId": 101,
"comentarioIds": []
}
},
"usuarios": {
"101": {
"id": 101,
"nombre": "Ana García",
"email": "ana@ejemplo.com"
},
"102": {
"id": 102,
"nombre": "Carlos López",
"email": "carlos@ejemplo.com"
}
},
"comentarios": {
"1": {
"id": 1,
"texto": "Excelente post",
"autorId": 102,
"postId": 1
}
}
},
"resultado": [1, 2]
}
Ventajas:
- ✅ Datos únicos sin duplicación
- ✅ Actualizaciones fáciles
- ✅ Menor tamaño
- ✅ Acceso O(1) por ID
Normalizar con Código
function normalizar(data, schema) {
const entidades = {};
const resultado = [];
function procesarItem(item, tipo) {
if (!entidades[tipo]) {
entidades[tipo] = {};
}
const id = item.id;
const itemNormalizado = { ...item };
// Normalizar relaciones
Object.keys(schema[tipo] || {}).forEach(relacion => {
const config = schema[tipo][relacion];
if (Array.isArray(itemNormalizado[relacion])) {
itemNormalizado[${relacion}Ids] = itemNormalizado[relacion].map(
subItem => {
procesarItem(subItem, config.tipo);
return subItem.id;
}
);
delete itemNormalizado[relacion];
} else if (itemNormalizado[relacion]) {
procesarItem(itemNormalizado[relacion], config.tipo);
itemNormalizado[${relacion}Id] = itemNormalizado[relacion].id;
delete itemNormalizado[relacion];
}
});
entidades[tipo][id] = itemNormalizado;
return id;
}
data.forEach(item => {
const id = procesarItem(item, 'posts');
resultado.push(id);
});
return { entidades, resultado };
}
// Schema
const schema = {
posts: {
autor: { tipo: 'usuarios' },
comentarios: { tipo: 'comentarios' }
},
comentarios: {
autor: { tipo: 'usuarios' }
}
};
// Uso
const datosNormalizados = normalizar(posts, schema);
Desnormalizar para Uso
function desnormalizar(entidades, resultado, tipo) {
return resultado.map(id => {
const item = { ...entidades[tipo][id] };
// Reconstruir relaciones
Object.keys(item).forEach(key => {
if (key.endsWith('Id')) {
const relacion = key.slice(0, -2);
const tipoRelacion = inferirTipo(relacion);
const relacionId = item[key];
item[relacion] = entidades[tipoRelacion][relacionId];
delete item[key];
} else if (key.endsWith('Ids')) {
const relacion = key.slice(0, -3);
const tipoRelacion = inferirTipo(relacion);
item[relacion] = item[key].map(
relacionId => entidades[tipoRelacion][relacionId]
);
delete item[key];
}
});
return item;
});
}
Grafos en JSON
Representación de Árbol
{
"id": "raiz",
"nombre": "Sistema de Archivos",
"tipo": "directorio",
"hijos": [
{
"id": "docs",
"nombre": "Documentos",
"tipo": "directorio",
"hijos": [
{
"id": "readme",
"nombre": "README.md",
"tipo": "archivo",
"tamano": 1024
}
]
},
{
"id": "src",
"nombre": "src",
"tipo": "directorio",
"hijos": []
}
]
}
Grafo con Adyacencias
{
"nodos": [
{ "id": "A", "nombre": "Madrid", "tipo": "ciudad" },
{ "id": "B", "nombre": "Barcelona", "tipo": "ciudad" },
{ "id": "C", "nombre": "Valencia", "tipo": "ciudad" }
],
"aristas": [
{ "origen": "A", "destino": "B", "distancia": 621, "tipo": "carretera" },
{ "origen": "B", "destino": "C", "distancia": 349, "tipo": "carretera" },
{ "origen": "A", "destino": "C", "distancia": 352, "tipo": "carretera" }
]
}
Lista de Adyacencia
{
"grafo": {
"A": {
"nombre": "Madrid",
"conexiones": [
{ "nodo": "B", "peso": 621 },
{ "nodo": "C", "peso": 352 }
]
},
"B": {
"nombre": "Barcelona",
"conexiones": [
{ "nodo": "A", "peso": 621 },
{ "nodo": "C", "peso": 349 }
]
},
"C": {
"nombre": "Valencia",
"conexiones": [
{ "nodo": "B", "peso": 349 },
{ "nodo": "A", "peso": 352 }
]
}
}
}
Algoritmo de Búsqueda en Grafo
class Grafo {
constructor(datos) {
this.nodos = new Map();
// Construir grafo
datos.nodos.forEach(nodo => {
this.nodos.set(nodo.id, { ...nodo, conexiones: [] });
});
datos.aristas.forEach(arista => {
this.nodos.get(arista.origen).conexiones.push({
destino: arista.destino,
peso: arista.distancia
});
});
}
// Búsqueda en amplitud (BFS)
bfs(inicio, objetivo) {
const cola = [inicio];
const visitados = new Set([inicio]);
const padres = new Map();
while (cola.length > 0) {
const actual = cola.shift();
if (actual === objetivo) {
return this.reconstruirCamino(padres, inicio, objetivo);
}
const nodo = this.nodos.get(actual);
nodo.conexiones.forEach(({ destino }) => {
if (!visitados.has(destino)) {
visitados.add(destino);
padres.set(destino, actual);
cola.push(destino);
}
});
}
return null;
}
reconstruirCamino(padres, inicio, objetivo) {
const camino = [objetivo];
let actual = objetivo;
while (actual !== inicio) {
actual = padres.get(actual);
camino.unshift(actual);
}
return camino;
}
}
// Uso
const grafo = new Grafo(datosGrafo);
const camino = grafo.bfs('A', 'C');
console.log(camino); // ['A', 'C']
Polimorfismo en JSON
Discriminador de Tipo
{
"figuras": [
{
"tipo": "circulo",
"radio": 5,
"centro": { "x": 0, "y": 0 }
},
{
"tipo": "rectangulo",
"ancho": 10,
"alto": 5,
"esquina": { "x": 0, "y": 0 }
},
{
"tipo": "triangulo",
"vertices": [
{ "x": 0, "y": 0 },
{ "x": 5, "y": 0 },
{ "x": 2.5, "y": 5 }
]
}
]
}
Procesamiento Polimórfico
class Figura {
static crear(datos) {
switch (datos.tipo) {
case 'circulo':
return new Circulo(datos);
case 'rectangulo':
return new Rectangulo(datos);
case 'triangulo':
return new Triangulo(datos);
default:
throw new Error(Tipo desconocido: ${datos.tipo});
}
}
}
class Circulo {
constructor({ radio, centro }) {
this.radio = radio;
this.centro = centro;
}
area() {
return Math.PI this.radio 2;
}
}
class Rectangulo {
constructor({ ancho, alto }) {
this.ancho = ancho;
this.alto = alto;
}
area() {
return this.ancho this.alto;
}
}
// Procesar
const figuras = datosJSON.figuras.map(f => Figura.crear(f));
const areas = figuras.map(f => f.area());
Versionado de Datos
Estrategia 1: Campo de Versión
{
"version": "2.0",
"datos": {
"usuario": {
"id": "123",
"email": "usuario@ejemplo.com",
"perfil": {
"nombre": "Ana García",
"preferencias": {
"idioma": "es",
"tema": "oscuro"
}
}
}
}
}
Migrar Versiones
const migraciones = {
'1.0': (datos) => {
// V1.0 → V1.1: Agregar campo email
return {
...datos,
version: '1.1',
email: datos.contacto.email
};
},
'1.1': (datos) => {
// V1.1 → V2.0: Reestructurar perfil
return {
version: '2.0',
datos: {
usuario: {
id: datos.id,
email: datos.email,
perfil: {
nombre: datos.nombre,
preferencias: datos.configuracion
}
}
}
};
}
};
function migrar(datos, versionObjetivo) {
let version = datos.version || '1.0';
let datosMigrados = { ...datos };
while (version !== versionObjetivo) {
if (!migraciones[version]) {
throw new Error(No hay migración para versión ${version});
}
datosMigrados = migracionesversion;
version = datosMigrados.version;
}
return datosMigrados;
}
Estrategia 2: Versionado en URL
/api/v1/usuarios → Versión 1
/api/v2/usuarios → Versión 2
Paginación Eficiente
Cursor-Based Pagination
{
"datos": [
{ "id": "user_101", "nombre": "Ana" },
{ "id": "user_102", "nombre": "Carlos" },
{ "id": "user_103", "nombre": "María" }
],
"paginacion": {
"siguiente": "user_103",
"anterior": null,
"tieneSiguiente": true,
"tieneAnterior": false
}
}
Implementación:
async function obtenerUsuarios(cursor = null, limite = 20) {
let query = db.usuarios
.orderBy('id')
.limit(limite + 1);
if (cursor) {
query = query.startAfter(cursor);
}
const resultados = await query.get();
const tieneSiguiente = resultados.length > limite;
const datos = resultados.slice(0, limite);
const ultimoId = datos[datos.length - 1]?.id;
return {
datos,
paginacion: {
siguiente: tieneSiguiente ? ultimoId : null,
anterior: cursor,
tieneSiguiente,
tieneAnterior: !!cursor
}
};
}
Offset-Based Pagination
{
"datos": [...],
"paginacion": {
"pagina": 2,
"porPagina": 20,
"total": 1534,
"totalPaginas": 77
}
}
HAL (Hypertext Application Language)
Estructura HAL
{
"_links": {
"self": { "href": "/api/usuarios/123" },
"posts": { "href": "/api/usuarios/123/posts" },
"amigos": { "href": "/api/usuarios/123/amigos" }
},
"id": "123",
"nombre": "Ana García",
"email": "ana@ejemplo.com",
"_embedded": {
"posts": [
{
"_links": {
"self": { "href": "/api/posts/1" }
},
"id": "1",
"titulo": "Mi primer post"
}
]
}
}
Ventajas:
- ✅ Auto-documentación
- ✅ Descubrimiento de API
- ✅ Navegación hipermedia
JSON-LD (Linked Data)
Contexto Semántico
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Ana García",
"email": "ana@ejemplo.com",
"address": {
"@type": "PostalAddress",
"streetAddress": "Calle Mayor 123",
"addressLocality": "Madrid",
"addressCountry": "ES"
},
"worksFor": {
"@type": "Organization",
"name": "TechCorp",
"url": "https://techcorp.ejemplo.com"
}
}
Uso:
- SEO mejorado
- Web semántica
- Interoperabilidad de datos
Event Sourcing con JSON
Estructura de Eventos
{
"eventos": [
{
"id": "evt_001",
"tipo": "UsuarioCreado",
"timestamp": "2024-01-15T10:00:00Z",
"agregadoId": "user_123",
"datos": {
"nombre": "Ana García",
"email": "ana@ejemplo.com"
}
},
{
"id": "evt_002",
"tipo": "EmailActualizado",
"timestamp": "2024-01-16T14:30:00Z",
"agregadoId": "user_123",
"datos": {
"emailAnterior": "ana@ejemplo.com",
"emailNuevo": "ana.garcia@ejemplo.com"
}
},
{
"id": "evt_003",
"tipo": "UsuarioDesactivado",
"timestamp": "2024-01-20T09:00:00Z",
"agregadoId": "user_123",
"datos": {
"razon": "Solicitud del usuario"
}
}
]
}
Reconstruir Estado
class Usuario {
constructor() {
this.id = null;
this.nombre = null;
this.email = null;
this.activo = false;
}
aplicarEvento(evento) {
switch (evento.tipo) {
case 'UsuarioCreado':
this.id = evento.agregadoId;
this.nombre = evento.datos.nombre;
this.email = evento.datos.email;
this.activo = true;
break;
case 'EmailActualizado':
this.email = evento.datos.emailNuevo;
break;
case 'UsuarioDesactivado':
this.activo = false;
break;
}
}
static reconstruirDesdeEventos(eventos) {
const usuario = new Usuario();
eventos.forEach(evento => usuario.aplicarEvento(evento));
return usuario;
}
}
// Uso
const usuario = Usuario.reconstruirDesdeEventos(
eventos.filter(e => e.agregadoId === 'user_123')
);
CQRS con JSON
Comandos
{
"comando": "CrearPedido",
"id": "cmd_001",
"timestamp": "2024-01-15T10:00:00Z",
"datos": {
"clienteId": "user_123",
"items": [
{ "productoId": "prod_456", "cantidad": 2 },
{ "productoId": "prod_789", "cantidad": 1 }
],
"direccionEnvio": {
"calle": "Calle Mayor 123",
"ciudad": "Madrid"
}
}
}
Consultas (Proyecciones)
{
"pedidosCliente": {
"clienteId": "user_123",
"pedidos": [
{
"id": "order_001",
"fecha": "2024-01-15",
"total": 129.99,
"estado": "enviado",
"itemCount": 3
},
{
"id": "order_002",
"fecha": "2024-02-01",
"total": 49.99,
"estado": "procesando",
"itemCount": 1
}
],
"totalGastado": 179.98,
"pedidosActivos": 1
}
}
Compresión y Optimización
Delta Compression
{
"base": {
"id": "doc_123",
"titulo": "Documento Original",
"contenido": "Lorem ipsum...",
"version": 1
},
"deltas": [
{
"version": 2,
"cambios": [
{ "op": "replace", "path": "/titulo", "value": "Documento Actualizado" }
]
},
{
"version": 3,
"cambios": [
{ "op": "add", "path": "/tags", "value": ["importante"] }
]
}
]
}
JSON Patch (RFC 6902)
[
{ "op": "add", "path": "/nuevo", "value": "valor" },
{ "op": "remove", "path": "/antiguo" },
{ "op": "replace", "path": "/existente", "value": "nuevoValor" },
{ "op": "move", "from": "/origen", "path": "/destino" },
{ "op": "copy", "from": "/fuente", "path": "/copia" },
{ "op": "test", "path": "/campo", "value": "valorEsperado" }
]
Mejores Prácticas
1. Diseño Consistente
{
"meta": {
"version": "1.0",
"timestamp": "2024-01-15T10:00:00Z",
"requestId": "req_123"
},
"data": {
// Datos de respuesta
},
"errors": [],
"links": {
"self": "/api/recurso",
"next": "/api/recurso?page=2"
}
}
2. Validación con JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"usuario": {
"type": "object",
"properties": {
"id": { "type": "string", "pattern": "^user_[0-9]+$" },
"email": { "type": "string", "format": "email" }
},
"required": ["id", "email"]
}
}
}
3. Documentación
Usa OpenAPI/Swagger para documentar estructuras JSON de APIs.
Conclusión
Las estructuras JSON avanzadas permiten modelar sistemas complejos eficientemente. Aplica estos patrones según las necesidades de tu aplicación.
Puntos Clave
- Normaliza datos para evitar duplicación
- Usa discriminadores para polimorfismo
- Implementa versionado desde el principio
- Considera HAL o JSON-LD para APIs RESTful
- Event Sourcing para auditoría completa
- Valida con JSON Schema
¡Domina JSON avanzado para arquitecturas escalables!
Artículos Relacionados
Entendiendo JSON Schema: Validación y Documentación de Datos
Aprende a usar JSON Schema para validar y documentar tus estructuras JSON. Guía completa con ejemplos prácticos en JavaScript y Python.
JSON en Ciencia de Datos: Pandas, NumPy y Análisis de Datos
Aprende a usar JSON en ciencia de datos con Python. Incluye Pandas, NumPy, visualización y casos de uso prácticos.
Trabajar con Archivos JSON Grandes: Técnicas de Streaming y Optimización
Aprende a manejar archivos JSON de varios GB con streaming, procesamiento incremental y optimización de memoria.