JSON_CONTAINS_PATH

La función JSON_CONTAINS_PATH verifica si una o más rutas existen dentro de un documento JSON. A diferencia de JSON_CONTAINS, que comprueba si un valor concreto está presente, JSON_CONTAINS_PATH solo se preocupa por la existencia de la estructura: si la clave existe, devuelve verdadero, sin importar qué valor contenga. Esto la convierte en una herramienta ideal para validar la estructura de documentos JSON, comprobar si un campo opcional fue proporcionado, o filtrar registros según los campos que contienen.

Sintaxis

JSON_CONTAINS_PATH(json_doc, tipo_busqueda, ruta [, ruta ...])

El primer argumento es el documento JSON que se inspecciona. El segundo es una cadena que indica el modo de búsqueda: 'one' devuelve 1 si al menos una de las rutas existe, mientras que 'all' devuelve 1 solo si todas las rutas proporcionadas existen. A partir del tercer argumento se especifican una o más rutas JSON a verificar. La función devuelve 1 si la condición se cumple, 0 si no se cumple, y NULL si el documento JSON es NULL.

JSON_CONTAINS_PATH(json_doc, 'one', '$.ruta1', '$.ruta2')
JSON_CONTAINS_PATH(json_doc, 'all', '$.ruta1', '$.ruta2')

Comportamiento básico

Veamos el comportamiento con un documento JSON sencillo:

SELECT JSON_CONTAINS_PATH(
  '{"nombre": "iPhone", "precio": 1299, "color": "negro"}',
  'one',
  '$.precio'
) AS existe_precio;
existe_precio
1

La ruta $.precio existe en el documento, por lo que devuelve 1. Si buscamos una ruta que no existe:

SELECT JSON_CONTAINS_PATH(
  '{"nombre": "iPhone", "precio": 1299}',
  'one',
  '$.descuento'
) AS existe_descuento;
existe_descuento
0

La diferencia entre los modos 'one' y 'all' se aprecia claramente cuando buscas varias rutas a la vez:

SELECT
    JSON_CONTAINS_PATH(
      '{"nombre": "iPhone", "precio": 1299}',
      'one', '$.precio', '$.descuento'
    ) AS modo_one,
    JSON_CONTAINS_PATH(
      '{"nombre": "iPhone", "precio": 1299}',
      'all', '$.precio', '$.descuento'
    ) AS modo_all;
modo_onemodo_all
10

Con 'one', basta con que $.precio exista para devolver 1. Con 'all', como $.descuento no existe, devuelve 0 porque no se cumplen todas las condiciones.

También puedes verificar rutas dentro de estructuras anidadas:

SELECT JSON_CONTAINS_PATH(
  '{"especificaciones": {"pantalla": "6.1 pulgadas", "batería": 4000}}',
  'one',
  '$.especificaciones.pantalla'
) AS existe;
existe
1

Y puedes verificar rutas dentro de arrays usando índices:

SELECT JSON_CONTAINS_PATH(
  '{"colores": ["rojo", "azul", "verde"]}',
  'one',
  '$.colores[2]'
) AS tercer_elemento;
tercer_elemento
1

El array tiene tres elementos (índices 0, 1 y 2), por lo que $.colores[2] existe. Si buscaras $.colores[5], devolvería 0 porque no hay un sexto elemento.

Caso práctico: validación de datos de producto

Imagina una tabla de productos donde cada registro tiene un campo JSON con información variable. No todos los productos incluyen los mismos campos, y necesitas identificar cuáles tienen la información completa:

CREATE TABLE productos (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nombre VARCHAR(100),
    detalles JSON
);
 
INSERT INTO productos (nombre, detalles) VALUES
('iPhone 15 Pro',
 '{"marca": "Apple", "precio": 1299, "peso": 187, "dimensiones": {"alto": 146.6, "ancho": 70.6}, "garantía": 24}'),
('Funda genérica',
 '{"precio": 9.99, "color": "negro"}'),
('Samsung Galaxy S24',
 '{"marca": "Samsung", "precio": 899, "peso": 167, "dimensiones": {"alto": 147.0, "ancho": 70.6}}'),
('Cable USB-C',
 '{"precio": 12.50, "longitud": "1m"}'),
('MacBook Air M3',
 '{"marca": "Apple", "precio": 1399, "peso": 1240, "dimensiones": {"alto": 304.1, "ancho": 215.0}, "garantía": 24}');

Para encontrar productos que tienen información completa de marca, peso y dimensiones:

SELECT nombre
FROM productos
WHERE JSON_CONTAINS_PATH(detalles, 'all',
    '$.marca', '$.peso', '$.dimensiones');
nombre
iPhone 15 Pro
Samsung Galaxy S24
MacBook Air M3

La funda genérica y el cable USB-C no aparecen porque les faltan esos campos. Para encontrar productos que tienen al menos marca o garantía:

SELECT nombre
FROM productos
WHERE JSON_CONTAINS_PATH(detalles, 'one',
    '$.marca', '$.garantía');
nombre
iPhone 15 Pro
Samsung Galaxy S24
MacBook Air M3

Caso práctico: detección de campos opcionales

