devalue

repository·main·Indexed 25 days ago

https://github.com/sveltejs/devalue

A high-performance serialization library that handles complex JavaScript types that standard JSON cannot, including Maps, Sets, BigInt, RegExp, Date, ArrayBuffer, TypedArray, URL, Temporal, and cyclical references. It provides functions like stringify, parse, and stringifyAsync for Promises, as well as uneval for generating JavaScript code. Designed for security with built-in XSS mitigation and support for custom types via reducers and revivers.

Tokens
6.7K
Snippets
14
Records
33
Agent score
81%

What's inside devalue

  1. Mitigate XSS when serializing for server-side rendering

    main

    When serializing state to be embedded in an HTML <script> tag, JSON.stringify is vulnerable to XSS attacks if the data contains strings like </script>. devalue.stringify and devalue.uneval automatically escape these characters (e.g., converting < to \u003C), making them safe for use in templates.

    // Safe way to embed state in a template
    const template = `
    <script>
      var preloaded = ${uneval(state)};
    </script>`;
  2. Serialize and deserialize custom types

    main

    You can handle custom classes by providing a mapping of type names to reducers in stringify, and a mapping of type names to revivers in parse or unflatten.

    • A reducer is a function that, if it returns a truthy value, tells devalue how to represent the value (e.g., as an array).
    • A reviver is a function that takes the serialized representation and returns a new instance of the custom type.
    class Vector {
    	constructor(x, y) {
    		this.x = x;
    		y = y;
    	}
    
    	magnitude() {
    		return Math.sqrt(this.x * this.x + this.y * this.y);
    	}
    }
    
    const stringified = devalue.stringify(new Vector(30, 40), {
    	Vector: (value) => value instanceof Vector && [value.x, value.y]
    });
    
    console.log(stringified); // [["Vector",1],[2,3],30,40]
    
    const vector = devalue.parse(stringified, {
    	Vector: ([x, y]) => new Vector(x, y)
    });
    
    console.log(vector.magnitude()); // 50
  3. Handle errors and locate offending values

    main

    If uneval or stringify encounters a function or a non-POJO that is not handled by a custom reducer, it will throw an error. The error object contains a path property that indicates the location of the offending value within the input structure.

    try {
    	const map = new Map();
    	map.set('key', function invalid() {});
    
    	uneval({
    		object: {
    			array: [map]
    		}
    	});
    } catch (e) {
    	console.log(e.path); // '.object.array[0].get("key")'
    }
  4. Use `uneval` to generate JavaScript code for a value

    main

    The uneval function takes a JavaScript value and returns a string containing the JavaScript code required to recreate that value. This is useful for generating the most compact output possible without requiring a separate parsing step. Note that any variables referenced in the generated code must be in scope when the code is executed.

    import * as devalue from 'devalue';
    
    let obj = { message: 'hello' };
    devalue.uneval(obj); // '{message:"hello"}'
    
    obj.self = obj;
    devalue.uneval(obj); // '(function(a){a.message="hello";a.self=a;return a}({}))'
  5. Serialize promises with `stringifyAsync`

    main

    stringifyAsync is an asynchronous version of stringify that can handle Promise objects. It awaits the promises and serializes their resolved values. The resulting output format is identical to the standard stringify output, meaning parse and unflatten can be used to deserialize the result.

    import * as devalue from 'devalue';
    
    let obj = {
    	quick: 'data',
    	slow: fetch('/api/slow').then((r) => r.json())
    };
    
    let stringified = await devalue.stringifyAsync(obj);
    devalue.parse(stringified); // { quick: 'data', slow: { ... } }
  6. Revive partial data with `unflatten`

    main

    If devalued data is embedded within a larger JSON string, use unflatten to revive only the specific portion of the data you need.

    import * as devalue from 'devalue';
    
    const json = `{
      "type": "data",
      "data": ${devalue.stringify(data)}
    }`;
    
    const data = devalue.unflatten(JSON.parse(json).data);
  7. Serialize and deserialize with `stringify` and `parse`

    main

    The stringify and parse functions are analogous to JSON.stringify and JSON.parse, but they support a much wider range of types including cyclical references, Map, Set, BigInt, RegExp, Date, ArrayBuffer, TypedArray, URL, and Temporal. Use these when evaluating JavaScript (via uneval) is not an option.

    import * as devalue from 'devalue';
    
    let obj = { message: 'hello' };
    
    let stringified = devalue.stringify(obj); // '[{"message":1},"hello"]'
    devalue.parse(stringified); // { message: 'hello' }
    
    obj.self = obj;
    
    stringified = devalue.stringify(obj); // '[{"message":1,"self":0},"hello"]'
    devalue.parse(stringified); // { message: 'hello', self: [Circular] }
  8. Override serialization and deserialization operations

    main

    You can customize how devalue interacts with values by providing an operations object. This is useful for:

    1. Side-effect-free serialization: Replacing prototype methods (like Date.prototype.toISOString) or property access (via get) to avoid executing user code or getters.
    2. Foreign-runtime serialization/revival: Implementing typeOf, get, set, etc., to allow devalue to inspect and manipulate values living in other environments (like a node:vm context or WASM) without touching them directly in the current runtime.
    // Example: Side-effect-free serialization
    const originalToISOString = Date.prototype.toISOString;
    
    const stringified = devalue.stringify(value, undefined, {
    	operations: {
    		// use a captured intrinsic instead of a (possibly patched) prototype method
    		toISOString: (date) => originalToISOString.call(date),
    
    		// read through descriptors so getters are never invoked
    		get: (object, key) => {
    			const descriptor = Object.getOwnPropertyDescriptor(object, key);
    			if (descriptor?.get) throw new Error(`refusing to invoke getter for "${key}"`);
    			return descriptor?.value;
    		}
    	}
    });
  9. Handle errors during parsing

    main

    The parse and unflatten functions may throw errors in the following scenarios:

    • Invalid input: Thrown if the input is not a valid number or array, or if array lengths/indices are invalid.
    • Invalid circular reference: Thrown if the input contains an invalid circular dependency that cannot be resolved.
    • Cannot parse an object with a __proto__ property: Thrown if an object in the serialized data attempts to define a __proto__ key, as a security measure.
    • Unknown type: Thrown if the serialized data contains a type tag that is not recognized by the parser.
    • Invalid ArrayBuffer encoding: Thrown if an ArrayBuffer is expected but the encoding is not a string.
    • Invalid data: Thrown if the structure of typed array data is malformed.
  10. Handle serialization errors

    main

    Devalue throws DevalueError in several scenarios:

    • Attempting to stringify a function.
    • Attempting to stringify a Symbol primitive.
    • Attempting to use stringify (synchronous) on a Promise or thenable.
    • Attempting to stringify arbitrary non-POJOs (Plain Old JavaScript Objects).
    • Attempting to stringify POJOs with symbolic keys.
    • Attempting to stringify objects that contain __proto__ keys.
  11. Customize deserialization with defaultParseOperations

    main
    Access defaultParseOperations to extend or modify how specific types are parsed from a string. This allows you to define custom logic for reconstructing specific types.