El lenguaje de consulta JSONPath en la práctica
En resumen: En 2024 JSONPath por fin obtuvo un estándar formal, RFC 9535. Este artículo explica la sintaxis, la semántica de los filtros, las funciones y las diferencias históricas entre implementaciones.

En resumen: localiza primero con una ruta explícita y filtra sobre ella, y escribe las expresiones conforme a RFC 9535; usa acceso a propiedades o JSON Pointer para un único campo fijo, y jq o código para la agregación.
Probablemente hayas visto expresiones como
$.store.book[?(@.price < 10)].title, pero no se convirtió en norma internacional hasta 2024.
1. Del estándar de facto al estándar formal
JSONPath lo propuso Stefan Gössner en 2007, y durante mucho tiempo la única "especificación" fue una sola entrada de blog, con cada implementación de cada lenguaje haciendo lo que le parecía:
- si los filtros necesitan paréntesis (
?(@.a)frente a?@.a); - si se admite el índice de array
[-1]; - si el resultado devuelve "valores" o "valores y rutas";
- si se permiten expresiones de script (
?(@.price < $.max)).
RFC 9535, publicado en febrero de 2024, unificó estas divergencias en una sola sintaxis y semántica y proporcionó una suite oficial de pruebas de conformidad. Los proyectos nuevos deberían seguir RFC 9535; al migrar proyectos antiguos, confirma primero la versión de la implementación.
2. Chuleta de sintaxis
| Sintaxis | Significado | Ejemplo |
|---|---|---|
$ |
Nodo raíz | $.store |
@ |
Nodo actual dentro de un filtro | $[?@.price < 10] |
.name |
Nodo hijo (el nombre debe coincidir con [A-Za-z_][A-Za-z0-9_]*) |
$.store.book |
['name'] |
Nodo hijo (cualquier nombre, incluidos los no ASCII y los que llevan puntos) | $['store']['书'] |
[0] |
Índice de array; los números negativos cuentan desde el final | $.book[0], $.book[-1] |
[0,2] |
Varios índices / varios nombres | $.book[0,2] |
[1:3] |
Porción, inicio incluido y fin excluido | $.book[1:3] |
[::2] |
Porción con paso | $.book[::2] |
[*] |
Comodín para todos los nodos hijos | $.book[*].title |
..name |
Descenso recursivo, buscando a cualquier profundidad | $..price |
..[*] |
Tomar todos los nodos de forma recursiva | $..* |
[?<expr>] |
Selector de filtro | $.book[?@.price < 10] |
Una semántica que debes recordar: JSONPath devuelve un conjunto de nodos (nodelist), no un valor único. Incluso cuando solo coincide un nodo, el resultado es una lista de longitud uno.
3. Recorrido de la sintaxis con un solo ejemplo
{
"store": {
"book": [
{ "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
{ "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
{ "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
| Expresión | Resultado |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 precios (incluido el precio de la bicicleta) |
$.store.book[*].title |
3 títulos |
$.store.book[?@.price < 10].title |
Títulos de los libros 1 y 3 |
$.store.book[?@.tags] |
Todos los libros que tienen tags (comprobación de existencia) |
$.store.book[-1].title |
Título del último libro |
$..book[0,1] |
Los dos primeros libros |
$.store.book[?@.price < $.expensive] |
Sintácticamente depende de la implementación; RFC 9535 no permite referencias a la raíz |
La última fila es una trampa histórica: muchas implementaciones permiten $.expensive dentro de un filtro para comparar entre nodos, algo que RFC 9535 prohíbe explícitamente —
un filtro solo puede acceder al nodo actual @. Cuando necesites comparar con un umbral global, pasa el umbral como argumento a la función de consulta.
4. Qué pueden expresar los filtros
Las expresiones de filtro de RFC 9535 admiten cuatro tipos de construcciones:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
Algunos ejemplos prácticos:
// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// More than one tag
$.store.book[?length(@.tags) > 1]
// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()
match() es una coincidencia de cadena completa y search() es una búsqueda de subcadena; el dialecto de expresiones regulares subyacente es I-Regexp (un subconjunto de RFC 9485),
que no admite ni retroreferencias ni lookaround — eso es a la vez una limitación y un escudo útil contra el DoS por expresiones regulares.
5. Diferencias entre implementaciones: comprueba antes de migrar
| Implementación | Lenguaje | Compatibilidad con RFC 9535 | Notas |
|---|---|---|---|
jsonpath-plus |
JavaScript | Mayormente compatible | Admite funciones de extensión y expresiones de script; más capaz pero no estrictamente estándar |
jsonpath (dchester) |
JavaScript | Parcial | Biblioteca veterana con sintaxis ?() y una estructura de resultado diferente |
ojg / gjson |
Go | ojg tiene buena compatibilidad |
gjson usa otra sintaxis de rutas y no es JSONPath |
jsonpath-ng |
Python | Parcial | Admite extensiones y es bastante permisiva |
Jayway JsonPath |
Java | Parcial | Ecosistema amplio; el comportamiento histórico difiere del RFC |
Tres comprobaciones antes de migrar:
- Si los paréntesis del filtro son obligatorios;
- Si se admiten índices negativos como
-1; - Si el resultado es un "array de valores" o un "array de objetos
{path, value}".
La tercera es la más fácil de tropezar: el frontend lee result[0], y después de cambiar de biblioteca recibe un objeto, dejando la página en blanco.
6. Rendimiento y seguridad
Rendimiento
- JSONPath es esencialmente un único recorrido con complejidad O(número de nodos), y no puede usar índices;
- El descenso recursivo con
..es el más caro; si puedes localizar primero con$.store.booky luego filtrar, no escribas$..book; - Compila las expresiones una vez y reutilízalas (la mayoría de bibliotecas ofrecen
compileo caché) en lugar de volver a analizarlas en cada consulta; - Para documentos con más de 100.000 nodos, considera primero la extracción exacta con JSON Pointer, o el preprocesamiento en el servidor con
jq.
Seguridad
- Las expresiones que vienen de la entrada del usuario deben tener límites de longitud y de profundidad de anidamiento;
- No dejes que JSONPath se encargue de la autorización — solo puede leer, y no puede expresar "qué campos son invisibles";
- Cuando la expresión regular usada por
match()viene de los usuarios, limita su complejidad; aunque es más segura que PCRE, sigue siendo recomendable un límite de longitud.
7. Cuándo no usar JSONPath
- Solo necesitas un campo fijo → usa
data.store.bicycle.colordirectamente; - Necesitas escribir de vuelta después de modificar → usa JSON Pointer o un parche; JSONPath no escribe de vuelta;
- Necesitas agregación (sumar, agrupar) → usa
jq, SQL o código; JSONPath solo selecciona; - El documento es enorme y solo necesitas unos pocos campos → usa un parser de streaming en lugar de leer todo el árbol en memoria primero.
8. Lista de comprobación
- [ ] Las expresiones siguen RFC 9535 y no dependen de referencias a la raíz;
- [ ] Está claro que el resultado es un conjunto de nodos; usa
[0]ovalue()para obtener un valor único; - [ ] Las expresiones están precompiladas y se reutilizan;
- [ ] Las expresiones controladas por el usuario tienen límites de longitud y complejidad;
- [ ] Se ejecutan los casos oficiales de conformidad al cambiar de implementación.
Preguntas frecuentes
¿Cómo elijo entre JSON Pointer y JSONPath?
Usa el acceso normal a propiedades o un JSON Pointer cuando solo necesites un campo fijo; recurre a JSONPath cuando necesites filtros, comodines o descenso recursivo. JSONPath solo selecciona y nunca escribe de vuelta, así que la escritura corresponde a JSON Pointer o a un parche.
¿Por qué un filtro no puede referenciar $.expensive?
RFC 9535 prohíbe explícitamente las referencias a la raíz; un filtro solo puede acceder al nodo actual @. Pasa el umbral como argumento a la función de consulta en su lugar. Implementaciones antiguas aceptan la sintaxis, pero se rompe al cambiar o actualizar de biblioteca.
¿Debería fiarme de result[0] para leer un valor?
Solo después de confirmar que la implementación actual devuelve un array de valores. Algunas bibliotecas devuelven objetos {path, value}, así que result[0] se convierte en un objeto y la página queda en blanco. Lee valores únicos mediante [0] o value(), y ejecuta la suite oficial de conformidad antes de cambiar de implementación.