superjson vs devalue vs flatted: Advanced JSON Serialization in JavaScript (2026)
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)
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on superjson v2.x, devalue v4.x, and flatted v3.x.