@traversable/json-schema
v0.0.26
Published
<br> <h1 align="center">แฏ๐๐ฟ๐ฎ๐๐ฒ๐ฟ๐๐ฎ๐ฏ๐น๐ฒ/๐ท๐๐ผ๐ป-๐๐ฐ๐ต๐ฒ๐บ๐ฎ</h1> <br>
Readme
[!NOTE] Currently this package only supports JSON Schema Draft 2020-12
Getting started
$ pnpm add @traversable/json-schemaHere'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 examplesTable of contents
Fuzz-tested, production ready
JsonSchema.checkJsonSchema.check.writeableJsonSchema.deepCloneJsonSchema.deepClone.writeableJsonSchema.deepEqualJsonSchema.deepEqual.writeableJsonSchema.toType
Advanced
Features
JsonSchema.check
JsonSchema.check converts a JSON Schema into a super-performant type-guard.
Notes
- Consistently better performance than Ajv
- Works in any environment that supports defining functions using the
Functionconstructor, including (as of May 2025) Cloudflare workers ๐
Performance comparison
Here's a Bolt sandbox if you'd like to run the benchmarks yourself.
โโโโโโโโโโโโโโโโโโ
โ Average โ
โโโโโโโโโผโโโโโโโโโโโโโโโโโค
โ Ajv โ 1.57x faster โ
โโโโโโโโโดโโโโโโโโโโโโโโโโโExample
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' }) // => falseSee also
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.
Notes
- Useful when you're consuming a set of JSON Schemas schemas and writing them all to disc
- Also useful for testing purposes or for troubleshooting, since it gives you a way to "see" exactly what the check functions check
Example
import { JsonSchema } from '@traversable/json-schema'
const check = JsonSchema.check.writeable({
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { enum: ['AL', 'AK', 'AZ', '...'] }
}
}
}
}, { typeName: 'User' })
console.log(check)
// Prints:
type User = {
firstName: string
lastName?: string
address: {
street1: string
street2?: string
city: string
state: "AL" | "AK" | "AZ" | "..."
}
}
function check(value: any): value is User {
return (
!!value &&
typeof value === "object" &&
typeof value.firstName === "string" &&
(!Object.hasOwn(value, "lastName") || typeof value.lastName === "string") &&
!!value.address &&
typeof value.address === "object" &&
typeof value.address.street1 === "string" &&
(!Object.hasOwn(value.address, "street2") ||
typeof value.address.street2 === "string") &&
typeof value.address.city === "string" &&
(value.address.state === "AL" ||
value.address.state === "AK" ||
value.address.state === "AZ" ||
value.address.state === "...")
)
}import { JsonSchema } from '@traversable/json-schema'
const check = JsonSchema.check.writeable({
$defs: {
state: { enum: ['AL', 'AK', 'AZ', '...'] },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: {
$ref: '#/$defs/state'
}
}
}
},
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
$ref: '#/$defs/address'
}
}
}, { typeName: 'User' })
console.log(check)
// Prints:
type State = "AL" | "AK" | "AZ" | "..."
type Address = {
street1: string
street2?: string
city: string
state: State
}
type User = {
firstName: string
lastName?: string
address: Address
}
function checkState(value: any) {
return value === "AL" || value === "AK" || value === "AZ" || value === "..."
}
function checkAddress(value: any) {
return (
!!value &&
typeof value === "object" &&
typeof value.street1 === "string" &&
(!Object.hasOwn(value, "street2") || typeof value.street2 === "string") &&
typeof value.city === "string" &&
checkState(value.state)
)
}
function check(value: any): value is User {
return (
!!value &&
typeof value === "object" &&
typeof value.firstName === "string" &&
(!Object.hasOwn(value, "lastName") || typeof value.lastName === "string") &&
checkAddress(value.address)
)
}See also
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.
Performance comparison
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 structuredCloneFor a more detailed breakdown, see all the benchmark results.
Example
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(sherlock, sherlockCloned) // => true
sherlock === sherlockCloned // => false
deepEqual(harry, harryCloned) // => true
harry === harryCloned // => falseSee also
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.
Example
import { JsonSchema } from '@traversable/json-schema'
const deepClone = JsonSchema.deepClone.writeable({
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { enum: ['AL', 'AK', 'AZ', '...'] }
}
}
}
}, { typeName: 'User' })
console.log(deepClone)
// Prints:
type User = {
firstName: string
lastName?: string
address: {
street1: string
street2?: string
city: string
state: "AL" | "AK" | "AZ" | "..."
}
}
function deepClone(prev: User): User {
return {
firstName: prev.firstName,
...(prev.lastName !== undefined && { lastName: prev.lastName }),
address: {
street1: prev.address.street1,
...(prev.address.street2 !== undefined && {
street2: prev.address.street2,
}),
city: prev.address.city,
state: prev.address.state,
},
}
}import { JsonSchema } from '@traversable/json-schema'
const deepClone = JsonSchema.deepClone.writeable({
$defs: {
state: { enum: ['AL', 'AK', 'AZ', '...'] },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: {
$ref: '#/$defs/state'
}
}
}
},
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
$ref: '#/$defs/address'
}
}
}, { typeName: 'User' })
console.log(deepClone)
// Prints:
type State = "AL" | "AK" | "AZ" | "..."
type Address = {
street1: string
street2?: string
city: string
state: State
}
type User = {
firstName: string
lastName?: string
address: Address
}
function deepCloneState(value: State): State {
return value
}
function deepCloneAddress(value: Address): Address {
return {
street1: value.street1,
...(value.street2 !== undefined && { street2: value.street2 }),
city: value.city,
state: deepCloneState(value.state),
}
}
function deepClone(prev: User): User {
return {
firstName: prev.firstName,
...(prev.lastName !== undefined && { lastName: prev.lastName }),
address: deepCloneAddress(prev.address),
}
}See also
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.
Performance comparison
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.
Notes
- Best performance
- Works in any environment that supports defining functions using the
Functionconstructor, including (as of May 2025) Cloudflare workers ๐
Example
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' }
) // => falseSee also
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.
Notes
- Useful when you're consuming a set of JSON Schemas and writing all them to disc somewhere
- Also useful for testing purposes or for troubleshooting, since it gives you a way to "see" exactly what the deepEqual functions are doing
Example
import { JsonSchema } from '@traversable/json-schema'
const deepEqual = JsonSchema.deepEqual.writeable({
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { enum: ['AL', 'AK', 'AZ', '...'] }
}
}
}
}, { typeName: 'User' })
console.log(deepEqual)
// Prints:
type User = {
firstName: string
lastName?: string
address: {
street1: string
street2?: string
city: string
state: "AL" | "AK" | "AZ" | "..."
}
}
function deepEqual(l: User, r: User): boolean {
if (l === r) return true
if (l.firstName !== r.firstName) return false
if ((l?.lastName === undefined || r?.lastName === undefined) && l?.lastName !== r?.lastName) return false
if (l?.lastName !== r?.lastName) return false
if (l.address !== r.address) {
if (l.address.street1 !== r.address.street1) return false
if (
(l.address?.street2 === undefined || r.address?.street2 === undefined) &&
l.address?.street2 !== r.address?.street2
)
return false
if (l.address?.street2 !== r.address?.street2) return false
if (l.address.city !== r.address.city) return false
if (l.address.state !== r.address.state) return false
}
return true
}import { JsonSchema } from '@traversable/json-schema'
const deepEqual = JsonSchema.deepEqual({
$defs: {
state: { enum: ['AL', 'AK', 'AZ', '...'] },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { $ref: '#/$defs/state' }
}
}
},
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: { $ref: '#/$defs/address' }
}
}, { typeName: 'User' })
console.log(deepEqual)
// Prints:
type State = "AL" | "AK" | "AZ" | "..."
type Address = {
street1: string
street2?: string
city: string
state: State
}
type User = {
firstName: string
lastName?: string
address: Address
}
function deepEqualState(l: State, r: State): boolean {
if (l !== r) return false
return true
}
function deepEqualAddress(l: Address, r: Address): boolean {
if (l.street1 !== r.street1) return false
if ((l?.street2 === undefined || r?.street2 === undefined) && l?.street2 !== r?.street2) return false
if (l?.street2 !== r?.street2) return false
if (l.city !== r.city) return false
if (!deepEqualState(l.state, r.state)) return false
return true
}
function deepEqual(l: User, r: User): boolean {
if (l === r) return true
if (l.firstName !== r.firstName) return false
if ((l?.lastName === undefined || r?.lastName === undefined) && l?.lastName !== r?.lastName) return false
if (l?.lastName !== r?.lastName) return false
if (!deepEqualAddress(l.address, r.address)) return false
return true
}See also
JsonSchema.toType
Convert a JSON Schema into its corresponding TypeScript type.
If the JSON Schema contains any references, the references will be compiled in a separate property of the return type.
Example
const UserType = JsonSchema.toType({
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { enum: ['AL', 'AK', 'AZ', '...'] }
}
}
}
}, { typeName: 'User' })
console.log(UserType.result)
// Prints:
type User = {
firstName: string
lastName?: string
address: {
street1: string
street2?: string
city: string
state: "AL" | "AK" | "AZ" | "..."
}
}import { JsonSchema, canonizeRefName } from '@traversable/json-schema'
const UserType = JsonSchema.toType({
$defs: {
state: { enum: ['AL', 'AK', 'AZ', '...'] },
address: {
type: 'object',
required: ['street1', 'city', 'state'],
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { $ref: '#/$defs/state' }
}
}
},
type: 'object',
required: ['firstName', 'address'],
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
address: {
$ref: '#/$defs/address'
}
}
}, { typeName: 'User' })
console.log([...Object.values(UserType.refs), UserType.result].join('\n'))
// Prints:
type State = "AL" | "AK" | "AZ" | "..."
type Address = {
street1: string
street2?: string
city: string
state: State
}
type User = {
firstName: string
lastName?: string
address: Address
}JsonSchema.fold
[!NOTE]
JsonSchema.foldis an advanced API.
Use JsonSchema.fold to define a recursive traversal of a JSON Schema. Useful when building a schema rewriter.
What does it do?
Writing an arbitrary traversal with JsonSchema.fold is:
- non-recursive
- 100% type-safe
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.
Example
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)
// transformed schema will be on the `result` property, transformed
// refs will be on the `refs` property
const { result: 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]) // falseThat's it!
If you'd like to see a more complex example, here's how JsonSchema.check is actually implemented.
Theory
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.
See also
JsonSchema.Functor
[!NOTE]
JsonSchema.Functoris 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.
