schematox
v1.2.0
Published
Define JSON compatible schema statically/programmatically and parse its subject with typesafety
Maintainers
Readme
Schematox
Schematox is a lightweight typesafe schema defined parser. All schemas are JSON compatible.
The library is focusing on fixed set of schema types: boolean, literal, number, string, array, object, record, tuple, union. Each schema can have parameters: optional, nullable, description. Each primitive schema has "brand" parameter as mean of making its subject type nominal. The rest parameters is schema specific range limiters.
Library supports static schema definition which means your schemas could be completely independent from schematox. One could use such schemas as source for generation other structures like DB models.
The library is small so exploring README.md is enough for understanding its API, checkout examples and you good to go:
- Install
- Minimal Requirements
- Features
- Quick Start
- Primitive Schema
- Compound Schema
- Schema Parameters
- Error Shape
Install
npm install schematoxMinimal Requirements
- ECMAScript version:
2018 - TypeScript version:
5.3.2
Features
- Statically defined JSON compatible schema
- Programmatically defined schema (struct, construct)
- Check defined schema correctness using non generic type Schema
- Ether-style error handling (no unexpected throws)
- First-class support for branded primitives (primitive nominal types alias)
- Construct type requirement for schema itself using exposed type generics
- Support the standard schema - a common interface for TypeScript validation libraries
Quick Start
One can use three ways of schema definition using schematox library:
- Static: a JSON-compatible object that structurally conforms to the
Schematype - Struct: is commonly accepted way of schema definition, as seen in zod or superstruct
- Construct: use
makeStructfunction to getstructfromstaticschema
All programmatically defined (struct, construct) schemas are eventually based on static, which could be accessed by __schema key. All schemas must be immutable constants and should not be mutated by the user. Each application of struct parameters related to schema update will create shallow copy of the original schema.
Static schema
A JSON-compatible object that structurally conforms to the Schema type.
The satisfies Schema check is optional, structurally valid schema will be accepted by the parser.
import { parse } from 'schematox'
import type { Schema, Infer } from 'schematox'
export const schema = {
type: 'object',
of: {
id: {
type: 'string',
brand: ['idFor', 'User'],
},
name: { type: 'string' },
},
} as const satisfies Schema
type User = Infer<typeof schema>
// ^? { id: string & { __idFor: 'User' }, name: string }
const subject = { id: '1' name: 'John' }
const parsed = parse(userSchema, subject)
// ^? ParseResult<User>
parsed.error
// ^? InvalidSubject[] | undefined
parsed.data
// ^? User | undefined
if (parsed.success === false) {
parsed.error
// ^? InvalidSubject[]
throw Error('Parsing error')
}
parsed.data
// ^? UserStruct
Is commonly accepted way of schema, as seen in zod or superstruct library:
import { object, string } from 'schematox'
import type { Infer } from 'schematox'
const struct = object({
id: string().brand('idFor', 'User'),
name: string(),
})
type User = Infer<typeof struct>
// ^? { id: string & { __idFor: 'User' }, name: string }
const subject = { id: '1', name: 'John' }
const parsed = struct.parse(subject)
// ^? ParseResult<User>
parsed.error
// ^? InvalidSubject[] | undefined
parsed.data
// ^? User | undefined
if (parsed.success === false) {
parsed.error
// ^? InvalidSubject[]
throw Error('Parsing error')
}
parsed.data
// ^? UserConstruct
import { makeStruct } from 'schematox'
import type { Schema } from 'schematox'
const schema = { type: 'string' } as const satisfies Schema
const string = makeStruct(schema)Primitive Schema
Any schema share optional/nullable/description/brand parameters.
Boolean
const schema = {
type: 'boolean',
optional: true,
nullable: true,
brand: ['x', 'y'],
description: 'x',
} as const satisfies Schema
const struct = boolean() //
.optional()
.nullable()
.brand('x', 'y')
.description('x')
// (boolean & { __x: 'y' }) | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>String
const schema = {
type: 'string',
optional: true,
nullable: true,
brand: ['x', 'y'],
minLength: 1,
maxLength: 2,
description: 'x',
} as const satisfies Schema
const struct = string()
.optional()
.nullable()
.brand('x', 'y')
.minLength(1)
.maxLength(2)
.description('x')
// (string & { __x: 'y' }) | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Literal
Could be string/number/boolean literal.
const schema = {
type: 'literal',
of: 'x',
optional: true,
nullable: true,
brand: ['x', 'y'],
description: 'x',
} as const satisfies Schema
const struct = literal('x') //
.optional()
.nullable()
.brand('x', 'y')
.description('x')
// ('x' & { __x: 'y' }) | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Number
We accept only finite numbers as valid number schema subjects.
const schema = {
type: 'number',
optional: true,
nullable: true,
brand: ['x', 'y'],
min: 1,
max: 2,
description: 'x',
} as const satisfies Schema
const struct = number()
.optional()
.nullable()
.brand('x', 'y')
.min(1)
.max(2)
.description('x')
// (number & { __x: 'y' }) | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>
//Compound Schema
Any compound schema could have any other schema type as its member including itself.
Array
const schema = {
type: 'array',
of: { type: 'string' },
optional: true,
minLength: 1,
maxLength: 1000,
description: 'x',
} as const satisfies Schema
const struct = array(string())
.optional()
.nullable()
.minLength(1)
.maxLength(1000)
.description('x')
// string[] | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Object
Extra properties in the parsed subject that are not specified in the object schema will not cause an error and will be skipped.
This is a deliberate decision that allows client schemas to remain functional whenever the API is extended.
const schema = {
type: 'object',
of: {
x: { type: 'string' },
y: { type: 'number' },
},
optional: true,
nullable: true,
description: 'x',
} as const satisfies Schema
const struct = object({
x: string(),
y: number(),
})
.optional()
.nullable()
.description('x')
// { x: string; y: number } | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Record
Undefined record entries are skipped in parsed results and ignored by range limiter counter. If a key exists, it means a value is also present.
const schema = {
type: 'record',
key: { type: 'string', brand: ['idFor', 'user'] },
of: { type: 'number' },
minLength: 1,
maxLength: 1,
optional: true,
nullable: true,
description: 'x',
} as const satisfies Schema
const userId = string().brand('idFor', 'user')
const struct = record(number(), userId)
.minLength(1)
.maxLength(1)
.optional()
.nullable()
.description('x')
// Record<string & { __brand: ['idFor', 'user'] }, number> | null | undefined
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Tuple
const schema = {
type: 'tuple',
of: [{ type: 'string' }, { type: 'number' }],
optional: true,
nullable: true,
description: 'x',
} as const satisfies Schema
const struct = tuple([string(), number()])
.optional()
.nullable()
.description('x')
// [string, number] | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Union
Be careful with object unions that do not have a unique discriminant. The parser will check the subject in the order that is specified in the union array and accept the first match.
const schema = {
type: 'union',
of: [{ type: 'string' }, { type: 'number' }],
optional: true,
nullable: true,
description: 'x',
} as const satisfies Schema
const struct = union([string(), number()])
.optional()
.nullable()
.description('x')
// string | number | undefined | null
type FromSchema = Infer<typeof schema>
type FromStruct = Infer<typeof struct>Schema Parameters
optional?: boolean– unionize withundefined:{ type: 'string', optinoal: true }result instring | undefinednullable?: boolean– unionize withnull:{ type: 'string', nullable: true }result instring | nullbrand?: [string, unknown]– make primitive type nominal "['idFor', 'User'] -> T & { __idFor: 'User' }"minLength/maxLength/min/max– schema type dependent limiting characteristicsdescription?: string– description of the particular schema property which can be used to provide more detailed information for the user/developer on validation/parse error
Error Shape
Nested schema example. Subject 0 is invalid, should be a string:
import { object, array, string } from 'schematox'
const struct = object({
x: object({
y: array(
object({
z: string(),
})
),
}),
})
const result = struct.parse({ x: { y: [{ z: 0 }] } })The result.error shape is:
[
{
"code": "INVALID_TYPE",
"path": ["x", "y", 0, "z"]
"schema": { "type": "string" },
"subject": 0,
}
]It's always an array of InvalidSubject entries, each has the following properties:
code:INVALID_TYPE: schema subject or default value don't meet schema type specificationsINVALID_RANGE:min/maxorminLength/maxLengthschema requirements aren't met
schema: the specific section ofschemawhere the invalid value is found.subject: the specific part of the validated subject where the invalid value exists.path: traces the route from the root to the error subject, with strings as keys and numbers as array indexes.
