Thursday, October 10, 2013

Pass JavaScript function via JSON. Pitfall and solution.

JSON is a lightweight data-interchange format. It is well readable and writable for humans and it is easy for machines to parse and generate. The most of JavaScript libraries, frameworks, plugins and whatever are configurable by options in JSON format. Sometimes you have to parse a JSON string (text) to get a real JavaScript object. For instance, sometimes you have to read the whole JSON structure from a file and make it available on the client-side as an JavaScript object. The JSON text should be well-formed. Passing a malformed JSON text results in a JavaScript exception being thrown.

What does "well-formed" mean? A well-formed JSON structure consists of data types string, number, object, array, boolean or null. Other data types are not allowed. An example:
{
    "name": "Max",
    "address": {
        "street": "Big Avenue 5",
        "zipcode": 12345,
        "country": "USA"
    },
    "age": 35,
    "married": true,
    "children": ["Mike", "John"]
}
You see here string values for keys "name", "street" and "country", number values for "zipcode" and "age", boolean value for "married", object for "address" and array for "children". But what is about JavaScript functions? Some scripts allow to pass functions as values. For instance as in some chart libraries:
{
    "margin": "2px",
    "colors": ["#FFFFFF", "CCCCCC"],
    "labelFormatter": function(value, axis) {return value + ' degree';}
}
If you try to parse this JSON text, you will face an error because function(value, axis) {return value + ' degree';} is not allowed here. Try to put this structure into this online JSON viewer to see that JSON format doesn't accept functions. So, the idea is to pass the function as string:
{
    "borderWidth": "2px",
    "colors": ["#FFFFFF", "CCCCCC"],
    "labelFormatter": "function(value, axis) {return value + ' degree';}"
}
This syntax can be parsed. But we have another problem now. We have to make a real JavaScript function from its string representation. After trying a lot I found an easy solution. Normally, to parse a well-formed JSON string and return the resulting JavaScript object, you can use a native JSON.parse(...) method (implemented in JavaScript 1.7), JSON.parse(...) from json2.js written by Douglas Crockford or the jQuery's $.parseJSON(...). The jQuery's method only expects one parameter, the JSON text: $.parseJSON(jsonText). So, you can not modify any values with it. The first two mentioned methods have two parameters
 
JSON.parse(text, reviver)
 
The second parameter is optional, but exactly this parameter will help us! reviver is a function, prescribes how the value originally produced by parsing is transformed, before being returned. More precise: the reviver function can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. An example:
var transformed = JSON.parse('{"p": 5}', function(k, v) {if (k === "") return v; return v * 2;});

// The object transformed is {p: 10}
In our case, the trick is to use eval in the reviver to replace the string value by the corresponding function object:
var jsonText = ".....";  // got from any source

var jsonTransformed = JSON.parse(jsonText, function (key, value) {
    if (value && (typeof value === 'string') && value.indexOf("function") === 0) {
        // we can only pass a function as string in JSON ==> doing a real function
        eval("var jsFunc = " + value);
        return jsFunc;
    }
         
    return value;
});
I know, eval is evil, but it doesn't hurt here. Well, who doesn't like eval there is another solution without it.
var jsonTransformed = JSON.parse(jsonText, function (key, value) {
    if (value && (typeof value === 'string') && value.indexOf("function") === 0) {
        // we can only pass a function as string in JSON ==> doing a real function
        var jsFunc = new Function('return ' + value)();
        return jsFunc;
    }
         
    return value;
});

6 comments:

  1. Thank you!!! I'm a mechanical engineer and really not a coder but have been trying to hack something together using javascript variables available on a site I don't have server side access to and this was the missing piece!

    ReplyDelete
  2. Thanks! This helped so much.

    ReplyDelete
  3. Thanks for this in depth example! I thought the reviver argument might be the solution but I couldn't figure out how to make it work until I came across your blog.

    ReplyDelete
  4. Brilliant. This saved my day. I needed to do some tricky string parsing from JSON and this helped out immensely.

    ReplyDelete

Note: Only a member of this blog may post a comment.