82

I have a JS object I would like to save in Local Storage for future use, and I cannot parse it to a string.

Code:

JSON.stringify({
    a: 5,
    b: function (param) {
        return param;
    }
})

Result:

"{"a":5}"

How do I save it for future use, if not with JSON?

(And creating my own Lexer-Parser to interupt string function I dont think is an option)

Amit
  • 5,924
  • 7
  • 46
  • 94
  • 1
    In the general case, you can't. One reason is that a function usually needs the enclosing scope where it can find some of the variables it uses. In very specific cases you can use the Function constructor. – Denys Séguret Apr 09 '16 at 13:10
  • 1
    *"(And creating my own Lexer-Parser to interupt string function I dont think is an option)"* Well, it **is** an option. Probably not a good option. – T.J. Crowder Apr 09 '16 at 13:12
  • 4
    This pretty much has to be an X/Y problem. Why would you need to store a function in local storage? – T.J. Crowder Apr 09 '16 at 13:13
  • @DenysSéguret and if it is (like above) does not use any out of scope parameters, can it be stored? – Amit Apr 09 '16 at 13:15
  • @T.J.Crowder I am using a table library, in which for every column I have a "cellRenderer" function, in which I manipulate the column's cells. You can change order, and delete columns, and I want to "save" the state of the table to create a new "view" for future use – Amit Apr 09 '16 at 13:17
  • JSON doesn't support JavaScript functions. It has a very limited scope of variable types it does support. Also I'm not sure if JavaScript even supports loading / dumping functions like that. I'd imagine you'd at the very minimum need to use something similar to Python's eval if you want dynamic function creation like that. As @TJCrowder said, I'd imagine there's a better solution to whatever it is you're trying to accomplish. – user161778 Apr 09 '16 at 13:21
  • @Amit: I'm not immediately seeing how that requires you to store functions...? – T.J. Crowder Apr 09 '16 at 13:26

7 Answers7

103

I'd recommend this approach:

Store arguments and the body in your json:

{"function":{"arguments":"a,b,c","body":"return a*b+c;"}}

Now parse json and instantiate the function:

var f = new Function(function.arguments, function.body);

I think it's save

Kostiantyn
  • 1,792
  • 2
  • 16
  • 21
  • 2
    This helped me on an almost irrelevant problem. Thanks! – Sanjay Bharathi Feb 05 '20 at 10:17
  • 1
    destructuring is also great for this too! This saved me a lot of headaches as well! – Gareth Compton Jul 12 '20 at 00:41
  • 1
    It's save the json way but not save for security reasons. new Function always creates a function in global scope. That's at least a security risk. – Kai Lehmann Mar 28 '21 at 07:48
  • 1
    +1 for use of the [Function constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function). It’s also pretty clearly separated if need be. – dakab Jul 01 '22 at 20:14
72

Usually a question like this indicates an X/Y problem: You need to do X, you think Y will help you do that, so you try to do Y, can't, and ask how to do Y. It would frequently be more useful to ask how to do X instead.

But answering the question asked: You could use replacer and reviver functions to convert the function to a string (during stringify) and back into a function (during parse) to store a string version of the function, but there are all sorts of issues with doing that, not least that the scope in which the function is defined may well matter to the function. (It doesn't matter to the function you've shown in the question, but I assume that's not really representative.) And converting a string from local storage into code you may run means that you are trusting that the local storage content hasn't been corrupted in a malicious way. Granted it's not likely unless the page is already vulnerable to XSS attacks, but it's an issue to keep in mind.

Here's an example, but I don't recommend it unless other options have been exhausted, not least because it uses eval, which (like its close cousin new Function)) can be a vector for malicious code:

// The object
var obj = {
    a: 5,
    b: function (param) {
        return param;
    }
};

// Convert to JSON using a replacer function to output
// the string version of a function with /Function(
// in front and )/ at the end.
var json = JSON.stringify(obj, function(key, value) {
  if (typeof value === "function") {
    return "/Function(" + value.toString() + ")/";
  }
  return value;
});

// Convert to an object using a reviver function that
// recognizes the /Function(...)/ value and converts it
// into a function via -shudder- `eval`.
var obj2 = JSON.parse(json, function(key, value) {
  if (typeof value === "string" &&
      value.startsWith("/Function(") &&
      value.endsWith(")/")) {
    value = value.substring(10, value.length - 2);
    return (0, eval)("(" + value + ")");
  }
  return value;
});
document.body.innerHTML = obj2.b(42);