Un uso frecuente de JSON_CONTAINS_PATH es detectar qué registros tienen campos opcionales para procesarlos de forma diferente. Considera una tabla de pedidos donde la información de envío puede variar:

CREATE TABLE pedidos (
    id INT PRIMARY KEY AUTO_INCREMENT,
    cliente VARCHAR(100),
    datos_envio JSON
);
 
INSERT INTO pedidos (cliente, datos_envio) VALUES
('María García', '{"dirección": "Calle Mayor 15", "ciudad": "Madrid", "código_postal": "28001", "instrucciones": "Dejar en portería", "teléfono_contacto": "612345678"}'),
('Carlos López', '{"dirección": "Av. Diagonal 200", "ciudad": "Barcelona", "código_postal": "08018"}'),
('Ana Martínez', '{"dirección": "Gran Vía 50", "ciudad": "Valencia", "código_postal": "46002", "instrucciones": "Llamar al timbre 3B"}');

Para identificar pedidos que incluyen instrucciones especiales de entrega:

SELECT
    cliente,
    CASE
        WHEN JSON_CONTAINS_PATH(datos_envio, 'one', '$.instrucciones')
        THEN datos_envio->>'$.instrucciones'
        ELSE 'Sin instrucciones'
    END AS instrucciones
FROM pedidos;
clienteinstrucciones
María GarcíaDejar en portería
Carlos LópezSin instrucciones
Ana MartínezLlamar al timbre 3B

Para contar cuántos pedidos tienen información de contacto adicional:

SELECT
    COUNT(*) AS total_pedidos,
    SUM(JSON_CONTAINS_PATH(datos_envio, 'one', '$.teléfono_contacto')) AS con_telefono,
    SUM(JSON_CONTAINS_PATH(datos_envio, 'one', '$.instrucciones')) AS con_instrucciones
FROM pedidos;
total_pedidoscon_telefonocon_instrucciones
312

Como JSON_CONTAINS_PATH devuelve 1 o 0, puedes usar SUM directamente para contar cuántos registros cumplen la condición.

Caso práctico: validación de estructura en respuestas de API

Cuando almacenas respuestas de APIs externas en columnas JSON, es habitual que la estructura varíe. Puedes usar JSON_CONTAINS_PATH para clasificar las respuestas según su completitud:

CREATE TABLE respuestas_api (
    id INT PRIMARY KEY AUTO_INCREMENT,
    endpoint VARCHAR(200),
    respuesta JSON
);
 
INSERT INTO respuestas_api (endpoint, respuesta) VALUES
('/api/pago', '{"estado": "completado", "transaccion_id": "TXN-001", "monto": 150.00, "moneda": "EUR", "metodo": "tarjeta"}'),
('/api/pago', '{"estado": "error", "código_error": "E-402", "mensaje": "Fondos insuficientes"}'),
('/api/pago', '{"estado": "pendiente", "transaccion_id": "TXN-003", "monto": 89.99}');
SELECT
    id,
    respuesta->>'$.estado' AS estado,
    CASE
        WHEN JSON_CONTAINS_PATH(respuesta, 'all', '$.transaccion_id', '$.monto', '$.metodo')
        THEN 'Completa'
        WHEN JSON_CONTAINS_PATH(respuesta, 'all', '$.código_error', '$.mensaje')
        THEN 'Error documentado'
        ELSE 'Parcial'
    END AS tipo_respuesta
FROM respuestas_api;
idestadotipo_respuesta
1completadoCompleta
2errorError documentado
3pendienteParcial

Manejo de NULL

Cuando el documento JSON es NULL, la función devuelve NULL:

SELECT JSON_CONTAINS_PATH(NULL, 'one', '$.clave') AS resultado;
resultado
NULL

Un detalle importante: si una clave existe pero su valor JSON es null, JSON_CONTAINS_PATH devuelve 1 porque la ruta sí existe. La función evalúa estructura, no contenido:

SELECT JSON_CONTAINS_PATH(
  '{"nombre": "Test", "descuento": null}',
  'one',
  '$.descuento'
) AS existe;
existe
1

Esto es útil para distinguir entre "el campo no fue proporcionado" y "el campo fue proporcionado con valor nulo".

Combinación con otras funciones

JSON_CONTAINS_PATH se complementa bien con JSON_EXTRACT para acceder a valores solo cuando sabes que existen:

SELECT
    nombre,
    CASE
        WHEN JSON_CONTAINS_PATH(detalles, 'one', '$.garantía')
        THEN CONCAT(detalles->>'$.garantía', ' meses')
        ELSE 'No disponible'
    END AS garantía
FROM productos;
nombregarantía
iPhone 15 Pro24 meses
Funda genéricaNo disponible
Samsung Galaxy S24No disponible
Cable USB-CNo disponible
MacBook Air M324 meses

Recuerda que JSON_CONTAINS_PATH verifica si la ruta existe, mientras que JSON_CONTAINS verifica si un valor concreto está presente. Si necesitas saber si un array contiene el valor "rojo", usa JSON_CONTAINS. Si necesitas saber si el campo color existe sin importar su valor, usa JSON_CONTAINS_PATH. En el siguiente artículo veremos JSON_SEARCH para buscar valores de texto dentro de documentos JSON.

Escrito por Eduardo Lázaro