JSON Toolkit

Filtros JSONPath en profundidad: recursión, porciones y coincidencia de múltiples condiciones

En resumen: El descenso recursivo, las porciones y los filtros con varias condiciones son donde JSONPath se vuelve potente y también donde más se usa mal. Cubre la semántica de filtros, la forma de los resultados y las diferencias entre bibliotecas.

Intermedio~10 min de lecturaPublicado el 2026-10-31Actualizado el 2026-10-31
Portada de Filtros JSONPath en profundidad

En resumen: localiza primero una profundidad conocida y luego añade porciones y filtros en lugar de recurrir a $..; una ruta simple dentro de un filtro solo comprueba existencia, así que para comprobar un booleano usa @.stock == true.

Cualquiera puede escribir $.store.book[0].title. Lo que realmente rompe JSONPath es combinar $.. con porciones y múltiples condiciones, y acabar obteniendo demasiados resultados, demasiado pocos o un orden que no esperabas.

1. Un documento de ejemplo

{
  "store": {
    "book": [
      { "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
      { "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
      { "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
      { "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "updatedAt": "2026-10-31"
}

Todas las expresiones siguientes usan este documento como entrada. Ten presentes dos premisas: el resultado es un conjunto de nodos, y su orden sigue el orden del documento y no el orden en el que se escribieron las condiciones. Dentro de un filtro, @ se refiere solo al nodo candidato actual.

2. Descenso recursivo $..: cómodo pero caro

.. significa "buscar de forma recursiva a cualquier profundidad desde la posición actual". RFC 9535 lo define como un segmento descendiente, y puede combinarse con nombres, comodines, índices, porciones y filtros:

$..price                  // every member named price at any depth
$..book[?(@.price < 10)]  // any book at any depth, then filter
$..[?(@.stock)]           // any node at any depth where stock exists

Tres cosas que hay que entender:

  1. $..book encuentra todos los nodos llamados book, sea cual sea su profundidad;
  2. El recorrido es en profundidad, y el orden de salida depende de cómo la implementación visite a los hijos;
  3. .. no puede usar ningún índice, así que su coste crece con el número total de nodos.

Por eso, $..price y $.store.book[*].price pueden diferir solo por el precio de la bicicleta en su salida, y sin embargo diferir en un orden de magnitud en coste. Cuando conozcas la profundidad, escribe siempre la ruta explícita. Si solo quieres comparar los límites de los dos lenguajes de rutas, consulta JSONPath frente a JSON Pointer.

3. Porciones de array y comodines: tres trampas

La sintaxis de porción es [start:end:step], con el inicio incluido y el fin excluido, y los números negativos cuentan desde el final:

Expresión Significado Resultado (con cuatro libros)
[0:2] Los dos primeros libros Libros 1 y 2
[1:] Desde el segundo libro hasta el final Libros 2, 3 y 4
[:2] Los dos primeros libros Libros 1 y 2
[-2:] Los dos últimos libros Libros 3 y 4
[::2] Un libro sí y otro no Libros 1 y 3
[::-1] Orden inverso Libros 4, 3, 2 y 1

Tres trampas:

  • Un paso de cero no es válido. RFC 9535 exige un paso distinto de cero, mientras que algunas implementaciones devuelven silenciosamente un conjunto vacío;
  • Los pasos negativos no están universalmente soportados. La implementación original de Goessner los admite, pero algunas implementaciones mínimas aceptan solo pasos positivos;
  • Filtrar después de una porción no amplía el ámbito. [0:2][?(@.stock)] solo filtra los dos libros producidos por la porción; no vuelve al array completo.

El comodín [*] se puede combinar con porciones, pero [*] produce "todos los nodos hijos". Si esos hijos son objetos, añadir .title da todos los títulos. Si un hijo es a su vez un array, .title no aplanará implícitamente otro nivel. Escribe [*] de nuevo cuando necesites expandir; no esperes un aplanado implícito.

4. Filtros con múltiples condiciones: comparaciones, lógica y existencia

Un filtro [?(...)] puede contener cuatro tipos de construcciones: comparaciones, operadores lógicos, comprobaciones de existencia y funciones. Una consulta habitual con múltiples condiciones se parece a esto:

$..book[?(@.price < 10 && @.stock)]

Significa "precio por debajo de 10 y stock verdadero". Presta atención a varios detalles semánticos:

  • @.stock es una comprobación de existencia, no "el valor es verdadero". Si el campo existe pero su valor es false, la condición sigue cumpliéndose. Para comprobar un valor booleano debes escribir @.stock == true;
  • && tiene mayor precedencia que ||. Añade paréntesis al mezclarlos en lugar de fiarte de la memoria;
  • Ambos lados de una comparación deben tener el mismo tipo. Los números y las cadenas no se convierten implícitamente; @.price < "10" es falso o un error en implementaciones estrictas;
  • Un campo ausente siempre hace falsa una comparación. @.missing < 10 no lanza un error; simplemente no coincide con nada.

Este es el punto donde más fácil se tropieza al escribir filtros como si fueran JavaScript: if (obj.stock) comprueba la veracidad en JavaScript, mientras que JSONPath comprueba la existencia. Para la sintaxis estándar completa y las funciones, vuelve a El lenguaje de consulta JSONPath en la práctica.

5. Expresiones de script: más potencia, menos portabilidad

La implementación original de Goessner y jsonpath-plus permiten comparaciones entre nodos como [?(@.price < $.max)], e incluso scripts arbitrarios. RFC 9535 permite explícitamente solo el acceso al nodo actual @ y prohíbe $ dentro de un filtro.

// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]

// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })

Las expresiones de script crean tres problemas:

  1. La portabilidad cae a cero. Cambiar de biblioteca significa reescribir todas las expresiones;
  2. El límite de seguridad desaparece. Cuando las expresiones vienen de los usuarios, la evaluación de scripts entrega la ejecución de código;
  3. La optimización estática se vuelve imposible. La implementación solo puede interpretar nodo a nodo, lo que es lento y difícil de cachear.

Si de verdad necesitas comparación entre nodos, pasa el umbral como parámetro de la consulta y filtra una segunda vez en el código de la aplicación. Normalmente eso es más estable que depender de una sintaxis de extensión.

6. Forma de los resultados: crees que obtienes valores, pero obtienes nodos

Una misma expresión puede devolver al menos tres formas según la biblioteca:

["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }]                 // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }]                          // JSON Pointer + value

Esto afecta directamente a tres cosas: cómo lee el frontend los resultados, cómo se deduplican los resultados y cómo se convierten los resultados de la consulta en operaciones de escritura. La nodelist definida por RFC 9535 es un conjunto de referencias a nodos, pero las implementaciones de JavaScript suelen devolver solo valores porque las referencias no pueden cruzar los límites entre procesos.

Consejo práctico:

  • Lee siempre un único valor mediante [0] o value(); nunca asumas que el resultado es un escalar;
  • Cuando necesites volver a escribir, elige una implementación que devuelva JSON Pointers o construye el pointer tú mismo;
  • Un resultado vacío y un resultado [null] son distintos; comprueba la longitud en lugar de la veracidad.

7. Diferencias entre bibliotecas: ejecuta la suite de conformidad antes de migrar

Capacidad Goessner original jsonpath-plus RFC 9535 estricto
Paréntesis en el filtro ?() Obligatorios Compatible Opcionales
Referencia a la raíz $.x Compatible Compatible Prohibida
Expresiones de script Compatible Compatible Prohibidas
Índices y pasos negativos Compatible Compatible Compatible
Devolver rutas Configurable Configurable No especificado
Funciones como length() Ninguna Extensiones Funciones estándar

Antes de migrar, verifica al menos cuatro cosas: si los paréntesis son obligatorios, si $ puede aparecer dentro de un filtro, cómo se comportan los pasos negativos y la estructura del resultado. La suite oficial de pruebas de conformidad cubre la mayoría de las diferencias de sintaxis y evita fallos silenciosos como "desaparecieron tres filas después de cambiar de biblioteca".

8. Rendimiento y lista de comprobación de malos usos

El rendimiento se reduce a una frase: JSONPath es un recorrido completo, y .. es el tipo de recorrido completo más caro.

  • Para documentos con más de 100.000 nodos, acota primero el ámbito con una ruta explícita y luego filtra el subconjunto;
  • Compila una expresión una vez y reutilízala; no vuelvas a analizar la cadena dentro de un bucle;
  • Cambia a jq o a código para la agregación (sumar, agrupar, ordenar); JSONPath solo selecciona;
  • Coloca las condiciones lo antes posible: $.store.book[?(@.price < 10)] recorre muchos menos nodos irrelevantes que $..[?(@.price < 10)].

Lista de comprobación de malos usos:

  • [ ] Tratar una comprobación de existencia como una comprobación de veracidad booleana;
  • [ ] Usar $.. en lugar de una profundidad conocida;
  • [ ] Depender de expresiones de script para comparar entre nodos;
  • [ ] Asumir que el resultado siempre es un array de valores;
  • [ ] Permitir expresiones controladas por el usuario sin límites de longitud y anidamiento.

Preguntas frecuentes

¿Por qué @.stock sigue coincidiendo cuando el campo es false?

Porque una ruta simple dentro de un filtro significa "el campo existe", no "el valor es verdadero". Para comprobar el booleano, compara explícitamente con @.stock == true. Esta es la mayor diferencia semántica entre JSONPath y la veracidad de JavaScript.

¿Es estable el orden del descenso recursivo?

No. RFC 9535 especifica semántica de conjunto de nodos, y el orden del recorrido depende de cómo la implementación visite a los hijos. Cuando necesites un orden fijo, localiza el array y lee por índice, o ordena los resultados en el código de la aplicación.

¿Se pueden combinar porciones y filtros?

Sí, pero recuerda el ámbito. $.store.book[0:2][?(@.stock)] filtra solo los dos libros producidos por la porción. Para filtrar primero y hacer la porción después, escribe $.store.book[?(@.stock)][0:2]; un orden distinto da un resultado distinto.

¿Por qué jsonpath-plus admite $.expensive cuando el estándar lo prohíbe?

Porque los filtros se definen solo en términos del nodo actual @. Una referencia a la raíz hace que una expresión dependa de todo el contexto del documento, lo que es difícil de analizar estáticamente y de portar entre lenguajes. Pasa el umbral como parámetro en lugar de incrustarlo en la expresión.

¿Cómo evito que el descenso recursivo recorra nodos irrelevantes?

Escribe la ruta explícita siempre que conozcas la profundidad, acota primero el ámbito y filtra solo el subconjunto en lugar de usar $.. en vez de un nivel conocido. Coloca las condiciones lo antes posible: $.store.book[?(@.price < 10)] recorre muchos menos nodos irrelevantes que $..[?(@.price < 10)]. Para documentos con más de 100.000 nodos, hazlo primero.