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

ts-jackson

v2.0.0

Published

Serialize and deserialize deeply nested JSON into TypeScript classes with decorators. Deep lodash-style path mapping, TC39 standard decorator support, strict type checking, zod-style safe parsing, polymorphism, Maps and naming strategies.

Readme

ts-jackson

CI npm license

Map ugly API JSON into clean TypeScript classes — and back. Inspired by Java's Jackson, built for the shape of real-world APIs.

import { JsonProperty, Serializable, deserialize, serialize } from 'ts-jackson'

const response = {
  track: {
    id: '42',
    album: { images: [{ url: 'https://cover.jpg' }] },
    duration_ms: 235000,
  },
}

@Serializable()
class Track {
  @JsonProperty('track.id')
  id: string

  @JsonProperty('track.album.images[0].url')
  coverUrl: string

  @JsonProperty('track.duration_ms')
  durationMs: number
}

const track = deserialize(response, Track)
// Track { id: '42', coverUrl: 'https://cover.jpg', durationMs: 235000 }

serialize(track)
// restores the original nested shape

Why ts-jackson

  • Deep path mapping — properties resolve through lodash path patterns ('track.album.images[0].url'), so a flat domain class can be built from arbitrarily nested JSON and serialized back to the same shape. Flat-rename-only mappers can't restructure.
  • Works with both decorator standards — legacy experimentalDecorators and TC39 standard decorators (the TypeScript 5+ default). That includes toolchains without emitDecoratorMetadata support such as esbuild and Vite.
  • Safe, zod-style parsingsafeDeserialize returns { success, data | errors } and collects every property error in one pass instead of throwing on the first.
  • Typed, structured errors — every error carries a kind discriminant plus fields like propertyName, path, expected, and the offending value.
  • Strict mode, polymorphism, Maps/dictionaries, naming strategies, access control — see the tour below.

Installation

npm install ts-jackson reflect-metadata

reflect-metadata is a peer dependency; the library imports it internally, so no extra setup is needed in your code.

TypeScript configuration

Legacy decorators (full feature set including type inference):

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

Standard (TC39) decorators — no flags at all. Types cannot be inferred (the standard has no emitDecoratorMetadata equivalent), so pass them explicitly where conversion matters:

@Serializable()
class Event {
  @JsonProperty({ type: Date })
  startsAt: Date

  @JsonProperty({ elementType: Image }) // elementType implies an array
  images: Image[]

  @JsonProperty({ type: Map, elementType: Image })
  imagesBySize: Map<string, Image>
}

Everything else — paths, hooks, strict, required, polymorphism — behaves identically in both modes.

API tour

Entry points

deserialize(json, Track)                 // throws on the first error
serialize(track)

deserializeArray(jsonArray, Track)       // top-level JSON arrays
serializeArray(tracks)

safeDeserialize(json, Track)             // never throws, collects all errors
safeSerialize(track)
safeDeserializeArray(jsonArray, Track)

deserialize forwards extra arguments to the constructor:

@Serializable()
class Cat {
  @JsonProperty() name: string
  constructor(readonly owner: string) {}
}

deserialize({ name: 'Moon' }, Cat, 'Ilias')

@JsonProperty

Accepts a path string, an array of paths, or an options object:

| Option | Purpose | | --- | --- | | path | JSON path (lodash syntax); defaults to the property name | | paths | Multiple paths resolved into a tuple/array | | pathAlternatives | Deserialize-only aliases; first non-null wins, primary path is tried first | | required | Throw RequiredPropertyError when the value is missing | | default | Substitute for missing values (checked before required, so the two combine) | | strict | Reject JSON values whose type doesn't match — see Strict mode | | access | 'deserialize-only' (skip on serialize) or 'serialize-only' (skip on deserialize) | | type | Explicit type; accepts a class or a lazy arrow thunk () => Class | | elementType | Element type for Array/Set/Map/dictionary values; thunk allowed | | resolveType | (json) => Class — per-value polymorphic dispatch | | validate | Predicate; failure throws ValidatePropertyError | | beforeDeserialize / deserialize / afterDeserialize | Deserialization hooks | | beforeSerialize / serialize / afterSerialize | Serialization hooks |

Deserialization pipeline order: resolve value → defaultrequiredbeforeDeserializestrictdeserialize (custom or built-in) → validate → assign; afterDeserialize hooks run after the whole instance is populated.

@Serializable

import { camelToSnakeCase } from 'ts-jackson'

