Skip to main content

Guide

superjson vs devalue vs flatted 2026

Compare superjson, devalue, and flatted for serializing JavaScript values beyond what JSON.stringify handles. Date, Set, Map, undefined, circular references.

·PkgPulse Team·
0

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.parse gives 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

PackageWeekly DownloadsTypes PreservedCircular RefsBundle 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

Featuresuperjsondevalueflatted
Bundle size~9KB~5KB~0.5KB
Date preservation
Set / Map
undefined
BigInt
RegExp
Circular references
Shared object identity
Custom types✅ register
TypeScript
Used bytRPC, Next.jsSvelteKitGeneral

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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.