How to Store a JavaScript Function in JSON: Fixing Stringify Issues for Local Storage
JSON (JavaScript Object Notation) is the go-to format for data interchange in web applications, prized for its simplicity and compatibility with nearly all programming languages. When combined with localStorage or sessionStorage, it becomes a powerful tool for persisting data client-side. However, JSON has a critical limitation: it cannot natively serialize JavaScript functions. This poses a problem when you need to store objects that include functions—for example, user preferences with custom validation logic, state objects with computed properties, or configuration data with dynamic behavior.
In this blog, we’ll explore why JSON struggles with functions, common workarounds (and their flaws), and robust solutions to safely store and retrieve functions in JSON, with a focus on practical use cases like localStorage. By the end, you’ll be equipped to handle function serialization like a pro, while avoiding security risks and data loss.
Table of Contents#
- Understanding the Problem: Why JSON Can’t Stringify Functions
- Common Workarounds and Their Limitations
- The Right Way: Serializing Functions for JSON Storage
- Practical Example: Storing a Function in Local Storage
- Security Considerations: The Risks of Executing Stored Code
- When to Avoid Storing Functions in JSON
- Conclusion
- References
1. Understanding the Problem: Why JSON Can’t Stringify Functions#
To grasp why functions are problematic for JSON, let’s start with how JSON.stringify() works. JSON only supports a narrow set of data types: strings, numbers, booleans, null, arrays, and plain objects. Functions, along with other JavaScript-specific types like Date, RegExp, Map, or Set, are not part of the JSON specification.
When JSON.stringify() encounters a function in an object, it ignores the function entirely (or converts it to null, depending on the context). Let’s test this with a simple example:
const userSettings = {
theme: "dark",
validateInput: (input) => input.length > 5 // A function we want to store
};
// Try to stringify the object
const jsonString = JSON.stringify(userSettings);
console.log(jsonString);
// Output: {"theme":"dark"} (the validateInput function is missing!)As you can see, the validateInput function is excluded from the serialized JSON. This is because JSON.stringify() skips non-serializable values like functions, Symbols, and undefined.
2. Common Workarounds and Their Limitations#
Developers often try quick fixes to store functions in JSON. Let’s explore these workarounds and why they’re risky or unreliable.
Workaround 1: Convert the Function to a String with toString()#
A naive approach is to manually convert the function to a string using Function.prototype.toString(), store the string in JSON, and later recreate the function with eval() or new Function().
Example:
// Serialize: Convert function to string
const userSettings = {
theme: "dark",
validateInput: (input) => input.length > 5
};
userSettings.validateInput = userSettings.validateInput.toString();
// Store in localStorage
localStorage.setItem("settings", JSON.stringify(userSettings));
// Retrieve and deserialize: Use eval() to recreate the function
const storedSettings = JSON.parse(localStorage.getItem("settings"));
storedSettings.validateInput = eval(`(${storedSettings.validateInput})`);
// Test the function
console.log(storedSettings.validateInput("hello")); // false (5 characters)
console.log(storedSettings.validateInput("hello123")); // true (8 characters)Limitations:
- Security Risks:
eval()executes arbitrary code, making your app vulnerable to cross-site scripting (XSS) attacks if the stored data is tampered with. - Loss of Context: Functions relying on closures (e.g., referencing external variables) will fail, as
eval()recreates the function in a global context, not the original scope. - Incomplete Serialization: Some functions (e.g., async functions, generator functions, or functions with
thisbindings) may not stringify correctly withtoString().
Workaround 2: Using JSON.stringify with a Replacer Function#
JSON.stringify accepts a second argument called a "replacer" function, which lets you customize serialization. You could use this to explicitly convert functions to strings:
const userSettings = {
theme: "dark",
validateInput: (input) => input.length > 5
};
// Use a replacer to convert functions to strings
const jsonString = JSON.stringify(userSettings, (key, value) => {
if (typeof value === "function") {
return value.toString(); // Convert function to string
}
return value;
});
// Now the JSON includes the function string
console.log(jsonString);
// Output: {"theme":"dark","validateInput":"(input) => input.length > 5"}This is slightly better than manual toString(), but it still suffers from the same issues as Workaround 1: security risks, context loss, and incomplete serialization.
3. The Right Way: Serializing Functions for JSON Storage#
To safely and reliably store functions in JSON, we need structured approaches that address the limitations of naive workarounds. Below are two robust methods.
3.1 Manual Serialization with toJSON and Revivers#
JSON supports custom serialization via the toJSON method. If an object defines a toJSON() function, JSON.stringify will use its return value instead of the object itself. We can leverage this to explicitly mark functions for serialization, then use a "reviver" function during parsing to recreate them.
Step 1: Define a toJSON Method for the Object#
Add a toJSON() method to your object to return a serializable version that includes function metadata. For example, wrap the function string in an object with a type field (e.g., "function") to flag it during parsing.
const userSettings = {
theme: "dark",
validateInput: (input) => input.length > 5,
// Custom toJSON method for serialization
toJSON() {
// Return a plain object with serializable values
return {
theme: this.theme,
// Explicitly serialize the function with a type marker
validateInput: {
type: "function",
value: this.validateInput.toString()
}
};
}
};Step 2: Use a Reviver Function During Parsing#
When parsing the JSON string back, use JSON.parse’s second argument: a "reviver" function. This function processes each key-value pair and can reconstruct non-serializable values like functions.
// Serialize and store in localStorage
const jsonString = JSON.stringify(userSettings);
localStorage.setItem("settings", jsonString);
// Retrieve and parse with a reviver to recreate functions
const storedSettings = JSON.parse(localStorage.getItem("settings"), (key, value) => {
// Check if the value is a serialized function
if (value && value.type === "function") {
// Recreate the function using new Function (safer than eval for simple cases)
return new Function(`return ${value.value}`)();
}
return value; // Return non-function values as-is
});
// Test the recreated function
console.log(storedSettings.validateInput("test")); // false (4 characters)
console.log(storedSettings.validateInput("longer")); // true (6 characters)Why This Works Better:#
- Explicit Intent: The
type: "function"marker makes it clear that the value is a serialized function, avoiding accidental execution of non-function strings. - Controlled Reconstruction: The reviver function ensures only marked values are converted to functions, reducing security risks (though not eliminating them entirely).
3.2 Using Libraries for Advanced Serialization#
For complex apps, manual serialization can become error-prone. Libraries like superjson or flatted extend JSON to support non-serializable types, including functions, dates, and even circular references.
Example with superjson#
superjson is a popular library that handles serialization of advanced JavaScript types out of the box.
Step 1: Install superjson
npm install superjsonStep 2: Serialize and Deserialize with superjson
import superjson from "superjson";
const userSettings = {
theme: "dark",
validateInput: (input) => input.length > 5
};
// Serialize (works with functions!)
const serialized = superjson.stringify(userSettings);
localStorage.setItem("settings", serialized);
// Deserialize (automatically recreates the function)
const deserialized = superjson.parse(localStorage.getItem("settings"));
console.log(deserialized.validateInput("hello")); // falsesuperjson handles function serialization by converting them to strings with metadata, then reconstructing them safely during parsing. It also supports async functions, generator functions, and closures (with caveats—closures may lose context).
4. Practical Example: Storing a Function in Local Storage#
Let’s walk through a complete example of storing a function in localStorage using the manual toJSON/reviver approach. We’ll create a "user preferences" object with a formatName function, serialize it, store it, and retrieve it.
Step 1: Define the Object with a Function#
const userPreferences = {
username: "alice",
formatName: (first, last) => `${last}, ${first}`, // Function to store
// Custom toJSON for serialization
toJSON() {
return {
username: this.username,
formatName: {
type: "function",
value: this.formatName.toString()
}
};
}
};Step 2: Serialize and Store in localStorage#
// Convert to JSON string
const jsonString = JSON.stringify(userPreferences);
// Store in localStorage
localStorage.setItem("userPrefs", jsonString);Step 3: Retrieve and Reconstruct the Function#
// Retrieve from localStorage
const storedJson = localStorage.getItem("userPrefs");
// Parse with a reviver to recreate the function
const retrievedPrefs = JSON.parse(storedJson, (key, value) => {
if (value && value.type === "function") {
// Recreate the function using new Function
return new Function(`return ${value.value}`)();
}
return value;
});Step 4: Test the Reconstructed Function#
console.log(retrievedPrefs.formatName("Alice", "Smith"));
// Output: "Smith, Alice" (function works!)5. Security Considerations: The Risks of Executing Stored Code#
Storing and reconstructing functions involves executing code from localStorage, which is vulnerable to tampering. Here’s what you need to know:
The Danger of eval() and new Function()#
Both eval() and new Function() execute arbitrary code. If an attacker modifies the stored function string (e.g., via XSS), they could run malicious code in your app. For example:
// Malicious function string stored in localStorage
const maliciousCode = `() => { stealUserData(); }`;
// If parsed with new Function, this executes the attack
const badFunction = new Function(`return ${maliciousCode}`)();
badFunction(); // Runs stealUserData()Mitigation Strategies#
-
Avoid Storing Untrusted Functions: Only store functions that are generated by your app (not user input).
-
Sanitize Function Strings: Validate the function string before execution (e.g., check for forbidden keywords like
fetchoreval). -
Use Function References Instead of Code: If possible, store a name or identifier for the function (e.g.,
"formatNameV2") and map it to a predefined function in your code:// Store a reference instead of the function itself const userPreferences = { username: "alice", formatName: "fullNameFormatter" }; localStorage.setItem("prefs", JSON.stringify(userPreferences)); // Predefined functions in your app const formatters = { fullNameFormatter: (first, last) => `${last}, ${first}` }; // Retrieve and use the reference const prefs = JSON.parse(localStorage.getItem("prefs")); const formatter = formatters[prefs.formatName]; // Look up the function
6. When to Avoid Storing Functions in JSON#
Storing functions in JSON is powerful but not always necessary. Avoid it in these scenarios:
1. The Function Relies on Closures#
Functions that depend on external variables (closures) will lose their context when serialized. For example:
const taxRate = 0.08;
const calculateTax = (price) => price * taxRate;
// Serializing calculateTax and recreating it will lose access to taxRate!2. Security Is Critical#
If your app handles sensitive data (e.g., banking, healthcare), avoid storing executable code. Even with sanitization, the risk of XSS is too high.
3. The Function Is Large or Complex#
Storing large function strings bloats localStorage (which has a ~5MB limit) and slows down serialization/parsing.
4. A Declarative Alternative Exists#
Instead of storing a function like (x) => x > 18, store a configuration value like { minAge: 18 } and use a fixed function to enforce it:
// Better: Store data, not logic
const userSettings = { minAge: 18 };
// Use a fixed function to validate
const validateAge = (age, settings) => age > settings.minAge;7. Conclusion#
Storing JavaScript functions in JSON (and thus localStorage) is possible but requires careful handling. The key takeaways are:
- JSON natively ignores functions, so you need custom serialization.
- Naive workarounds (e.g.,
eval()+toString()) are insecure and error-prone. - Manual serialization with
toJSONand revivers gives you control but requires explicit handling. - Libraries like
superjsonsimplify serialization for complex types. - Security first: Avoid storing untrusted functions, and prefer declarative data over executable code when possible.
By following these guidelines, you can safely persist functions client-side while avoiding common pitfalls.