@Serializable({
  formatPropertyName: camelToSnakeCase, // accessToken ⇆ access_token
  strict: true,                         // strict for every property
})
class Token {
  @JsonProperty() accessToken: string   // maps to 'access_token'
  @JsonProperty('expires') expiresIn: number // explicit path wins
  @JsonProperty({ strict: false }) raw: unknown // per-property opt-out
}

Both options are inherited by subclasses and can be overridden. Bundled naming strategies: camelToSnakeCase, camelToKebabCase, camelToPascalCase — or pass any (name: string) => string.

Path resolution

// Single deep path
@JsonProperty('track.album.images[0].url')
coverUrl: string

// Multiple paths → tuple
@JsonProperty({ paths: ['id', 'meta.rev'] })
idAndRevision: [string, number]

// Aliases: first non-null of snack → treat → goodie
@JsonProperty({ pathAlternatives: ['treat', 'goodie'] })
snack: string

Serialization always writes to the primary path, so deserialize(serialize(x)) is stable.

Collections

Array, Set, Map, and plain-object dictionaries are supported; elementType types their values:

@Serializable()
class Gallery {
  @JsonProperty({ elementType: Image })
  images: Image[]

  @JsonProperty({ elementType: Image })
  imagesBySize: Map<string, Image>        // { small: {...} } → Map

  @JsonProperty({ elementType: Image })
  imagesByName: Record<string, Image>     // values become Image instances
}

Polymorphism

resolveType picks the concrete class per value:

@Serializable()
class Canvas {
  @JsonProperty({
    elementType: Shape,
    resolveType: (json) => ('radius' in json ? Circle : Square),
  })
  shapes: Shape[]
}

Serialization dispatches on each value's runtime class automatically.

Self-referencing and circular types

Use arrow-function thunks when a class references itself or when model files import each other circularly:

@Serializable()
class Category {
  @JsonProperty() name: string

  @JsonProperty({ elementType: () => Category })
  children: Category[]
}

Strict mode

By default, primitives are coerced (Number('42') → 42, Number('abc') → NaN). With strict — per property or class-wide — the raw JSON value must already match the declared type:

@JsonProperty({ strict: true })
age: number

deserialize({ age: '30' }, Person)
// TypeMismatchError: Property 'age' (path: 'age') in Person failed type
// check: expected Number, received string ("30").

null and missing values pass strict checks (use required for presence); a custom deserialize function bypasses them.

Error handling

All library errors extend TsJacksonError and expose structured fields:

| Class | kind | Fields | | --- | --- | --- | | RequiredPropertyError | 'required' | propertyName, path, className | | TypeMismatchError | 'type-mismatch' | propertyName, path, className, expected, value | | ValidatePropertyError | 'validation' | propertyName, className, value | | SerializableError | 'not-serializable' | className |

Throwing style:

try {
  deserialize(json, Track)
} catch (error) {
  if (error instanceof TypeMismatchError) {
    console.log(error.propertyName, error.expected, error.value)
  }
}

Functional style — collects every error in one pass:

import { safeDeserialize, isTsJacksonError } from 'ts-jackson'

const result = safeDeserialize(json, Track)
if (!result.success) {
  for (const error of result.errors.filter(isTsJacksonError)) {
    switch (error.kind) {
      case 'required':      // error.path
      case 'type-mismatch': // error.expected, error.value
      case 'validation':    // error.value
    }
  }
}

SerializableEntity

Base class bundling the API and removing the need for @Serializable:

class Image extends SerializableEntity {
  @JsonProperty({ required: true })
  url: string
}

const image = Image.deserialize({ url: '...' })
image.serialize()
image.stringify()

Migrating from v1

  • Node/TS baseline: compiled output targets ES2017; TypeScript peer tooling expects modern versions.
  • reflect-metadata is now a peer dependency — installed alongside the lib (npm 7+ does this automatically); the library still imports it for you.
  • Errors: collection shape mismatches now throw TypeMismatchError (previously a bare TypeError); all errors expose structured fields and kind.
  • Packaging: an exports map now defines the public entry point — deep imports from dist/ internals are no longer part of the API. Plain Node ESM (import ... from 'ts-jackson') now works.
  • Everything decorated for v1 keeps deserializing identically; subclass metadata no longer leaks into parent classes (previously a bug).

Examples

See src/examples for real-world models (Spotify API entities, OAuth tokens) used as living documentation and tests.

License

MIT