Skip to main content

superjson vs devalue vs flatted: Advanced JSON Serialization in JavaScript (2026)

·PkgPulse Team

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)

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.