TL;DR
JSON.stringify loses type information — Date becomes a string, undefined disappears, Set and Map become empty objects. superjson is the most popular solution, preserving all JS types and designed for client-server data transfer (used heavily with tRPC and Next.js). devalue is the Svelte/SvelteKit library — handles circular references and is slightly smaller. flatted focuses specifically on circular reference support with a minimal API. For tRPC or Next.js Server Actions, use superjson. For serializing recursive data structures, use devalue or flatted.
Key Takeaways
- superjson: ~3M weekly downloads — preserves Date, Set, Map, undefined, BigInt, RegExp, and more
- devalue: ~1.5M weekly downloads — handles circular/repeated references, used by SvelteKit
- flatted: ~8M weekly downloads — lightweight, circular JSON only, 0.5KB
JSON.stringify(new Date())→ a string,JSON.parsegives you a string back, not a Date- superjson solves this by sending metadata alongside the serialized value
- Use superjson with tRPC — it's the default serializer when you need type preservation
Download Trends
| Package | Weekly Downloads | Types Preserved | Circular Refs | Bundle Size |
|---|---|---|---|---|
superjson | ~3M | ✅ All JS types | ✅ | ~9KB |
devalue | ~1.5M | ✅ Most | ✅ Excellent | ~5KB |
flatted | ~8M | ❌ JSON only | ✅ | ~0.5KB |
The Problem: What JSON.stringify Loses
const original = {
date: new Date("2026-03-09"),
set: new Set([1, 2, 3]),
map: new Map([["key", "value"]]),
undefinedValue: undefined,
bigint: 9007199254740993n,
regexp: /pattern/gi,
nan: NaN,
infinity: Infinity,
nested: {
circular: null as any,
},
}
// Create circular reference:
original.nested.circular = original
// JSON.stringify behavior:
const json = JSON.stringify({
date: new Date("2026-03-09"), // "2026-03-09T00:00:00.000Z" — string, not Date
set: new Set([1, 2, 3]), // {} — empty object!
map: new Map([["key", "value"]]), // {} — empty object!
undefinedValue: undefined, // Field is REMOVED from output
bigint: 9007199254740993n, // TypeError: Cannot serialize BigInt
regexp: /pattern/gi, // {} — empty object!
nan: NaN, // null
infinity: Infinity, // null
// circular: would throw "Converting circular structure to JSON"
})
superjson
superjson preserves the original type information by serializing both the value and metadata about its type.
Basic usage
import superjson from "superjson"
const data = {
createdAt: new Date("2026-03-09T10:00:00Z"),
tags: new Set(["react", "typescript", "npm"]),
metadata: new Map([["version", "18.0.0"], ["downloads", "25M"]]),
undefinedField: undefined,
bigCount: 9007199254740993n,
pattern: /npm-package-[\w-]+/gi,
}
// Serialize:
const { json, meta } = superjson.serialize(data)
// json: { createdAt: "2026-03-09T10:00:00.000Z", tags: ["react","typescript","npm"], ... }
// meta: { values: { createdAt: ["Date"], tags: ["set"], metadata: ["map"], ... } }
// Round-trip through string:
const stringified = superjson.stringify(data)
const restored = superjson.parse<typeof data>(stringified)
// All types preserved:
console.log(restored.createdAt instanceof Date) // true
console.log(restored.tags instanceof Set) // true
console.log(restored.tags.has("react")) // true
console.log(restored.metadata instanceof Map) // true
console.log(restored.bigCount === 9007199254740993n) // true
console.log(restored.undefinedField === undefined) // true
With tRPC (superjson as transformer)
// server/trpc.ts:
import { initTRPC } from "@trpc/server"
import superjson from "superjson"
const t = initTRPC.context<Context>().create({
transformer: superjson, // Preserves Date, Set, Map across network boundary
})
export const router = t.router
export const publicProcedure = t.procedure
// server/router.ts:
const appRouter = router({
getPackageData: publicProcedure
.input(z.object({ name: z.string() }))
.query(async ({ input }) => {
return {
name: input.name,
publishedAt: new Date("2020-01-01"), // Will arrive as Date on client
tags: new Set(["ui", "popular"]), // Will arrive as Set on client
downloads: new Map([["2026", 25000000n]]), // BigInt preserved
}
}),
})
// client:
const data = await trpc.getPackageData.query({ name: "react" })
console.log(data.publishedAt instanceof Date) // true — not a string!
console.log(data.tags instanceof Set) // true
With Next.js Server Actions
// app/actions.ts
"use server"
import superjson from "superjson"
export async function getPackageStats(name: string) {
const data = await fetchFromDB(name)
// Server-to-client: serialize with superjson
return superjson.serialize({
fetchedAt: new Date(),
downloads: new Map(Object.entries(data.monthlyDownloads)),
tags: new Set(data.tags),
})
}
// app/page.tsx
"use client"
import superjson from "superjson"
import { getPackageStats } from "./actions"
export default function Page() {
const handleFetch = async () => {
const serialized = await getPackageStats("react")
const data = superjson.deserialize(serialized)
// data.fetchedAt is a Date, data.downloads is a Map
}
}
Custom serializers
import superjson from "superjson"
import Decimal from "decimal.js"
// Register custom type serializer:
superjson.registerCustom<Decimal, string>(
{
isApplicable: (v): v is Decimal => v instanceof Decimal,
serialize: (v) => v.toString(),
deserialize: (v) => new Decimal(v),
},
"decimal.js"
)
const data = {
price: new Decimal("1234.5678"),
total: new Decimal("999999.99"),
}
const str = superjson.stringify(data)
const restored = superjson.parse<typeof data>(str)
console.log(restored.price instanceof Decimal) // true
console.log(restored.price.toString()) // "1234.5678"
devalue
devalue is the serializer used by SvelteKit and Svelte's data loading — handles circular references and repeated values efficiently.
Basic usage
import * as devalue from "devalue"
// Simple serialization:
const data = { name: "react", downloads: 25000000 }
const str = devalue.stringify(data)
const restored = devalue.parse(str)
// Handles circular references (superjson does too, but devalue's output is more compact):
const a: any = { name: "a" }
const b: any = { name: "b", ref: a }
a.ref = b // Circular: a → b → a
const str2 = devalue.stringify(a)
const restored2 = devalue.parse(str2)
console.log(restored2.ref.ref === restored2) // true — circular preserved
// Repeated references (shared identity):
const shared = { value: 42 }
const data2 = { x: shared, y: shared } // Same object referenced twice
const str3 = devalue.stringify(data2)
// devalue serializes shared objects once and references them
// superjson would duplicate them
const restored3 = devalue.parse(str3)
console.log(restored3.x === restored3.y) // true — identity preserved
SvelteKit usage
// +page.server.ts — SvelteKit uses devalue automatically:
import type { PageServerLoad } from "./$types"
export const load: PageServerLoad = async ({ params }) => {
return {
package: {
name: params.name,
publishedAt: new Date(), // Date preserved automatically
tags: new Set(["ui"]), // Set preserved — SvelteKit handles it
},
}
}
// No manual serialization needed — SvelteKit's load uses devalue under the hood
flatted
flatted is the minimalist option — solves one problem (circular JSON) with a tiny footprint.
Basic usage
import { stringify, parse } from "flatted"
// Circular reference that JSON.stringify can't handle:
const obj: any = { name: "circular" }
obj.self = obj
// JSON.stringify would throw; flatted handles it:
const str = stringify(obj)
const restored = parse(str)
console.log(restored.self === restored) // true
// Useful for logging complex objects with circular refs:
function safeLog(data: unknown) {
try {
console.log(JSON.stringify(data)) // Might throw for circular refs
} catch {
console.log(stringify(data)) // Always works
}
}
// JSON.parse compatible shape (arrays with refs):
console.log(str)
// ["[\"0\",\"1\"]",{"name":"circular","self":"[0]"}]
// — compact encoding of the circular structure
Feature Comparison
| Feature | superjson | devalue | flatted |
|---|---|---|---|
| Bundle size | ~9KB | ~5KB | ~0.5KB |
| Date preservation | ✅ | ✅ | ❌ |
| Set / Map | ✅ | ✅ | ❌ |
| undefined | ✅ | ✅ | ❌ |
| BigInt | ✅ | ✅ | ❌ |
| RegExp | ✅ | ✅ | ❌ |
| Circular references | ✅ | ✅ | ✅ |
| Shared object identity | ✅ | ✅ | ✅ |
| Custom types | ✅ register | ❌ | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| Used by | tRPC, Next.js | SvelteKit | General |
When to Use Each
Choose superjson if:
- Using tRPC (it's the recommended default transformer)
- Sending rich data (Dates, Sets, Maps) between Next.js server and client
- You need custom type serializers for domain models (like
Decimal) - Data needs to survive client-server boundaries with type fidelity
Choose devalue if:
- Using SvelteKit (built-in)
- You need efficient handling of shared object references (tree structures, graphs)
- You want smaller bundle size than superjson while preserving types
Choose flatted if:
- You only need circular reference support, nothing more
- Logging complex objects that might have circular refs
- Bundle size is critical (~0.5KB vs 9KB for superjson)
- You don't need type preservation (Dates can be strings)
Migration Guide
Adding superjson to an existing tRPC setup
If you have a tRPC setup without type-safe serialization and Date values are arriving as strings on the client:
// Before: tRPC without transformer (Dates arrive as strings)
import { initTRPC } from "@trpc/server"
const t = initTRPC.context<Context>().create()
// Routes return Date → client receives string
// After: add superjson transformer
import { initTRPC } from "@trpc/server"
import superjson from "superjson"
const t = initTRPC.context<Context>().create({
transformer: superjson,
})
// Now Dates, Sets, Maps, BigInts all survive the network round-trip
You must add the transformer on both server and client:
// Client setup (trpc.ts):
import { createTRPCNext } from "@trpc/next"
import superjson from "superjson"
import type { AppRouter } from "../server/router"
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
transformer: superjson, // Match the server transformer
links: [/* ... */],
}
},
})
Replacing manual date parsing with superjson
A common pattern before superjson was manually converting date strings back to Date objects after fetch:
// Before: manual date deserialization
async function fetchPackageData(name: string) {
const data = await fetch(`/api/packages/${name}`).then(r => r.json())
return {
...data,
publishedAt: new Date(data.publishedAt), // Manual conversion
updatedAt: new Date(data.updatedAt), // Easy to forget
}
}
// After: superjson handles it automatically
import superjson from "superjson"
async function fetchPackageData(name: string) {
const raw = await fetch(`/api/packages/${name}`).then(r => r.text())
return superjson.parse(raw) // All types restored automatically
}
// Server returns:
// return superjson.stringify({ publishedAt: new Date(), updatedAt: new Date() })
Security Implications of Custom Serialization
Using alternative serialization libraries introduces trust boundary considerations that do not exist with plain JSON.stringify. When data crosses a network boundary and is deserialized with superjson.parse() or devalue.parse(), the deserialized types are determined by the metadata embedded in the serialized payload — not solely by your TypeScript types. An attacker who can modify the serialized payload in transit could, in theory, supply a metadata object that instructs the deserializer to reconstruct a different type than intended. Both superjson and devalue are aware of this threat model: superjson's parse() only reconstructs types from a fixed allowlist (Date, Set, Map, BigInt, RegExp, undefined — no arbitrary object constructors), and devalue similarly limits reconstruction to known safe types.
For tRPC applications using superjson as the transformer, the serialized payload travels over HTTP as a POST body or query string parameter. The superjson metadata is a JSON object describing paths within the value and their types — it is not executable and cannot cause prototype pollution or arbitrary code execution. This is fundamentally different from JSON.parse() with a reviver function on a raw object that could include constructor or __proto__ keys. However, teams deploying in environments where regulatory compliance requires inspecting all serialized data formats (PCI DSS, HIPAA environments) should document their superjson/devalue usage and verify that the serialized format can be audited by their security tooling, as it differs from plain JSON.
Performance Benchmarks at Scale
Serialization performance is rarely the bottleneck in typical applications, but it matters in high-frequency paths — serializing hundreds of tRPC responses per second, or serializing large normalized state objects in SvelteKit's load functions. Superjson's serialization involves two passes: first JSON.stringify of the value, then construction of the metadata object describing type information, then a second JSON.stringify combining both. The overhead versus plain JSON.stringify is approximately 15–30% on typical objects, growing to 50–100% on objects with many Date or Map instances (since more metadata entries are generated). For most API responses, this overhead is immeasurable compared to database query time or network latency.
Flatted's performance advantage — a single-pass encoding that handles circular references — is more significant in comparison to alternatives that require multiple passes. Benchmarks show flatted serializing complex recursive tree structures 3–5x faster than alternatives that traverse the structure multiple times. Devalue sits between superjson and flatted in performance: its compact encoding of shared references means smaller output for graphs with repeated values, but the reference tracking adds bookkeeping overhead that is measurable for deeply nested objects. For the specific use case of serializing large normalized state (Redux-style stores where entities are referenced by ID from multiple places), devalue's shared reference handling both improves performance and significantly reduces the size of the serialized output compared to superjson's approach of duplicating shared objects.
Community Adoption in 2026
superjson reaches approximately 3 million weekly downloads, driven almost entirely by its role as the recommended transformer in the tRPC ecosystem. The T3 Stack (create-t3-app) ships with superjson as the tRPC transformer by default, meaning every project scaffolded with T3 includes superjson. The library solves a real pain point in full-stack TypeScript applications: the JavaScript type system includes Date, Set, Map, BigInt, and undefined, but JSON serialization loses all of these, forcing developers to manually reconstruct types after deserialization. superjson's design — serializing type metadata alongside the value, compatible with the standard JSON wire format — makes it transparent to HTTP infrastructure while preserving JavaScript's richer type vocabulary.
devalue at approximately 1.5 million weekly downloads is downloaded primarily as a SvelteKit dependency. Rich Harris (creator of Svelte) wrote devalue to power SvelteKit's load function return value serialization — when a +page.server.ts load function returns data containing Date objects, Sets, Maps, or circular references, SvelteKit uses devalue to serialize it for hydration on the client. Developers using SvelteKit get devalue's benefits without consciously choosing it. Outside of SvelteKit, devalue is valued for its efficient handling of shared object identity: if two properties reference the same object, devalue serializes the object once and stores references, which superjson does not do. This matters for tree structures, graph data, and normalized state.
flatted reaches approximately 8 million weekly downloads despite its minimal scope — it handles only circular references, nothing more. The high download count reflects its use as a utility library across logging frameworks, serialization libraries, and debugging tools that need to safely stringify any JavaScript value without risk of circular reference exceptions. Its 0.5KB bundle size makes it practically free to include. Many libraries depend on flatted as a safe alternative to JSON.stringify for internal serialization of user-provided objects (which might contain circular references), rather than as a developer-facing serialization format.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on superjson v2.x, devalue v4.x, and flatted v3.x.
Compare serialization and utility packages on PkgPulse →
See also: AVA vs Jest and ohash vs object-hash vs hash-wasm, acorn vs @babel/parser vs espree.