JSONPath-Plus / JSONPath

A fork of JSONPath from http://goessner.net/articles/JsonPath/
Other
975 stars 175 forks source link

Syntax check of JSONPath without evaluation #134

Open b4rd opened 4 years ago

b4rd commented 4 years ago

Is there a way to determine whether a given path is syntactically correct?

It seems that, from the evaluation result it's not possible to tell whether there was no match or the path was simply invalid.

macebalp commented 2 years ago

I need this too, have a UI tool where users can input JSONPath that will be evaluated by a backend process processing JSONs. I don't mind evaluating in the UI, but an invalid JSONPath only fails if the JSON provided matches the path to the invalid point otherwise it just returns false. Example:

JSONPath: $.properties.products[?(@.product_id.match(/prd1|prd2/)].price

Note one parentheses is missing in the above path

If i do:

JSONPath({ path: data.revenue, json: {});

I don't any indication of the faulty path. I do get the exception if i do:

JSONPath({ path: data.revenue, json: {
      properties: {
          products: [
              {
                  product_id: "prdWhatever",
                  price: 1
              }
          ]
      }
  }
  });

I've looking into the code to se i could trick the lib to evaluate all the parts of the JSONPath with no success.

brettz9 commented 2 years ago

Did you try the option wrap: false? undefined should be the case when there is no matching.

brettz9 commented 2 years ago

Oh, sorry, I misunderstood--thought you were asking whether one could distinguish between an actual empty array being found and no matches.

The problem is that JSONPath is not schema-aware searching. What is an invalid path anyways in plain JSON? Each document can be different unless you are imposing schema restraints such as with JSON Schema. You might see if such a tool exists already, bearing in mind though that with JSON Schema, any number of possibilities could be defined in the schema, so a generic JSON Schema tool to try to detect the validity of a path could become very complex (albeit useful).

Now, yes, a feature could be added within this library to check along the way, but this project is not being very actively developed now. A PR might be welcome if simple and well-documented enough to review.

leviznull commented 2 years ago

@brettz9 I think that adding a function for a strictly pre-evaluation phase syntax validation would be useful on it's own.

This would be extremely useful for validating <input> fields and would allow us to give instant feedback to the user about the syntax error, whilst also saving us from executing an invalid query.

I believe we could tweak the expression parsing logic from Stefan Goessner original JSON path implementation to do syntax validation only - without evaluating the path against a JSON object.

/* JSONPath 0.8.5 - XPath for JSON
 *
 * Copyright (c) 2007 Stefan Goessner (goessner.net)
 * Licensed under the MIT (MIT-LICENSE.txt) licence.
 *
 * Proposal of Chris Zyp goes into version 0.9.x
 * Issue 7 resolved
 */
function jsonPath(obj, expr, arg) {
   var P = {
      resultType: arg && arg.resultType || "VALUE",
      result: [],
      /**
      * normalizes the JSON path expression
      * @param {*} expr the JSON path expression
      * @returns the normalized JSON path expression
      */
      normalize: function(expr) {
         var subx = [];
         return expr.replace(/[\['](\??\(.*?\))[\]']|\['(.*?)'\]/g, function($0,$1,$2){return "[#"+(subx.push($1||$2)-1)+"]";})  /* http://code.google.com/p/jsonpath/issues/detail?id=4 */
                    .replace(/'?\.'?|\['?/g, ";")
                    .replace(/;;;|;;/g, ";..;")
                    .replace(/;$|'?\]|'$/g, "")
                    .replace(/#([0-9]+)/g, function($0,$1){return subx[$1];});
      },
      /**
      * digits are replaced by index access
      * @param {*} path
      * @returns
      */
      asPath: function(path) {
         var x = path.split(";"), p = "$";
         for (var i=1,n=x.length; i<n; i++)
            p += /^[0-9*]+$/.test(x[i]) ? ("["+x[i]+"]") : ("['"+x[i]+"']");
         return p;
      },
      /**
      * stores the valid path segments into paths
      * @param {*} p path the path segment
      * @param {*} v true if path is not null or empty and was added to store, else false
      * @returns
      */
      store: function(p, v) {
         if (p) P.result[P.result.length] = P.resultType == "PATH" ? P.asPath(p) : v;
         return !!p;
      },
      /**
      * parses the JSON path expression. depending upon the various path segment patterns, stores the path segments into paths
      * @param {*} expr
      * @param {*} val
      * @param {*} path
      */
      trace: function(expr, val, path) {
         if (expr !== "") {
            var x = expr.split(";"), loc = x.shift();
            x = x.join(";");
            if (val && val.hasOwnProperty(loc))
               P.trace(x, val[loc], path + ";" + loc);
            else if (loc === "*")
               P.walk(loc, x, val, path, function(m,l,x,v,p) { P.trace(m+";"+x,v,p); });
            else if (loc === "..") {
               P.trace(x, val, path);
               P.walk(loc, x, val, path, function(m,l,x,v,p) { typeof v[m] === "object" && P.trace("..;"+x,v[m],p+";"+m); });
            }
            else if (/^\(.*?\)$/.test(loc)) // [(expr)]
               P.trace(P.eval(loc, val, path.substr(path.lastIndexOf(";")+1))+";"+x, val, path);
            else if (/^\?\(.*?\)$/.test(loc)) // [?(expr)]
               P.walk(loc, x, val, path, function(m,l,x,v,p) { if (P.eval(l.replace(/^\?\((.*?)\)$/,"$1"), v instanceof Array ? v[m] : v, m)) P.trace(m+";"+x,v,p); }); // issue 5 resolved
            else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) // [start:end:step]  phyton slice syntax
               P.slice(loc, x, val, path);
            else if (/,/.test(loc)) { // [name1,name2,...]
               for (var s=loc.split(/'?,'?/),i=0,n=s.length; i<n; i++)
                  P.trace(s[i]+";"+x, val, path);
            }
         }
         else
            P.store(path, val);
      },
      walk: function(loc, expr, val, path, f) {
         if (val instanceof Array) {
            for (var i=0,n=val.length; i<n; i++)
               if (i in val)
                  f(i,loc,expr,val,path);
         }
         else if (typeof val === "object") {
            for (var m in val)
               if (val.hasOwnProperty(m))
                  f(m,loc,expr,val,path);
         }
      },
      slice: function(loc, expr, val, path) {
         if (val instanceof Array) {
            var len=val.length, start=0, end=len, step=1;
            loc.replace(/^(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$/g, function($0,$1,$2,$3){start=parseInt($1||start);end=parseInt($2||end);step=parseInt($3||step);});
            start = (start < 0) ? Math.max(0,start+len) : Math.min(len,start);
            end   = (end < 0)   ? Math.max(0,end+len)   : Math.min(len,end);
            for (var i=start; i<end; i+=step)
               P.trace(i+";"+expr, val, path);
         }
      },
      eval: function(x, _v, _vname) {
         try { return $ && _v && eval(x.replace(/(^|[^\\])@/g, "$1_v").replace(/\\@/g, "@")); }  // issue 7 : resolved ..
         catch(e) { throw new SyntaxError("jsonPath: " + e.message + ": " + x.replace(/(^|[^\\])@/g, "$1_v").replace(/\\@/g, "@")); }  // issue 7 : resolved ..
      }
   };

   var $ = obj;
   if (expr && obj && (P.resultType == "VALUE" || P.resultType == "PATH")) {
      P.trace(P.normalize(expr).replace(/^\$;?/,""), obj, "$");  // issue 6 resolved
      return P.result.length ? P.result : false;
   }
}