The construct (0, eval)("(" + value + ")"); ensures that eval runs at global scope rather than within the scope of the reviver function. Normally eval has a magic ability to use the scope you call it in, but that only works when you call it directly. Indirect eval as shown (or just var e = eval; e("(" + value + ")");) doesn't have that magic ability, it runs at global scope.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Not likely? Eh, probably more likely than we'd think if the website is vulnerable anywhere to XSS. I certainly hope the asker reconsiders his approach. – user161778 Apr 09 '16 at 13:31
  • 1
    @user161778: If the site is *already* vulnerable to XSS, I don't think this makes it any worse. If it isn't, as local storage is tied to the origin, I don't think this opens a new door. But I agree: I wouldn't do it. – T.J. Crowder Apr 09 '16 at 13:33
  • Thanks you very much, this solution works! This project is not widely accessible, it is only for my company, I belive that my project is XSS vulnerable, but this does not open new doors as far as I understand. If I would store the functions in a database, sure, someone can inject code to everyone. – Amit Apr 09 '16 at 13:36
  • Well it depends on the website, I've seen sites that display random user content get exploited. That effectively means some users are potentially persistently exploited on that website as opposed to just when they visit that page (as the standard webpage code is the one evaling arbitrary JS). I guess it'd be hard to know without knowing OP's application. – user161778 Apr 09 '16 at 13:38
  • 1
    A bit necro, but instead of using `eval`, you can pass the string to `Function(`+value.substring()+`)`. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions#The_Function_constructor The function constructor does have a more limited scope to prevent some `eval` attacks – Kremnari May 20 '20 at 12:48
  • @Kremnari - Yes, I mention `new Function` in the answer. *Slightly* less scope, but fundamentally, we're still talking arbitrary code execution. :-) (I'd probably use indirect `eval` to restrict the scope: `(0, eval)("(" + value + ")");`...and probably should have above! ...and now I have, thanks!) – T.J. Crowder May 20 '20 at 12:54
  • I like your comment for showing up security risk but not saying dump things like "eval is eval". There a several solutions for scoping like call, bind or even the prototype chain. – Kai Lehmann Mar 28 '21 at 07:46
19

You can't store functions in JSON.

The value in JSON may contain only string, number, object, array, true, false or null:

enter image description here

Check out it on JSON site.

denysdovhan
  • 908
  • 8
  • 21
  • Is there a way though, to convert a function to a string and backwards? – Amit Apr 09 '16 at 13:20
  • 13
    "There is no way to store functions in JSON." — That isn't true. That just shows there is no native function data type. A function could be expressed as a string. – Quentin Apr 09 '16 at 13:21
  • 25
    @denysdovhan — Oh, it isn't, but "terrible idea" is a long way from "impossible" – Quentin Apr 09 '16 at 13:23
  • @Amit yes, you can do this using `(function () { ... }).toString()`, but it isn't good way. Maybe you have to think a little bit more about your case to solve that without using of functions. – denysdovhan Apr 09 '16 at 13:24
  • 8
    This answer is technically correct IMO. You're not storing a function, you're storing a string which can be evaluated to a function. – user161778 Apr 09 '16 at 13:33
  • @Quentin It's not a terrible idea. If you want an easy way to provide things like macros etc for users json is an absolutely great idea. And it's absolutely not impossible. Write a simple tokenizer and parser and *bing* you can easily parse every string into a function and backwards. That's what the engine is dooing everytime you load an js file ;) – Kai Lehmann Mar 28 '21 at 07:51
6

One simple way of doing this is

var dstr = JSON.stringify( { a: 5
                           , b: x => x
                           }
                         , (k,v) => typeof v === "function" ? "" + v : v
                         );
Redu
  • 25,060
  • 6
  • 56
  • 76
3

I've taken to storing the function name, along with the parameter values, in an array, with the first item in the array being the function name prepended with a $, to separate them from normal arrays.

{
    "object": {
        "your-function": ["$functionName", "param-1", "param-2"],
        "color": ["$getColor", "brand", "brand-2"],
        "normal-array": ["normal", "array"]
        ...
    }
}

