npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@depthbomb/serde

v2.0.0

Published

Type-safe JSON ↔ Class serialization for TypeScript

Readme

@depthbomb/serde

Type-safe JSON ↔ Class serialization for TypeScript


Installation

yarn add @depthbomb/serde
bun add @depthbomb/serde
npm install @depthbomb/serde

Legacy TypeScript decorators use the following setting. Standard ECMAScript field decorators are also supported and do not require experimentalDecorators.

{
	"compilerOptions": {
		"experimentalDecorators": true,
		"strict": true
	}
}

Quick start

import { toJSON, serialize, deserialize, Serializable, JSONProperty } from '@depthbomb/serde';

@Serializable()
class User {
	@JSONProperty({ name: 'first_name' })
	firstName!: string;

	@JSONProperty({ name: 'last_name' })
	lastName!: string;

	@JSONProperty()
	age!: number;
}

// Deserialize
const user = deserialize(User, { first_name: 'Leon', last_name: 'Kennedy', age: 49 });
console.log(user.firstName);       // 'Leon'
console.log(user instanceof User); // true

// Serialize
const plain = serialize(user); // { first_name: 'Leon', last_name: 'Kennedy', age: 49 }
const json  = toJSON(user, 2); // pretty-printed JSON string

Decorators

@Serializable()

Marks a class as a serialization target. Required for any class used as a nested type.

@Serializable()
class MyClass { ... }

@JSONProperty(options?)

Marks a property for (de)serialization. All options are optional.

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | property name | JSON key to read/write | | aliases | string[] | [] | Legacy JSON keys accepted on input | | type | Constructor \| () => Constructor | — | Nested class type (use thunk for forward refs) | | codec | JSONCodec<T> | — | Reusable typed value ↔ JSON conversion; mutually exclusive with type | | isArray | boolean | false | Property holds T[] | | isMap | boolean | false | Property holds Map<string, T> | | isSet | boolean | false | Property holds Set<T> | | optional | boolean | true | Skip if key absent; false = required | | nullable | 'ignore' \| 'null' \| 'error' | 'ignore' | Behaviour when value is null | | defaultValue | T \| (() => T) | — | Default when key is absent | | deserializeTransform | (raw: unknown) => T | identity | Post-deserialization transform | | serializeTransform | (value: T) => unknown | identity | Pre-serialization transform | | validate | (value: T) => boolean \| string \| void | — | Validator; false/string throws | | deserializeAsyncTransform | (value: T) => Promise<T> | — | Async post-deserialization transform | | serializeAsyncTransform | (value: T) => Promise<unknown> | — | Async serialization transform | | validateAsync | (value: T) => Promise<boolean \| string \| void> | — | Async validator | | groups | string[] | [] | Named serialization projections | | sensitive | boolean | false | Omit unless includeSensitive is enabled |


@JSONDiscriminator(field) + @JSONSubType(value, Class)

Enable polymorphic deserialization using a discriminator field.

@Serializable()
@JSONDiscriminator('type')
@JSONSubType('circle', Circle)
@JSONSubType('rectangle', Rectangle)
class Shape {
	@JSONProperty() type!: string;
	@JSONProperty() color!: string;
}

// Automatically dispatches to the right subclass:
const s = deserialize(Shape, { type: 'circle', color: 'red', radius: 5 });
console.log(s instanceof Circle); // true

API Reference

// Deserialization
deserialize<T>(ctor: Constructor<T>, data: Record<string, unknown> | string, path?: string, options?: IDeserializeOptions): T
deserializeArray<T>(ctor: Constructor<T>, data: Record<string, unknown>[] | string, path?: string, options?: IDeserializeOptions): T[]
deserializeAsync<T>(ctor: Constructor<T>, data: Record<string, unknown> | string, path?: string, options?: IDeserializeOptions): Promise<T>
deserializeArrayAsync<T>(...): Promise<T[]>
fromJSON<T>(ctor: Constructor<T>, json: string): T
fromJSONAsync<T>(ctor: Constructor<T>, json: string): Promise<T>

