telejson
TeleJSON serializes JavaScript values that plain JSON loses, including cyclic references, Date, RegExp, Error, BigInt, undefined, Symbol, NaN and infinities. It produces a JSON string containing tagged sentinel values, then rebuilds supported values during parsing. Storybook uses this kind of transport for rich data between contexts. Version 8 no longer serializes functions, and class instances return as plain records rather than working instances.
Useful for short-lived JavaScript-only transport when cycles and debugging values matter, especially around Storybook tooling. Do not adopt it from the README alone: version 8 removed functions, truncates deep data by default, and does not give classes or arbitrary tagged strings a lossless round trip.
Use it if
- You need to send Storybook-style diagnostic or control data containing cycles, errors, dates and undefined values between JavaScript contexts
- You want a stringify and parse pair that preserves repeated object identity as well as direct cycles
- You need CommonJS and ESM builds plus bundled TypeScript declarations with no required runtime dependencies
- You can keep both ends on the same TeleJSON contract and do not need another language to interpret the wire format
- You need to serialize functions: version 8 deliberately removes function support even though the current README still claims functions are stringified and evaluated
- You need real class, Map, Set or typed-array round trips: class instances are reduced to own properties and metadata, their prototypes and methods are not restored, and collection internals are not a documented format
- Your payload can contain arbitrary strings that resemble TeleJSON tags such as _Infinity_, _undefined_ or _date_ followed by an ISO timestamp: the reviver interprets those prefixes as typed values
- You need a durable cross-language or database format: the output is valid JSON but its underscore-prefixed tags and duplicate-reference paths are a TeleJSON-specific protocol
- You cannot accept silent depth truncation: maxDepth defaults to 10 and deeper objects and arrays become placeholder strings rather than causing an error
Setup reality
Install telejson and import stringify and parse; there are no peer dependencies, native builds, credentials or config files. The published package supplies CommonJS, ESM and TypeScript declarations, but it has no exports map, so let your runtime choose main or module rather than importing files under dist. The important setup is a data contract. Both ends should use the same major version and matching allowDate, allowRegExp, allowError, allowUndefined and allowSymbol decisions. maxDepth defaults to 10, where deeper objects become "[Object]" and arrays become "[Array(length)]". Raise it deliberately for trusted bounded data, not blindly for huge inspector objects. Version 8 removes function serialization entirely: function-valued properties disappear during stringify, and the old allowFunction option shown in the README is absent from the shipped Options type. Class instances are copied to plain data with class-name metadata, but parse does not recreate the constructor prototype or methods. Errors are recreated as Error objects with copied name, message, stack, cause and enumerable properties, not as the original subclass. Local symbols come back as new symbols, while Symbol.for values return through the global registry. The format encodes types as ordinary strings such as _NaN_, _bigint_123 and _duplicate_[...], so arbitrary user strings can collide with control tags. Treat TeleJSON as an internal JavaScript-to-JavaScript protocol, include your own envelope version, validate the parsed shape, and do not promise long-term storage compatibility without fixtures that lock the exact wire behavior. When using JSON.stringify directly, call replacer(options) and reviver(options); passing the factory functions themselves does not work.
Patterns
Round-trip supported rich valuesround-trip-rich-data
import { parse, stringify } from 'telejson'
const wire = stringify({
createdAt: new Date('2026-08-08T12:00:00Z'),
matcher: /invoice-\d+/i,
total: 12345678901234567890n,
})
const value = parse(wire)
console.log(value.createdAt instanceof Date)
console.log(value.matcher.test('INVOICE-42'))This is a TeleJSON-specific wire format, not language-neutral JSON semantics. Keep both sender and receiver on a tested contract.
Preserve a cyclic referencepreserve-cycles
const root = { name: 'root' }
root.self = root
const copy = parse(stringify(root))
console.log(copy.self === copy) // trueCycles are encoded as path references. Parse only payloads within your expected schema because the reference markers are part of the private protocol.
Preserve repeated object identitypreserve-shared-reference
const shared = { id: 42 }
const input = { first: shared, second: shared }
const output = parse(stringify(input))
console.log(output.first === output.second) // trueTeleJSON preserves identity for repeated objects, unlike duplicating both values through ordinary JSON. Do not rely on the exact encoded path text.
Carry error detailsserialize-error
const error = new Error('payment failed', { cause: { code: 'DECLINED' } })
error.requestId = 'req_123'
const restored = parse(stringify({ error })).error
console.log(restored instanceof Error)
console.log(restored.message, restored.cause, restored.requestId)The result is an Error with copied fields, not necessarily the original subclass. Stack traces may contain sensitive paths and should be filtered before transport.
Preserve NaN, infinities and BigIntpreserve-special-numbers
const input = {
invalid: Number.NaN,
high: Number.POSITIVE_INFINITY,
low: Number.NEGATIVE_INFINITY,
exact: 9007199254740993n,
}
const output = parse(stringify(input))
console.log(Number.isNaN(output.invalid), output.exact === input.exact)BigInt restoration requires a runtime with BigInt. Special values are represented with reserved strings inside the JSON payload.
Keep undefined object keys and array slotspreserve-undefined
const input = { optional: undefined, list: [1, undefined, 3] }
const output = parse(stringify(input))
console.log(Object.hasOwn(output, 'optional')) // true
console.log(output.optional === undefined) // true
console.log(output.list[1] === undefined) // trueTeleJSON uses the literal marker _undefined_ and mutates the parsed graph afterward. A real user string with that exact value will also become undefined.
Round-trip local and global symbolsserialize-symbols
const local = Symbol('draft')
const global = Symbol.for('app.status')
const output = parse(stringify({ local, global }))
console.log(output.local.description) // draft
console.log(output.local === local) // false
console.log(output.global === Symbol.for('app.status')) // trueA local symbol is recreated and loses identity. Global symbols use Symbol.for, which shares registry keys across the current JavaScript realm.
Bound inspector payload depthlimit-depth
const wire = stringify(largeDiagnosticObject, { maxDepth: 5 })
const preview = parse(wire)Objects beyond the limit become [Object] and arrays become [Array(length)]. The default limit is 10, and truncation does not throw.
Produce indented outputpretty-print
const text = stringify({ date: new Date('2026-08-08T12:00:00Z') }, {
space: 2,
})
console.log(text)Indentation increases payload size and is meant for logs or fixtures. It does not make the internal type tags portable to other parsers.
Disable selected type conversionsdisable-rich-types
const options = {
allowDate: false,
allowRegExp: false,
allowError: false,
allowUndefined: false,
allowSymbol: false,
}
const text = stringify(input, options)
const output = parse(text, options)Use the same options on both sides. Disallowed properties may disappear rather than produce an error, so validate the resulting shape.
Use the replacer and reviver factoriesuse-json-hooks
import { replacer, reviver } from 'telejson'
const options = {
maxDepth: 10,
space: undefined,
allowRegExp: true,
allowSymbol: true,
allowDate: true,
allowUndefined: true,
allowError: true,
}
const text = JSON.stringify(input, replacer(options), 2)
const output = JSON.parse(text, reviver(options))Both exports are factories and must be called. parse() also performs an extra pass that restores undefined values, so direct JSON.parse plus reviver is not identical for that case.
Convert a class instance to data deliberatelynormalize-class-instance
class User {
constructor(id, name) {
this.id = id
this.name = name
}
label() { return `${this.id}: ${this.name}` }
}
const parsed = parse(stringify(new User(7, 'Ada')))
console.log(parsed.id, parsed.name)
console.log(parsed instanceof User) // falseVersion 8 keeps own data properties but does not restore the prototype or methods. Prefer an explicit toDTO function when the receiver needs a stable schema.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| superjson | npm | You want a popular typed JSON layer for application data, including dates, maps, sets and custom transformers |
| devalue | npm | You need compact serialization for rich JavaScript values and server-to-client hydration with an explicit safety model |
| flatted | npm | Cycles and repeated references are the main requirement and extra Date, Error or Symbol handling is unnecessary |
| serialize-javascript | npm | You specifically need JavaScript source serialization and understand the security and content-policy consequences |