Tipo de dato JSON
MySQL incorporó el tipo de dato JSON nativo a partir de la versión 5.7.8. A diferencia de almacenar JSON como texto en una columna VARCHAR o TEXT, el tipo JSON valida automáticamente que el contenido sea un documento JSON válido al momento de la inserción, lo almacena en un formato binario interno optimizado para lecturas rápidas y permite usar todas las funciones JSON del motor para consultar, modificar y extraer datos sin necesidad de parsear la cadena en la aplicación.
Por qué existe un tipo JSON nativo
Antes de que existiera el tipo JSON, los desarrolladores almacenaban documentos JSON como texto plano. Esto funcionaba para guardar y recuperar el documento completo, pero presentaba tres problemas serios: no había validación (podías insertar JSON malformado sin enterarte), no había rendimiento en las búsquedas (para buscar un valor dentro del JSON, MySQL tenía que recorrer toda la cadena como texto) y no había funciones especializadas para manipular el documento parcialmente.
El tipo nativo JSON resuelve estos tres problemas. Al insertar un valor, MySQL verifica que sea JSON válido y rechaza la operación si no lo es. Internamente almacena el documento en formato binario, lo que permite acceder a claves y elementos del array sin parsear todo el documento. Y proporciona más de 30 funciones para crear, leer, modificar y buscar dentro de documentos JSON directamente desde SQL.
Crear una tabla con columna JSON
Para definir una columna JSON, usa el tipo JSON en la definición de la tabla:
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(200) NOT NULL,
precio DECIMAL(10,2) NOT NULL,
atributos JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);La columna atributos almacenará un documento JSON con características variables del producto. Un portátil tendrá RAM y procesador, mientras que una camiseta tendrá talla y color. Esta flexibilidad es precisamente la ventaja del tipo JSON: cada fila puede tener una estructura diferente en esa columna.
Insertar datos JSON
Puedes insertar documentos JSON usando cadenas de texto que contengan JSON válido:
INSERT INTO productos (nombre, precio, atributos) VALUES
('MacBook Air M3', 1399.00, '{"ram": "16GB", "almacenamiento": "512GB", "procesador": "Apple M3", "color": "Medianoche"}'),
('iPhone 15 Pro', 1299.99, '{"almacenamiento": "256GB", "color": "Titanio negro", "pantalla": "6.1 pulgadas", "chip": "A17 Pro"}'),
('Camiseta algodón básica', 24.99, '{"talla": "M", "color": "Blanco", "material": "100% algodón", "genero": "Unisex"}');MySQL valida cada cadena antes de almacenarla. Si intentas insertar JSON malformado, obtienes un error inmediato:
INSERT INTO productos (nombre, precio, atributos) VALUES
('Producto malo', 9.99, '{"clave": valor_sin_comillas}');ERROR 3140 (22032): Invalid JSON text: "Invalid value."
at position 10 in value for column 'productos.atributos'.
Este comportamiento de validación automática es una de las razones principales para preferir el tipo JSON sobre TEXT.
También puedes usar funciones JSON para construir el documento
En lugar de escribir la cadena JSON a mano, puedes utilizar las funciones JSON_OBJECT y JSON_ARRAY:
INSERT INTO productos (nombre, precio, atributos) VALUES
('Samsung Galaxy S24', 899.99,
JSON_OBJECT(
'almacenamiento', '128GB',
'colores_disponibles', JSON_ARRAY('Negro', 'Violeta', 'Ámbar'),
'pantalla', JSON_OBJECT('tamaño', '6.2 pulgadas', 'tipo', 'Dynamic AMOLED')
)
);Esta forma es más segura porque las funciones se encargan de escapar caracteres especiales y garantizar un formato válido.
Consultar datos de una columna JSON
Para recuperar el documento completo, simplemente selecciona la columna:
SELECT nombre, atributos
FROM productos
WHERE nombre = 'MacBook Air M3';| nombre | atributos |
|---|---|
| MacBook Air M3 | {"ram": "16GB", "almacenamiento": "512GB", "procesador": "Apple M3", "color": "Medianoche"} |
Para extraer un valor específico dentro del documento, utiliza el operador -> con una ruta JSON Path:
SELECT nombre, atributos->'$.color' AS color
FROM productos;| nombre | color |
|---|---|
| MacBook Air M3 | "Medianoche" |
| iPhone 15 Pro | "Titanio negro" |
| Camiseta algodón básica | "Blanco" |
| Samsung Galaxy S24 | NULL |
Observa que los valores devueltos por -> vienen entrecomillados. Si necesitas el valor sin comillas, usa ->>:
SELECT nombre, atributos->>'$.color' AS color
FROM productos;| nombre | color |
|---|---|
| MacBook Air M3 | Medianoche |
| iPhone 15 Pro | Titanio negro |
| Camiseta algodón básica | Blanco |
| Samsung Galaxy S24 | NULL |
Filtrar filas por valores dentro del JSON
Puedes usar valores extraídos del JSON en la cláusula WHERE:
SELECT nombre, precio
FROM productos
WHERE atributos->>'$.color' = 'Blanco';| nombre | precio |
|---|---|
| Camiseta algodón básica | 24.99 |
También puedes verificar si un documento contiene un valor determinado con JSON_CONTAINS:
SELECT nombre, precio
FROM productos
WHERE JSON_CONTAINS(atributos, '"Negro"', '$.colores_disponibles');| nombre | precio |
|---|---|
| Samsung Galaxy S24 | 899.99 |
Formato binario interno y tamaño
Aunque insertas JSON como texto, MySQL lo convierte a un formato binario interno que ocupa más espacio que la cadena original pero permite accesos posicionales mucho más rápidos. Puedes comprobar el tamaño real almacenado con JSON_STORAGE_SIZE:
SELECT nombre,
LENGTH(atributos) AS longitud_texto,
JSON_STORAGE_SIZE(atributos) AS tamaño_binario
FROM productos;| nombre | longitud_texto | tamaño_binario |
|---|---|---|
| MacBook Air M3 | 91 | 137 |
| iPhone 15 Pro | 101 | 152 |
| Camiseta algodón básica | 80 | 122 |
| Samsung Galaxy S24 | 143 | 218 |
El formato binario es ligeramente mayor, pero la ganancia en velocidad de acceso compensa con creces este coste. El tamaño máximo de un documento JSON en MySQL está limitado por la variable del sistema max_allowed_packet, que por defecto es de 64 MB.
Manejo de NULL
Una columna JSON puede ser NULL, y es importante distinguir entre un JSON NULL y un SQL NULL:
INSERT INTO productos (nombre, precio, atributos) VALUES
('Producto sin atributos', 5.99, NULL),
('Producto con null JSON', 5.99, 'null');SELECT nombre, atributos, atributos IS NULL AS es_null_sql
FROM productos
WHERE precio = 5.99;| nombre | atributos | es_null_sql |
|---|---|---|
| Producto sin atributos | NULL | 1 |
| Producto con null JSON | null | 0 |
En el primer caso, la columna no tiene valor (SQL NULL). En el segundo, la columna contiene un documento JSON válido cuyo valor es el literal null de JSON. Son conceptos diferentes: SQL NULL significa ausencia de dato, mientras que JSON null es un valor JSON legítimo.
Cuándo usar JSON y cuándo normalizar
El tipo JSON es ideal cuando la estructura de los datos varía entre filas, como atributos de productos de diferentes categorías, preferencias de usuario personalizables, respuestas de APIs externas que necesitas almacenar tal cual o configuraciones flexibles que cambian con el tiempo.
Sin embargo, no deberías usar JSON para reemplazar columnas relacionales normales. Si todos los productos tienen siempre un nombre, un precio y un stock, esos datos deben ser columnas regulares. El JSON es para lo que varía, no para lo que es constante. Las columnas regulares tienen ventajas importantes: indexación directa, validación de tipos, restricciones de integridad (FOREIGN KEY, NOT NULL, UNIQUE) y mejor rendimiento en consultas.
Una buena regla es usar JSON para datos semiestructurados que consultas ocasionalmente, y columnas normales para datos que filtras, ordenas o agrupas frecuentemente. Si te encuentras extrayendo constantemente el mismo campo del JSON en un WHERE, probablemente deberías convertirlo en una columna regular o al menos crear una columna generada con un índice.
Caso práctico: configuración de usuario
Un ejemplo clásico donde JSON brilla es el almacenamiento de preferencias de usuario:
CREATE TABLE preferencias_usuario (
usuario_id INT PRIMARY KEY,
config JSON NOT NULL DEFAULT ('{}'),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
INSERT INTO preferencias_usuario (usuario_id, config) VALUES
(1, '{"tema": "oscuro", "idioma": "es", "notificaciones": {"email": true, "push": false}, "articulos_por_pagina": 25}'),
(2, '{"tema": "claro", "idioma": "en", "notificaciones": {"email": true, "push": true}}'),
(3, '{"tema": "oscuro", "idioma": "es"}');Cada usuario puede tener diferentes preferencias sin necesidad de crear una columna para cada posible configuración. Puedes acceder a valores anidados con la notación de punto:
SELECT usuario_id,
config->>'$.tema' AS tema,
config->>'$.notificaciones.email' AS notif_email
FROM preferencias_usuario;| usuario_id | tema | notif_email |
|---|---|---|
| 1 | oscuro | true |
| 2 | claro | true |
| 3 | oscuro | NULL |
El usuario 3 no tiene configuración de notificaciones, por lo que la extracción devuelve NULL en lugar de un error. Este comportamiento seguro permite trabajar con documentos de estructura variable sin preocuparse por claves inexistentes.
En el siguiente artículo veremos JSON Path para navegar por documentos JSON.
Escrito por Eduardo Lázaro