// Serialization
serialize<T extends object>(instance: T, path?: string, options?: ISerializeOptions): Record<string, unknown>
serializeArray<T extends object>(instances: T[], path?: string, options?: ISerializeOptions): Record<string, unknown>[]
serializeAsync<T extends object>(instance: T, path?: string, options?: ISerializeOptions): Promise<Record<string, unknown>>
serializeArrayAsync<T extends object>(...): Promise<Record<string, unknown>[]>
toJSON<T>(instance: T, space?: number): string
toJSONAsync<T>(instance: T, space?: number): Promise<string>

// Utilities
clone<T>(ctor: Constructor<T>, instance: T): T
patch<T>(ctor: Constructor<T>, instance: T, partial: Record<string, unknown>): T
isSerializable(ctor: Constructor): boolean
isEnum(obj: unknown): boolean
generateJSONSchema(ctor: Constructor, namingStrategy?: NamingStrategy): Record<string, unknown>

Deserialization Options

When calling deserialize() or deserializeArray(), pass a fourth options argument:

interface IDeserializeOptions {
	/**
	 * When `true`, any JSON keys not declared via `@JSONProperty` cause
	 * a `SerializationError`. Useful for validating untrusted input.
	 * Defaults to `false`.
	 */
	strict?: boolean;

	/**
	 * Global key mapping strategy for JSON <-> property name conversion.
	 * Explicit `@JSONProperty({ name })` values always win.
	 */
	namingStrategy?: (propertyKey: string) => string;
	unknownProperties?: 'ignore' | 'error' | 'collect';
	unknownProperty?: string; // destination property for collect mode
}

Example:

const user = deserialize(User, data, '$', { strict: true });

Serialization Options

interface ISerializeOptions {
	/**
	 * Global key mapping strategy for property -> JSON key conversion.
	 * Explicit `@JSONProperty({ name })` values always win.
	 */
	namingStrategy?: (propertyKey: string) => string;
	groups?: string[];
	includeSensitive?: boolean;
}

Schema evolution and unknown fields

Aliases accept old names while serialization always emits the canonical name. Version migrations run in order; migration N upgrades version N to N + 1.

@Serializable()
@JSONVersion(2, {
	migrations: {
		0: data => ({ ...data, name: data.old_name }),
		1: data => ({ ...data, fullName: data.name }),
	},
})
class User {
	@JSONProperty({ aliases: ['name'], optional: false }) fullName!: string;
}

const value = deserialize(User, payload, '$', {
	unknownProperties: 'collect',
	unknownProperty: 'extensions',
});

Serialization emits the current $version. Configure another field with JSONVersion(..., { field: 'version' }).

Codecs and projections

const epochCodec: JSONCodec<Date, number> = {
	serialize: date => date.getTime(),
	deserialize: value => new Date(value),
	schema: { type: 'integer' },
};

class Session {
	@JSONProperty({ codec: epochCodec }) createdAt!: Date;
	@JSONProperty({ groups: ['admin'], sensitive: true }) token!: string;
}

serialize(session, '$', { groups: ['admin'], includeSensitive: true });

generateJSONSchema(Session) produces draft 2020-12 JSON Schema, including nested definitions, enums, aliases, groups, nullability, collections, and codec schema fragments.

Global Naming Strategies

You can globally format property keys into standardized JSON casing like snake_case or PascalCase without manually applying the .name attribute on every single property wrapper:

import { NamingStrategies } from '@depthbomb/serde';

@Serializable()
class User {
	@JSONProperty() firstName!: string;
	@JSONProperty() lastName!: string;
}

const payload = { first_name: 'John', last_name: 'Doe' };
const user = deserialize(User, payload, '$', {
	namingStrategy: NamingStrategies.camelToSnake
});

console.log(user.firstName); // "John"

Note: Any property that explicitly declares @JSONProperty({ name: 'CUSTOM' }) will bypass the NamingStrategy directly and preserve its intentional schema name.


