@traversable/json-schema
is a schema rewriter for JSON Schema
specs.
Currently this package only supports JSON Schema Draft 2020-12
$ pnpm add @traversable/json-schema
Here's an example of importing the library:
import { JsonSchema } from '@traversable/json-schema'
// or, if you prefer, you can use named imports:
import { deepClone, deepEqual } from '@traversable/json-schema'
// see below for specific examples
JsonSchema.check
JsonSchema.check.writeable
JsonSchema.deepClone
JsonSchema.deepClone.writeable
JsonSchema.deepEqual
JsonSchema.deepEqual.writeable
JsonSchema.check
JsonSchema.check
converts a JSON Schema into a super-performant type-guard.
Function
constructor, including (as of May 2025) Cloudflare workers ๐Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
โโโโโโโโโโโโโโโโโโ
โ Average โ
โโโโโโโโโผโโโโโโโโโโโโโโโโโค
โ Ajv โ 1.57x faster โ
โโโโโโโโโดโโโโโโโโโโโโโโโโโ
import { JsonSchema } from '@traversable/json-schema'
const check = JsonSchema.check({
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
})
check({ street1: '221B Baker St', city: 'London' }) // => true
check({ street1: '221B Baker St' }) // => false
JsonSchema.check.writeable
JsonSchema.check
converts a JSON Schema into a super-performant type-guard.
Compared to JsonSchema.check
, JsonSchema.check.writeable
returns
the check function in stringified ("writeable") form.
import { JsonSchema } from '@traversable/json-schema'
const check = JsonSchema.check.writeable({
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
}, { typeName: 'Address' })
console.log(check)
// =>
// type Address = { street1: string; street2?: string; city: string }
// function check(value: Address) {
// return (
// !!value &&
// typeof value === "object" &&
// typeof value.street1 === "string" &&
// (!Object.hasOwn(value, "street2") || typeof value.street2 === "string") &&
// typeof value.city === "string"
// )
// }
JsonSchema.deepClone
JsonSchema.deepClone
lets users derive a specialized "deep copy" function that works with values that have been already validated.
Because the values have already been validated, clone times are significantly faster than alternatives like window.structuredClone
and Lodash.cloneDeep
.
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
โโโโโโโโโโโโโโโโโโโ
โ Average โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโค
โ Lodash.cloneDeep โ 13.99x faster โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโค
โ window.structuredClone โ 17.23x faster โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
This article goes into more detail about what makes JsonSchema.deepClone
so fast.
Lodash 868.72 ns/iter 1.00 ยตs โโ
(269.22 ns โฆ 1.20 ยตs) 1.14 ยตs โโ
( 8.05 b โฆ 963.18 b) 307.93 b โโโโโโโโ
โโโโโโโโโโโ
โโ
3.64 ipc ( 1.47% stalls) 98.24% L1 data cache
2.41k cycles 8.77k instructions 38.66% retired LD/ST ( 3.39k)
structuredClone 1.07 ยตs/iter 1.08 ยตs โโโ
(1.02 ยตs โฆ 1.24 ยตs) 1.22 ยตs โโโโโโ
( 13.91 b โฆ 369.62 b) 38.79 b โโ
โโโโโโโโโโโโโโโโโโโ
4.35 ipc ( 1.33% stalls) 98.23% L1 data cache
3.10k cycles 13.50k instructions 34.90% retired LD/ST ( 4.71k)
JSON.stringify + JSON.parse 527.05 ns/iter 575.48 ns โ โ
(367.58 ns โฆ 2.30 ยตs) 732.21 ns โโ โโ
( 3.97 b โฆ 383.93 b) 75.70 b โโโโโโโโโโ
โโโโโโโโโโโ
4.41 ipc ( 1.07% stalls) 98.42% L1 data cache
1.53k cycles 6.73k instructions 36.86% retired LD/ST ( 2.48k)
JsonSchema.deepClone 62.08 ns/iter 65.56 ns โโ
(8.95 ns โฆ 255.66 ns) 208.93 ns โโโ
( 1.92 b โฆ 214.18 b) 47.77 b โโโโโโโโ
โโโโโโโโโโโโโ
2.94 ipc ( 1.29% stalls) 98.86% L1 data cache
164.89 cycles 485.47 instructions 44.83% retired LD/ST ( 217.63)
Lodash โคโ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ 868.72 ns
structuredClone โคโ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ 1.07 ยตs
JSON.stringify + JSON.parse โคโ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ 527.05 ns
JsonSchema.deepClone โค 62.08 ns
โ โ
โ โ
โท โโโโโโโโฌโโโโโ โท
Lodash โโโโโโโโโโโโโโโโค โ โโโโโโค
โต โโโโโโโโดโโโโโ โต
โทโฌโ โท
structuredClone โโโโโโโโค
โตโดโ โต
โท โโโฌโโ โท
JSON.stringify + JSON.parse โโโโโค โ โโโโโโโค
โต โโโดโโ โต
โทโโฌ โท
JsonSchema.deepClone โโคโโโโโโค
โตโโด โต
โ โ
8.95 ns 613.29 ns 1.22 ยตs
summary
JsonSchema.deepClone
8.49x faster than JSON.stringify + JSON.parse
13.99x faster than Lodash
17.23x faster than structuredClone
For a more detailed breakdown, see all the benchmark results.
import { JsonSchema } from '@traversable/json-schema'
const Address = {
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
} as const
const deepClone = JsonSchema.deepClone(Address)
const deepEqual = JsonSchema.deepEqual(Address)
const sherlock = { street1: '221 Baker St', street2: '#B', city: 'London' }
const harry = { street1: '4 Privet Dr', city: 'Little Whinging' }
const sherlockCloned = deepClone(sherlock)
const harryCloned = deepClone(harry)
deepEqual(sherlockCloned, sherlock) // => true
sherlock === sherlockCloned // => false
deepEqual(harryCloned, harry) // => true
harry === harryCloned // => false
JsonSchema.deepClone.writeable
JsonSchema.deepClone.writeable
lets users derive a specialized "deep clone" function that works with values that have been already validated.
Compared to JsonSchema.deepClone
, JsonSchema.deepClone.writeable
returns
the clone function in stringified ("writeable") form.
import { JsonSchema } from '@traversable/json-schema'
const deepClone = JsonSchema.deepClone.writeable({
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
}, { typeName: 'Address' })
console.log(deepClone)
// =>
// type Address = { street1: string; street2?: string; city: string; }
// function deepClone(prev: Address): Address {
// return {
// street1: prev.street1,
// ...prev.street2 !== undefined && { street2: prev.street2 },
// city: prev.city
// }
// }
JsonSchema.deepEqual
JsonSchema.deepEqual
lets users derive a specialized "deep equal" function that works with values that have been already validated.
Because the values have already been validated, comparison times are significantly faster than alternatives like NodeJS.isDeepStrictEqual
and Lodash.isEqual
.
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ Array (avg) โ Object (avg) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโค
โ NodeJS.isDeepStrictEqual โ 40.3x faster โ 56.5x faster โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโค
โ Lodash.isEqual โ 53.7x faster โ 60.1x faster โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโ
This article goes into more detail about what makes JsonSchema.deepEqual
so fast.
Function
constructor, including (as of May 2025) Cloudflare workers ๐import { JsonSchema } from '@traversable/json-schema'
const deepEqual = JsonSchema.deepEqual({
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
})
deepEqual(
{ street1: '221 Baker St', street2: '#B', city: 'London' },
{ street1: '221 Baker St', street2: '#B', city: 'London' }
) // => true
deepEqual(
{ street1: '221 Baker St', street2: '#B', city: 'London' },
{ street1: '4 Privet Dr', city: 'Little Whinging' }
) // => false
JsonSchema.deepEqual.writeable
JsonSchema.deepEqual.writeable
lets users derive a specialized "deep equal" function that works with values that have been already validated.
Compared to JsonSchema.deepEqual
, JsonSchema.deepEqual.writeable
returns
the deep equal function in stringified ("writeable") form.
import { JsonSchema } from '@traversable/json-schema'
const deepEqual = JsonSchema.deepEqual.writeable({
type: 'object',
required: ['street1', 'city'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
}
}, { typeName: 'Address' })
console.log(deepEqual)
// =>
// type Address = { street1: string; street2?: string; city: string; }
// function deepEqual(x: Address, y: Address) {
// if (x === y) return true;
// if (x.street1 !== y.street1) return false;
// if (x.street2 !== y.street2) return false;
// if (x.city !== y.city) return false;
// return true;
// }
JsonSchema.fold
JsonSchema.fold
is an advanced API.
Use JsonSchema.fold
to define a recursive traversal of a JSON Schema. Useful when building a schema rewriter.
Writing an arbitrary traversal with JsonSchema.fold
is:
The way it works is pretty simple: if you imagine all the places in the JSON Schema specification that are recursive, those "holes" will be the type that you provide via type parameter.
As an example, let's write a function called check
that takes a JSON Schema, and returns a function that validates its input against the schema.
Here's how you could use JsonSchema.fold
to implement it:
import { JsonSchema } from '@traversable/json-schema'
const isObject = (u: unknown): u is { [x: string]: unknown } =>
!!u && typeof u === 'object' && !Array.isArray(u)
const check = JsonSchema.fold<(data: unknown) => boolean>(
(schema) => { // ๐_______________________๐
// this type will fill the "holes" in our schema
switch (true) {
case JsonSchema.isNull(schema):
return (data) => data === null
case JsonSchema.isBoolean(schema):
return (data) => typeof data === 'boolean'
case JsonSchema.isInteger(schema):
return (data) => Number.isSafeInteger(data)
case JsonSchema.isNumber(schema):
return (data) => Number.isFinite(data)
case JsonSchema.isArray(schema):
return (data) => Array.isArray(data)
&& schema.every(schema.items)
// ๐___๐
// items: (data: unknown) => boolean
case JsonSchema.isObject(schema):
return (data) => isObject(data)
&& Object.entries(schema.properties).every(
([key, property]) => schema.required.includes(key)
// ๐______๐
// property: (data: unknown) => boolean
? (Object.hasOwn(data, key) && property(data[key]))
: (!Object.hasOwn(data, key) || property(data[key]))
)
default: return () => false
}
}
)
// Let's use `check` to create a predicate:
const isBooleanArray = check({
type: 'array',
items: { type: 'boolean' }
})
// Using the predicate looks like this:
isBooleanArray([false]) // true
isBooleanArray([true, 42]) // false
That's it!
If you'd like to see a more complex example, here's how JsonSchema.check
is actually implemented.
JsonSchema.fold
is similar to, but more powerful than, the visitor pattern.
If you're curious about the theory behind it, its implementation was based on a 1991 paper called Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire.
JsonSchema.Functor
JsonSchema.Functor
is an advanced API.
JsonSchema.Functor
is the primary abstraction that powers @traversable/json-schema
.
JsonSchema.Functor
is a powertool. Most of @traversable/json-schema
uses JsonSchema.Functor
under the hood.
Compared to the rest of the library, it's fairly "low-level", so unless you're doing something pretty advanced you probably won't need to use it directly.