JSON_OBJECTAGG
La función JSON_OBJECTAGG es una función de agregación que construye un objeto JSON a partir de pares clave-valor extraídos de las filas de un resultado. Mientras que JSON_ARRAYAGG reúne valores en un array, JSON_OBJECTAGG los organiza en un objeto con claves y valores, lo que resulta ideal para pivotar datos o crear estructuras de configuración directamente desde SQL.
Sintaxis
JSON_OBJECTAGG(clave, valor)La función recibe exactamente dos argumentos. El primero se convierte en la clave del objeto JSON y el segundo en su valor asociado. La clave debe ser una expresión que produzca una cadena de texto, ya que las claves de un objeto JSON siempre son strings. El valor puede ser de cualquier tipo que MySQL sepa convertir a JSON: números, cadenas, booleanos, NULL u otros documentos JSON.
JSON_OBJECTAGG se utiliza como cualquier función de agregación, normalmente acompañada de GROUP BY para crear un objeto por cada grupo de filas. Si se usa sin GROUP BY, genera un único objeto con todas las filas del resultado.
Comportamiento básico
Para entender cómo funciona, imagina una tabla configuraciones que almacena pares clave-valor para una aplicación:
CREATE TABLE configuraciones (
id INT AUTO_INCREMENT PRIMARY KEY,
seccion VARCHAR(50),
clave VARCHAR(100),
valor VARCHAR(255)
);
INSERT INTO configuraciones (seccion, clave, valor) VALUES
('correo', 'smtp_host', 'smtp.empresa.com'),
('correo', 'smtp_puerto', '587'),
('correo', 'smtp_seguridad', 'TLS'),
('app', 'nombre', 'TiendaOnline'),
('app', 'version', '3.2.1'),
('app', 'modo_debug', 'false');Si quieres convertir toda la tabla en un solo objeto JSON plano:
SELECT JSON_OBJECTAGG(clave, valor) AS config
FROM configuraciones;| config |
|---|
| {"version": "3.2.1", "nombre": "TiendaOnline", "smtp_host": "smtp.empresa.com", "modo_debug": "false", "smtp_puerto": "587", "smtp_seguridad": "TLS"} |
MySQL toma cada fila y usa la columna clave como clave del objeto y la columna valor como su valor. El resultado es un objeto JSON que contiene todos los pares.
Cuando combinas JSON_OBJECTAGG con GROUP BY, obtienes un objeto por cada grupo:
SELECT
seccion,
JSON_OBJECTAGG(clave, valor) AS parametros
FROM configuraciones
GROUP BY seccion;| seccion | parametros |
|---|---|
| correo | {"smtp_host": "smtp.empresa.com", "smtp_puerto": "587", "smtp_seguridad": "TLS"} |
| app | {"nombre": "TiendaOnline", "version": "3.2.1", "modo_debug": "false"} |
Ahora cada sección tiene su propio objeto JSON con sus configuraciones específicas. Este patrón es extremadamente útil para transformar datos normalizados en estructuras JSON compactas.
Caso práctico: catálogo de atributos de producto
En una tienda online, los productos suelen tener atributos variables. Una camiseta tiene talla y color, mientras que un portátil tiene RAM y procesador. Una tabla de atributos normalizada puede convertirse fácilmente en objetos JSON por producto:
CREATE TABLE productos_atributos (
producto_id INT,
producto_nombre VARCHAR(100),
atributo VARCHAR(50),
valor VARCHAR(100)
);
INSERT INTO productos_atributos VALUES
(1, 'iPhone 15 Pro', 'color', 'Titanio Natural'),
(1, 'iPhone 15 Pro', 'almacenamiento', '256GB'),
(1, 'iPhone 15 Pro', 'chip', 'A17 Pro'),
(2, 'MacBook Air M3', 'color', 'Medianoche'),
(2, 'MacBook Air M3', 'ram', '16GB'),
(2, 'MacBook Air M3', 'pantalla', '15 pulgadas'),
(3, 'Camiseta algodón', 'talla', 'M'),
(3, 'Camiseta algodón', 'color', 'Azul marino'),
(3, 'Camiseta algodón', 'material', '100% algodón');SELECT
producto_id,
producto_nombre,
JSON_OBJECTAGG(atributo, valor) AS especificaciones
FROM productos_atributos
GROUP BY producto_id, producto_nombre;| producto_id | producto_nombre | especificaciones |
|---|---|---|
| 1 | iPhone 15 Pro | {"chip": "A17 Pro", "color": "Titanio Natural", "almacenamiento": "256GB"} |
| 2 | MacBook Air M3 | {"ram": "16GB", "color": "Medianoche", "pantalla": "15 pulgadas"} |
| 3 | Camiseta algodón | {"color": "Azul marino", "talla": "M", "material": "100% algodón"} |
Cada producto obtiene su propio objeto JSON con atributos diferentes según su naturaleza. Este enfoque es muy habitual en bases de datos de e-commerce donde los productos tienen esquemas flexibles.
Caso práctico: pivotar datos de ventas
Supongamos que tienes un resumen de ventas mensuales y quieres pivotar las filas en un objeto donde cada clave es un mes:
CREATE TABLE ventas_mensuales (
anio INT,
mes VARCHAR(20),
total DECIMAL(10,2)
);
INSERT INTO ventas_mensuales VALUES
(2025, 'enero', 45200.00),
(2025, 'febrero', 38900.50),
(2025, 'marzo', 52100.75),
(2024, 'enero', 41000.00),
(2024, 'febrero', 35600.25),
(2024, 'marzo', 48300.00);SELECT
anio,
JSON_OBJECTAGG(mes, total) AS ventas_por_mes
FROM ventas_mensuales
GROUP BY anio;| anio | ventas_por_mes |
|---|---|
| 2024 | {"enero": 41000.00, "marzo": 48300.00, "febrero": 35600.25} |
| 2025 | {"enero": 45200.00, "marzo": 52100.75, "febrero": 38900.50} |
En lugar de tener múltiples filas por año, obtienes una sola fila con un objeto JSON que contiene todos los meses. Esto es especialmente útil cuando necesitas devolver datos pivotados a una aplicación frontend que espera objetos JSON.
Caso práctico: construir respuestas de API
Cuando una API necesita devolver datos agrupados con estructura de diccionario, JSON_OBJECTAGG permite construir la respuesta directamente en la consulta:
CREATE TABLE permisos_usuario (
usuario_id INT,
recurso VARCHAR(50),
permiso VARCHAR(20)
);
INSERT INTO permisos_usuario VALUES
(1, 'productos', 'lectura_escritura'),
(1, 'pedidos', 'solo_lectura'),
(1, 'usuarios', 'administrador'),
(2, 'productos', 'solo_lectura'),
(2, 'pedidos', 'lectura_escritura'),
(2, 'usuarios', 'denegado');SELECT
usuario_id,
JSON_OBJECTAGG(recurso, permiso) AS permisos
FROM permisos_usuario
GROUP BY usuario_id;| usuario_id | permisos |
|---|---|
| 1 | {"pedidos": "solo_lectura", "usuarios": "administrador", "productos": "lectura_escritura"} |
| 2 | {"pedidos": "lectura_escritura", "usuarios": "denegado", "productos": "solo_lectura"} |
La aplicación puede consumir directamente este JSON para verificar permisos por recurso sin necesidad de transformaciones adicionales.
Manejo de NULL y claves duplicadas
Si la clave es NULL, MySQL lanza un error. Las claves de un objeto JSON no pueden ser nulas:
SELECT JSON_OBJECTAGG(NULL, 'valor');
-- Error: JSON documents may not contain NULL member names.Si el valor es NULL, se incluye en el objeto sin problema:
SELECT JSON_OBJECTAGG('clave', NULL);| JSON_OBJECTAGG('clave', NULL) |
|---|
| {"clave": null} |
Cuando hay claves duplicadas en el grupo, MySQL usa el último valor encontrado. No se genera error ni advertencia, simplemente se sobrescribe:
SELECT JSON_OBJECTAGG(clave, valor) AS resultado
FROM (
SELECT 'modo' AS clave, 'produccion' AS valor
UNION ALL
SELECT 'modo', 'desarrollo'
) datos;| resultado |
|---|
| {"modo": "desarrollo"} |
El valor 'desarrollo' sobrescribió a 'produccion' porque ambos compartían la clave 'modo'. Asegúrate de que las claves sean únicas dentro de cada grupo para evitar pérdida de datos inadvertida.
Si el conjunto de filas está vacío (por ejemplo, un grupo sin filas), JSON_OBJECTAGG devuelve NULL.
Combinación con otras funciones
Puedes combinar JSON_OBJECTAGG con otras funciones JSON para crear estructuras más elaboradas. Por ejemplo, puedes anidar arrays dentro del objeto:
SELECT JSON_OBJECT(
'empresa', 'TiendaOnline',
'configuracion', (
SELECT JSON_OBJECTAGG(clave, valor)
FROM configuraciones
WHERE seccion = 'app'
)
) AS respuesta;| respuesta |
|---|
| {"empresa": "TiendaOnline", "configuracion": {"nombre": "TiendaOnline", "version": "3.2.1", "modo_debug": "false"}} |
También puedes usar CAST para que los valores numéricos se almacenen como números JSON en lugar de cadenas:
SELECT JSON_OBJECTAGG(
mes,
CAST(total AS JSON)
) AS ventas
FROM ventas_mensuales
WHERE anio = 2025;De esta forma el valor en el objeto es un número JSON real, no una cadena entrecomillada.
En el siguiente artículo veremos JSON_TABLE para convertir datos JSON en filas y columnas relacionales.
Escrito por Eduardo Lázaro