Recipes

Nested classes

@Serializable()
class Address {
	@JSONProperty() street!: string;
	@JSONProperty() city!: string;
}

@Serializable()
class Person {
	@JSONProperty() name!: string;
	@JSONProperty({ type: () => Address }) address!: Address;
}

const person = deserialize(Person, {
	name: 'Grace',
	address: { street: '42 Broadway', city: 'New York' },
});
console.log(person.address instanceof Address); // true

Arrays of classes

@Serializable()
class Order {
	@JSONProperty({ type: () => LineItem, isArray: true })
	items!: LineItem[];
}

Maps

@Serializable()
class Catalog {
	// Serialized as a plain object { 'sku-1': {...}, ... }
	@JSONProperty({ type: () => Product, isMap: true })
	products!: Map<string, Product>;
}

Sets

@Serializable()
class TagGroup {
	// Serialized as a plain array ["foo", "bar"]
	@JSONProperty({ type: String, isSet: true })
	tags!: Set<string>;
}

Dates and URLs

JavaScript Date and URL objects are supported natively without custom transforms.

@Serializable()
class Event {
	@JSONProperty({ type: Date })
	startDate!: Date;

	@JSONProperty({ type: URL })
	link!: URL;
}

const ev = deserialize(Event, {
	startDate: '2026-03-20T11:23:46.000Z',
	link: 'https://example.com/foo'
});
console.log(ev.startDate.getFullYear()); // 2026
console.log(ev.link.pathname); // /foo

Enums

enum Status {
	Active = 'ACTIVE',
	Inactive = 'INACTIVE'
}

enum Priority {
	Low = 0,
	Medium = 1,
	High = 2
}

@Serializable()
class Task {
	@JSONProperty()
	title!: string;

	@JSONProperty({ type: () => Status })
	status!: Status;

	@JSONProperty({ type: () => Priority })
	priority!: Priority;
}

const task = deserialize(Task, {
	title: 'Fix bug',
	status: 'ACTIVE',
	priority: 1
});

console.log(task.status);   // 'ACTIVE'
console.log(task.priority); // 1

const plain = serialize(task); // { title: 'Fix bug', status: 'ACTIVE', priority: 1 }

Default values

@Serializable()
class Settings {
	@JSONProperty({ defaultValue: 'light' }) theme!: string;
	@JSONProperty({ defaultValue: () => [] }) tags!: string[]; // factory for safe mutable defaults
}

Required properties

@Serializable()
class Config {
	@JSONProperty({ optional: false })
	apiKey!: string; // throws SerializationError if missing
}

Validation

@Serializable()
class Product {
	@JSONProperty({ validate: (v: number) => v > 0 || 'Price must be positive' })
	price!: number;
}

Null handling

@Serializable()
class Record {
	@JSONProperty({ nullable: 'null' }) mayBeNull!: string | null; // preserved
	@JSONProperty({ nullable: 'ignore' }) skipNull?: string;       // omitted (default)
	@JSONProperty({ nullable: 'error' }) mustExist!: string;       // throws
}

Inheritance

@Serializable()
class Animal {
	@JSONProperty() name!: string;
}

@Serializable()
class Pet extends Animal {
	@JSONProperty() ownerName!: string;
	// `name` is inherited and still serialized
}

Clone & patch

const copy    = clone(User, user);              // deep-independent copy
const updated = patch(User, user, { age: 37 }); // non-destructive update

Error handling

All library, parser, migration, codec, transform, validation, and constructor failures are exposed as SerializationError. Native failures are preserved in .cause; .path uses JSONPath-style notation:

import { SerializationError } from '@depthbomb/serde';

try {
	deserialize(User, {});
} catch (err) {
	if (err instanceof SerializationError) {
		console.log(err.message); // [@depthbomb/serde] Missing required property '...' (at '$.fieldName')
		console.log(err.path);    // '$.fieldName'
	}
}