<br> <h1 align="center">แฏ๐๐ฟ๐ฎ๐๐ฒ๐ฟ๐๐ฎ๐ฏ๐น๐ฒ/๐ท๐๐ผ๐ป-๐๐ฐ๐ต๐ฒ๐บ๐ฎ</h1> <br>
npm install @traversable/json-schema
@traversable/json-schema is a schema rewriter for JSON Schema specs.
> [!NOTE]
> Currently this package only supports JSON Schema Draft 2020-12
``bash`
$ pnpm add @traversable/json-schema
Here's an example of importing the library:
`typescript
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.toType
- JsonSchema.fold
- JsonSchema.Functor
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 Function constructor, 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
`typescript
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
`
#### See 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
Without references:
`typescript
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 === "...")
)
}
`
With references:
`typescript
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.check
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.
Click to see the detailed benchmark summary
`
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.
#### Example
`typescript
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 // => false
`
#### See 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
Without references:
`typescript
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,
},
}
}
`
With references:
`typescript
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.deepClone
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 Function constructor, including (as of May 2025) Cloudflare workers ๐
#### Example
`typescript
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
`
#### See 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
Without references:
`typescript
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
}
`
With references:
`typescript
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.deepEqual
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
Without references:
`typescript
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" | "..."
}
}
`
With references:
`typescript
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
}
`
> [!NOTE]
> JsonSchema.fold is 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:
1. non-recursive
2. 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:
`typescript
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, transformedrefs
// refs will be on the 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]) // false
`
That'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.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.
#### See also
- JsonSchema.fold`