In the above example I have Sass and JS functions to retrieve color values from a global map/object. Parsing the function in this manner naturally requires custom code, but in terms of "storing" functions in JSON, I like this way of doing it.

ESR
  • 1,669
  • 1
  • 18
  • 22
  • "Parsing the function in this manner naturally requires custom code": is there an example of such custom code around somewhere? – phhu Apr 16 '22 at 13:52
  • 1
    @phhu once upon a time I was doing this, took me a while to find, but I believe this is an example of such a thing https://github.com/esr360/One-Nexus/blob/007d0f68ebbd06b7491cba9ba931dc84b1dc8893/src/tools/scss/_eval-config.scss – ESR Apr 20 '22 at 16:08
  • thanks for looking this out! I posted a question on this eventually: see https://stackoverflow.com/questions/71912731/substitution-of-function-names-and-parameters-for-function-output-in-json-like-o for the suggestion of using a JSON.parse reviver function. – phhu Apr 20 '22 at 16:59
3

I have created JSON.parseIt() and JSON.stringifyIt() functions based on the first answer without using eval

JSON.stringifyIt = (obj)=>{
    return(
        JSON.stringify(obj, function(key, value) {
            if (typeof value === "function") {
                return "/Function(" + value.toString() + ")/";
            }
            if(typeof value === "string"){
                return "/String(" + value.toString() + ")/"
            }
            return value;
        })
    )
}
JSON.parseIt=(json)=>{
    return(
        JSON.parse(json, function(key, value) {
            if (typeof value === "string" &&
            value.startsWith("/Function(") &&
            value.endsWith(")/")) {
                value = value.substring(10, value.length - 2);
                var string = value.slice(value.indexOf("(") + 1, value.indexOf(")"));
                if(/\S+/g.test(string)){
                    return (new Function(string,value.slice(value.indexOf("{") + 1, value.lastIndexOf("}"))))

                }else{
                    return (new Function(value.slice(value.indexOf("{") + 1, value.lastIndexOf("}"))));
                }
                
            }
            if (typeof value === "string" &&
            value.startsWith("/String(") &&
            value.endsWith(")/")){
                value = value.substring(8, value.length - 2);
            }
            return value;
        })
    )
}

// DEMO

var obj = {
    string:"a string",
    number:10,
    func:()=>{
        console.log("this is a string from a parsed json function");
    },
    secFunc:(none,ntwo)=>{console.log(none + ntwo)} ,
    confuse:"/Function(hello)/"
}
const stringifiedObj = JSON.stringifyIt(obj);
console.log("the stringified object is: ",stringifiedObj);

const parsedObj = JSON.parseIt(stringifiedObj);

// console.log("the parsed object is:  ",parsedObj);
console.log(parsedObj.string);
console.log(parsedObj.number);
console.log(parsedObj.confuse);
parsedObj.func();
parsedObj.secFunc(5,6);

The problems I fixed were

  • Removed eval.
  • there was a problem in the stringifying and parsing that if I give a string like "/Function(hello)/" will be a function when parsed
  • Made it to two functions
  • Added parameter insertation
Musafiroon
  • 623
  • 6
  • 20
  • 1
    I think `new Function` is considered dangerous in similar ways to `eval` (as metntioned in T.J. Crowder's answer above) - I'm not sure that this solution removes the code injection concern (entirely). – phhu Apr 16 '22 at 13:54
0

For someone that still need include, for whatever reason, the function definition in JSON, this code can help (but can be slow depending object size):

function Object2JsonWithFunctions(o, space = null) {
var functionList = {}
var fnSeq = 0;

var snrepl = function(k,v){
    if(typeof v === 'function'){
        fnSeq++;
        var funcName = `___fun${fnSeq}___`;
        var funcText = ''+v;
        
        functionList[funcName] = funcText
        
        return funcName;
    }
    
    return v;
}

var RawJson = JSON.stringify(o, snrepl, space);

for(func in functionList){
    var PropValue = `"${func}"`;
    RawJson = RawJson.replace(PropValue, functionList[func])
}

return RawJson;}

The code will do the normal convert to JSON. For functions, the original stringify will return as "prop":"function()..." (function as a string)... The code above will create a placeholder (e.g: "prop":"fn1") and create a function list... After, will replace every placeholder to original function body...

Rodrigo
  • 92
  • 6