Skip to content

About

Fast, zero-dependency metadata schema and interface definition language (IDL) for Node.js and browsers — validation and parsing with a JIT backend, domain models, TypeScript and JSON Schema

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

metaschema

npm CI node dependencies docs license

Fast, zero-dependency metadata schema and interface definition language for JavaScript. Declare data structures and domain models once as plain objects; validate and parse data against them at tens of millions of checks a second, with every problem returned as data; and generate TypeScript types and JSON Schema from the same definitions. Node.js and browsers.

const { Schema } = require('@alexify/metaschema');

const user = new Schema('User', {
  name: { first: 'string', last: 'string' },
  email: { type: 'string', length: { min: 5, max: 64 } },
  age: '?number',
  roles: { array: { enum: ['admin', 'editor', 'viewer'] } },
});

const result = user.check({ name: { first: 'Marcus' }, email: 'm@r', roles: ['owner'] });

result.errors;
// [
//   'Field "User.name.last" is required',
//   'Field "User.email" value is too short',
//   'Field "User.roles[0]" value is not of enum: admin, editor, viewer'
// ]

result.issues;
// [
//   { code: 'required', path: ['name', 'last'], message: 'is required', params: {} },
//   { code: 'length', path: ['email'], message: 'value is too short', params: { min: 5, max: 64, actual: 3 } },
//   { code: 'enum', path: ['roles', 0], message: 'value is not of enum: admin, editor, viewer', params: { values: ['admin', 'editor', 'viewer'] } }
// ]

const event = Schema.from({
  name: 'string',
  'level?': { type: 'integer', default: 1 },
  when: { type: 'string', parse: (text) => new Date(text) },
});
event.parse({ name: 'deploy', when: '2026-10-10T12:00:00.000Z' }).value;
// { name: 'deploy', when: 2026-10-10T12:00:00.000Z, level: 1 }

Why metaschema

  • Compact syntax. '?string' is an optional string, 'tags?' an optional key, { array: 'number' } an array, ['number', 'number'] a tuple, 'Company' a reference. The long form ({ type: 'string', length: [3, 32] }) is there when a field needs options.
  • Errors, not exceptions. check walks the whole value and returns every problem as data: a code, the path as an array of keys, the params it was made from, and a message rendered through a locale (English built in, @alexify/metaschema/locales/uk shipped). Only a broken definition throws.
  • Parse, not only a verdict. parse runs the same check and gives the value of the program: defaults filled in, unknown keys stripped on request, a field's parse function decoding its value (a date-time string into a Date) and serialize the way back; checkAsync and parseAsync await validators and decoders that return promises.
  • String formats. { type: 'string', format: 'email' } is a rule beside length and pattern; @alexify/metaschema/formats registers 35 formats (uuid, ulid, jwt, email, date-time, duration, ipv4, cidrv6, url, json-pointer, e164, iban, base64, semver, …) and your own, and the name travels into JSON Schema, samples and inference.
  • Coercion. parse with coerce: true (a call, a schema or a field) converts the text of a query string, a form or a CSV row to the types of the fields before the check ('42' to a number, an ISO string to a Date), strictly and never in check; coerce() of @alexify/metaschema/coerce is that step alone.
  • Deriving schemas. pick, omit, partial (deep too), required, extend and keyof of @alexify/metaschema/derive make the create and update bodies and the views of an entity from the entity, typed by its definition.
  • MongoDB. @alexify/metaschema/mongodb is the schema side of Mongoose without the driver dependency: BSON types, _id and ObjectId references written and read back, the indexes and the collection validator of a model, and the driver's updates checked against the schema.
  • Records and streams. checkEach and parseEach of @alexify/metaschema/stream run a schema over an iterable of records, lazily and in order, yielding { index, record, result } for each; the async pair keeps concurrency records in flight over a sync or async source, and transform is a TransformStream that lets the valid records through and reports the others.
  • Domain models. Entities, registries, dictionaries and projections, with references, relations and indexes, ordered by dependency and checked for missing references.
  • TypeScript. InferSchema<typeof schema> is the type of a value the schema accepts, computed from the definition with nothing generated, and a model renders its entities as interfaces. The package ships hand-written typings for its own API.
  • Standard Schema. Every schema implements Standard Schema v1 (schema['~standard'], which is parse), so tRPC, TanStack Form, Hono and any other consumer of the interface take it as they take a zod or valibot schema.
  • JSON Schema. toJSONSchema renders a schema or a whole model as JSON Schema draft 2020-12 or draft-07, an OpenAPI 3.0 schema object or a MongoDB $jsonSchema, with a strict profile for LLM structured outputs; ~standard.jsonSchema is the Standard JSON Schema converter. The generators live in the @alexify/metaschema/generators subpath, so a program that only validates does not carry them.
  • Fast. Every check is compiled into a closure on its first use and runs in a context of its own: about 10 million validations a second of a flat struct on Node 24, and about 30 million through the JIT backend of @alexify/metaschema/compile, which generates the same check where new Function is allowed and keeps the closures where it is not. Against ten other validators see Benchmarks; how the core gets there is on the Performance page.
  • The model as data. model.toJSON() is a snapshot other programs read the model through (a migration tool, an introspecting service, a committed baseline), schema.toSnapshot() the same document for one schema and the entities it reaches (a codec built from it), with the guarantees of format 1 written down, and @alexify/metaschema/model tells two versions apart (diff), classifies the change by the Confluent rules (compatibility), draws a value every schema accepts (sample), unifies two schemas (merge) and infers a definition from the values that exist (infer); aliases and deprecated let a model carry its history, and fromJSONSchema of the generators reads a JSON Schema document back.
  • Zero dependencies, 21.3 KB min+gzip. CommonJS with ESM named imports, no build step, one package for Node.js and browsers; the generators add 7.7 KB, the JIT backend 4.3 KB, the model tools 12.7 KB, the record tools 0.9 KB, the string formats 3.9 KB, the derive tools 2.2 KB, the converters of coercion 1.9 KB and the MongoDB tools 3.6 KB (on top of the converters, which they require) where they are imported. A program that reads schemas without validating them (a client that encodes from their snapshots) imports @alexify/metaschema/definition, the half of the core that builds them: 10.3 KB.

Benchmarks

metaschema on its two backends beside Metarhia's metaschema, Zod, Valibot, ArkType, Effect Schema, TypeBox, Ajv, Yup, Joi and Superstruct: the object of the moltar suite in its four modes, every issue of an invalid value and building a schema, each library in a fresh process and checked for the right verdicts before it is timed. On the JIT metaschema validates 30 to 48 million objects a second, and in the strict modes (unknown keys rejected) only TypeBox's compiler is faster; on its closures, with no eval at all, 4.6 to 9.8 million, ahead of every interpreter in the set. Asked for every issue of an invalid value with its path, it is second only to Ajv, and 6 to 100 times faster than the other TypeScript-first validators; a cold start (load, build, first check) takes 5.7 ms.

Nested object, parseStrict: A valid object of 6 fields and a nested object of 3. parseStrict: unknown keys rejected, the value returned. ops/s, log scale, higher is better.

9 more scenarios Flat object, parseSafe: A valid object of 6 fields. parseSafe: unknown keys allowed, the value returned. ops/s, log scale, higher is better. Not shown, the mode cannot be expressed: Metarhia metaschema. Flat object, parseStrict: A valid object of 6 fields. parseStrict: unknown keys rejected, the value returned. ops/s, log scale, higher is better. Flat object, assertLoose: A valid object of 6 fields. assertLoose: unknown keys allowed, the verdict only. ops/s, log scale, higher is better. Not shown, the mode cannot be expressed: Metarhia metaschema. Flat object, assertStrict: A valid object of 6 fields. assertStrict: unknown keys rejected, the verdict only. ops/s, log scale, higher is better. Nested object, parseSafe: A valid object of 6 fields and a nested object of 3. parseSafe: unknown keys allowed, the value returned. ops/s, log scale, higher is better. Not shown, the mode cannot be expressed: Metarhia metaschema. Nested object, assertLoose: A valid object of 6 fields and a nested object of 3. assertLoose: unknown keys allowed, the verdict only. ops/s, log scale, higher is better. Not shown, the mode cannot be expressed: Metarhia metaschema. Nested object, assertStrict: A valid object of 6 fields and a nested object of 3. assertStrict: unknown keys rejected, the verdict only. ops/s, log scale, higher is better. Every issue of an invalid value: The nested object with four wrong fields, unknown keys rejected; every issue collected with its path. ops/s, log scale, higher is better. Build a schema: The strict nested schema built from its definition, ready to check. schemas/s, log scale, higher is better.
Library Flat object, parseSafe
ops/s
Flat object, parseStrict
ops/s
Flat object, assertLoose
ops/s
Flat object, assertStrict
ops/s
Nested object, parseSafe
ops/s
Nested object, parseStrict
ops/s
Nested object, assertLoose
ops/s
Nested object, assertStrict
ops/s
Every issue of an invalid value
ops/s
Build a schema
schemas/s
metaschema
check() · compile(), JIT · v3.3.0
48,400,000 38,900,000 49,700,000 40,800,000 42,000,000 29,900,000 44,100,000 30,300,000 6,650,000 32,300
metaschema
check() · closures · v3.3.0
9,430,000 9,020,000 10,000,000 9,200,000 5,100,000 4,810,000 5,090,000 4,750,000 3,000,000 190,000
Metarhia metaschema
check() · strict only · v2.2.2
— 958,000 — 959,000 — 550,000 — 546,000 428,000 282,000
Zod
safeParse() · v4.6.6
26,500,000 12,800,000 27,800,000 12,700,000 16,700,000 7,340,000 16,900,000 7,160,000 445,000 82,100
Valibot
safeParse() · is() · v1.5.0
3,570,000 2,950,000 3,620,000 3,010,000 2,130,000 1,800,000 2,080,000 1,780,000 1,150,000 2,050,000
ArkType
type(value) · allows() · v2.2.8
50,500,000 7,400,000 171,000,000 8,640,000 38,100,000 4,020,000 134,000,000 4,430,000 68,000 14,600
Effect Schema
decodeUnknownResult() · is() · v4.0.3
4,200,000 1,970,000 4,220,000 1,980,000 2,310,000 1,000,000 2,310,000 1,000,000 359,000 672,000
TypeBox
Compile(…).Check() · v1.3.36
165,000,000 79,900,000 188,000,000 83,700,000 119,000,000 43,200,000 136,000,000 43,800,000 90,600 36,200
TypeBox
Value.Check() · v1.3.36
263,000 191,000 264,000 192,000 170,000 118,000 167,000 114,000 89,400 422,000
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
65,700,000 34,600,000 71,900,000 35,300,000 51,500,000 23,500,000 55,400,000 24,100,000 15,000,000 1,840
Yup
validateSync() · isValidSync(), strict · v1.7.1
313,000 298,000 314,000 295,000 184,000 175,000 185,000 168,000 11,800 53,900
Joi
validate(), convert: false · v18.2.9
851,000 843,000 834,000 855,000 534,000 543,000 535,000 525,000 257,000 37,400
Superstruct
validate() · is() · v2.0.2
492,000 453,000 496,000 443,000 304,000 266,000 299,000 257,000 100,000 1,770,000

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each library in a fresh process · 2026-10-13. Medians, rounded to three significant figures; — where the library cannot express the mode.

How to read these numbers and the method behind them are on the Benchmarks page, where the charts are interactive.

Cold start

Cold start: Load the library, build the strict nested schema, check one value: a fresh process, median of 20 runs. Lower is better.

Library Load Build First check Total
Superstruct
validate() · is() · v2.0.2
0.7 ms 0.2 ms 0.3 ms 1.2 ms
Valibot
safeParse() · is() · v1.5.0
3.7 ms 0.1 ms 0.1 ms 3.9 ms
Yup
validateSync() · isValidSync(), strict · v1.7.1
2.4 ms 1.1 ms 0.7 ms 4.2 ms
metaschema
check() · closures · v3.3.0
4.7 ms 1.0 ms 0.4 ms 6.1 ms
metaschema
check() · compile(), JIT · v3.3.0
5.9 ms 1.7 ms 0.2 ms 7.8 ms
Metarhia metaschema
check() · strict only · v2.2.2
20.7 ms 0.6 ms 0.2 ms 21.5 ms
Joi
validate(), convert: false · v18.2.9
23.1 ms 1.2 ms 0.6 ms 25.0 ms
Zod
safeParse() · v4.6.6
27.7 ms 1.5 ms 1.2 ms 30.3 ms
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
14.6 ms 24.4 ms 0.2 ms 39.3 ms
Effect Schema
decodeUnknownResult() · is() · v4.0.3
72.4 ms 0.2 ms 0.8 ms 73.4 ms
TypeBox
Value.Check() · v1.3.36
96.6 ms 0.1 ms 1.1 ms 97.8 ms
TypeBox
Compile(…).Check() · v1.3.36
96.9 ms 1.9 ms 0.0 ms 98.9 ms
ArkType
type(value) · allows() · v2.2.8
107.8 ms 1.4 ms 0.1 ms 109.3 ms

Bundle size

Bundle size: A browser bundle that builds the strict nested schema and checks a value (esbuild, minified, tree-shaken), min+gzip. Lower is better.

Library min min+gzip
Valibot
safeParse() · is() · v1.5.0
3.3 KB 1.2 KB
Superstruct
validate() · is() · v2.0.2
3.5 KB 1.5 KB
Metarhia metaschema
check() · strict only · v2.2.2
30.0 KB 11.0 KB minified with keepNames: its checks compare constructor names
Yup
validateSync() · isValidSync(), strict · v1.7.1
41.4 KB 13.2 KB
metaschema
check() · closures · v3.3.0
52.6 KB 18.9 KB
metaschema
check() · compile(), JIT · v3.3.0
65.2 KB 23.1 KB
Effect Schema
decodeUnknownResult() · is() · v4.0.3
72.9 KB 24.4 KB
Zod
safeParse() · v4.6.6
88.4 KB 25.7 KB
TypeBox
Value.Check() · v1.3.36
146.7 KB 37.5 KB
Ajv
compile(…)(value), draft 2020-12 · v8.20.0
130.9 KB 39.5 KB
TypeBox
Compile(…).Check() · v1.3.36
161.5 KB 40.8 KB
ArkType
type(value) · allows() · v2.2.8
151.9 KB 46.4 KB
Joi
validate(), convert: false · v18.2.9
169.5 KB 53.3 KB

Release over release

metaschema 3.3.0 against Metarhia's metaschema 2.2.2: How many times faster, scenario by scenario (1 = as fast). Higher is better.

Every release, scenario by scenario check — nested struct, valid, release over release: ops/s, linear scale, higher is better. The same harness and machine for every release. Schema.from — flat struct, release over release: schemas/s, linear scale, higher is better. The same harness and machine for every release. Schema.from — nested struct, release over release: schemas/s, linear scale, higher is better. The same harness and machine for every release. check — flat struct, valid, release over release: ops/s, linear scale, higher is better. The same harness and machine for every release. check — flat struct, invalid, release over release: ops/s, linear scale, higher is better. The same harness and machine for every release. check — nested struct, invalid, release over release: ops/s, linear scale, higher is better. The same harness and machine for every release.
Scenario Metarhia 2.2.2 1.0.0 2.0.0 3.0.0 3.0.0
JIT
3.1.0 3.1.0
JIT
3.2.0 3.2.0
JIT
3.3.0 3.3.0
JIT
Schema.from — flat struct 581,000 321,000 362,000 372,000 — 367,000 — 356,000 — 332,000 —
Schema.from — nested struct 156,000 95,600 104,000 99,000 — 98,400 — 98,600 — 91,100 —
check — flat struct, valid 1,440,000 2,180,000 10,800,000 10,700,000 38,600,000 11,700,000 37,900,000 11,600,000 37,900,000 11,600,000 38,200,000
check — flat struct, invalid 1,010,000 1,110,000 4,020,000 3,940,000 6,130,000 3,880,000 6,170,000 3,900,000 6,160,000 3,850,000 6,240,000
check — nested struct, valid 238,000 368,000 2,950,000 2,930,000 19,700,000 2,920,000 19,600,000 2,910,000 19,700,000 2,910,000 19,700,000
check — nested struct, invalid 222,000 300,000 1,570,000 1,580,000 2,140,000 1,570,000 2,110,000 1,570,000 2,110,000 1,590,000 2,120,000

Apple M3 Max · 16 cores · Node v24.14.1 · median of 5 × 1 s samples, each release in a fresh process · 2026-10-13. ops/s (schemas/s for Schema.from), medians.

metaschema on its two backends

metaschema on its two backends: pnpm bench: ops/s, log scale, higher is better.

Compared with alternatives

metaschema against twenty alternatives in four directions: TypeScript-first validators, JSON Schema validators and compilers, interface definition languages built for schema evolution, and an ODM. Every cell was checked against the source of the published package (October 2026), not its README; the comparison page has the method, what the table shows and where each library fits, in a table you can scroll with the features pinned.

Feature metaschema
schemas + IDL
Metarhia metaschema
IDL, the original
Zod
TS-first
Valibot
TS-first
ArkType
TS-first
Effect Schema
TS-first
Superstruct
TS-first
io-ts
TS-first
Runtypes
TS-first
myzod
TS-first
Valita
TS-first
Yup
validator
Joi
validator
VineJS
validator
class-validator
decorators
Ajv
JSON Schema
TypeBox
JSON Schema
typia
AOT compiler
avsc
IDL · Avro
protobuf.js
IDL · Protobuf
Mongoose
ODM
Package
Version compared3.4.02.2.24.6.61.5.02.2.84.0.32.0.212.2.227.0.51.12.120.5.51.7.118.2.94.4.00.15.18.20.01.3.36315.1.05.7.98.8.19.11.1
Latest release2026-10 (this release)2025-052026-102026-092026-102026-102024-072024-122026-082025-022026-102025-092026-092026-042026-022026-042026-102026-102025-072026-102026-10
Runtime dependencies✅ 02: metautil, metavm✅ 0✅ 03: @ark/schema, @ark/util, arkregex✅ 0✅ 00 + peer fp-ts✅ 0✅ 0✅ 04: property-expr, tiny-case, toposort, type-fest7 (@hapi/*, …)10 (validator.js, dayjs, …)3 (validator.js, libphonenumber-js, …)4✅ 03: randexp, @typia/interface, @typia/utils✅ 01: long7, incl. mongodb driver
LicenseMITMITMITMITMITMITMITMITMITMITMITMITBSD-3-ClauseMITMITMITMITMITMITBSD-3-ClauseMIT
Runtime
Runs on✅ Node 18+, browsers, edge⚠️ Node; browsers via bundler4✅ Node, browsers, Deno, Bun, edge✅ Node, browsers, Deno, Bun, edge✅ Node, browsers, Deno, Bun, edge✅ Node, browsers, Deno, Bun, edge✅ Node ≥14, browsers, edge✅ Node, browsers, edge✅ Node, Deno (JSR), browsers, edge⚠️ Node; browsers/edge via a bundler✅ Node ≥22, Deno (JSR), browsers, edge✅ Node, browsers, Deno, Bun, edge⚠️ Node ≥ 20; browsers via a separate bundle5⚠️ Node, Bun, Deno; browsers if eval is allowed6✅ Node, browsers, Deno, Bun, edge⚠️ Node, browsers; CSP/edge need standalone7✅ Node, browsers, Deno, Bun, edge✅ generated JS runs anywhere⚠️ Node; browsers need polyfills8⚠️ Node, browsers, Deno, Bun⚠️ Node ≥ 20.19; browser: @mongoosejs/browser9
Module formatCJS, ESM named importsCJSESM + CJSESM + CJSESMESMESM + CJSCJS + ESM (module field)ESM + CJSCJSESMESM + CJSCJSESMESM + CJSCJSESMESM + CJSCJSCJS (+ UMD dist)CJS
Works without eval (strict CSP, edge)✅ closures; JIT opt-in, falls back✅ never (validation)⚠️ object JIT by default; auto fallback10✅ never⚠️ JIT by default; auto jitless under CSP✅ never by default (JIT opt-in)✅ never✅ never✅ never✅ never✅ never✅ never✅ never❌ new AsyncFunction per schema✅ never⚠️ new Function; standalone precompile⚠️ Compile evals, falls back; Value never✅ code is generated at build time❌ needs new Function⚠️ reflection evals; static code not✅ never (validation)
Works without a build step✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS✅ plain JS⚠️ legacy TS decorators, or by hand11✅ plain JS; standalone optional✅ plain JS❌ requires ttsc transform (TypeScript 7)12✅ plain JS⚠️ optional pbjs codegen✅ plain JS
Defining schemas
How a schema is writtenplain object: '?string', { array: 'number' }plain object literal ('?string', kind key)builder / method chain (zod/mini: functional)functions + pipe()TS-syntax strings in object literals + chainingbuilder values: Schema.Struct({...}).pipe/.checkfunction combinators: object({ a: string() })codec combinators: t.type({ a: t.string })runtype values: Object({ a: String })builder chain: myzod.object({ a: myzod.string() })builder chain: v.object({ a: v.string() })builder / method chainbuilder / method chainbuilder / method chain (mutable)decorators on classesJSON Schema or JTD documentbuilder (Type.Object…); TS-syntax strings via Type.ScriptTS types + transformer; type tagsAvro schema JSON (.avsc) or IDL.proto IDL or JSON descriptorMongoose Schema (SchemaType options)
The definition is data (store, send, rebuild)✅ plain objects (functions aside)✅ plain objects (functions aside)⚠️ via toJSONSchema / fromJSONSchema❌ export only (@valibot/to-json-schema)✅ string/object defs; .json IR✅ SchemaRepresentation.toJson / fromJson❌ no❌ no❌ no❌ no❌ no❌ describe() is one-way✅ describe() ↔ Joi.build()❌ toJSON() holds functions❌ registerSchema() is broken13✅ JSON Schema / JTD documents✅ types are JSON Schema objects⚠️ only via json.schema + @typia/utils✅ Avro JSON schema✅ JSON descriptors⚠️ via toJSONSchema() only
Optional vs nullable✅ '?x', 'key?', nullable: true⚠️ '?type' = absent/undefined/null✅ optional, exactOptional, nullable, nullish✅ optional, exactOptional, nullable, nullish, undefinedable✅ 'key?' (no undefined), '| null', '| undefined'✅ optionalKey, optional, NullOr, UndefinedOr, NullishOr✅ optional(), nullable()✅ t.partial vs t.union([X, t.null])✅ optional(), nullable(), undefinedable()✅ optional(), nullable()✅ optional(), nullable()✅ optional() / nullable() / defined()✅ optional default; allow(null); required()⚠️ optional() also accepts null14⚠️ IsOptional() skips null and undefined✅ required vs type null / nullable✅ Optional, Union with Null, Undefined✅ TS ?:, | null, | undefined⚠️ ['null', T] union; absent needs default⚠️ presence only; null = unset⚠️ optional = absent/undefined/null
Unions, discriminated✅ union, discriminator❌ none✅ union, discriminatedUnion, xor✅ union, variant (discriminated)✅ 'a | b', .or(); auto-discriminated✅ Union, Literals, TaggedUnion⚠️ union(); no discriminated union✅ union(), tag detected automatically⚠️ Union(...); no discriminator⚠️ union([..]) / or(); no discriminator✅ union(), auto-discriminated❌ no union type✅ alternatives(); conditional() switch✅ union() with if(), unionOfTypes()⚠️ class-transformer discriminator✅ anyOf / oneOf; discriminator opt-in✅ Type.Union (anyOf); no discriminator✅ TS unions; discriminant detected✅ unions; wrapped = tagged by name✅ oneof (virtual discriminator)✅ Schema.Types.Union; discriminators
Tuples✅ ['number', 'string']⚠️ array shorthand, scalars only✅ z.tuple with rest✅ tuple, strictTuple, tupleWithRest✅ ['string', 'number'], variadic '...'✅ Tuple, TupleWithRest✅ tuple([..])⚠️ tuple([..]) strips extra items15✅ Tuple(...), with Spread rest✅ tuple([..])✅ tuple([..]), variadic via concat16✅ tuple()✅ array().ordered()✅ vine.tuple()❌ none✅ prefixItems + items: false✅ Type.Tuple✅ TS tuples❌ none❌ none❌ none
Recursive schemas✅ by name, a schema its own without a model; cycles tracked✅ by entity name in a Model✅ getters or z.lazy✅ v.lazy✅ scope names, 'this'✅ Schema.suspend✅ lazy(() => S)✅ t.recursion(name, f)✅ Lazy(() => R)✅ lazy(() => S)✅ lazy(() => T)✅ lazy()✅ link('#id')❌ none✅ @ValidateNested + @Type(() => Self)✅ $ref, $dynamicRef✅ Type.Cyclic, Type.Module✅ recursive TS types✅ named types✅ self-referencing messages✅ schema.add({ self: schema })
Schemas referenced by name✅ capitalized names in a Model✅ capitalized names via Model❌ JS variables only❌ JS variables only✅ scope({ User, Group }) by name❌ JS variables only❌ none❌ none❌ none❌ none❌ none❌ none (ref() is a value ref)✅ id() + link('#id'), shared()❌ none❌ none✅ $ref by $id; ajv.addSchema✅ Type.Ref + context, Module, $id✅ TS type names✅ names + namespaces, registry✅ full names via Root⚠️ ref: 'Model' for populate
Custom types registered by name✅ registered by name✅ Model types: js alias or checkType❌ z.custom() values, no name registry❌ v.custom() values, no name registry✅ scope aliases, generics, keywords⚠️ Schema.declare; reviver ids for restore⚠️ define(name, fn); no registry⚠️ new t.Type(name, is, validate, encode); no registry⚠️ withBrand/withConstraint; no registry⚠️ withPredicate/map on a base type; no registry⚠️ unknown().assert()/chain(); no registry⚠️ addMethod(), mixed(check)✅ Joi.extend({ type })⚠️ createRule(), macro() methods✅ registerDecorator(), @ValidatorConstraint⚠️ addKeyword / addSchema, not types⚠️ Unsafe + Refine; Script/Module names✅ custom tags (tags.TagBase)✅ logicalTypes classes, typeHook❌ messages and enums only✅ Schema.Types.Name = class
String formats✅ 35 + your own (./formats)❌ none✅ 30+ (email, uuid, url, ip, iso.datetime…)✅ ~37 actions (email, uuid, url, isoDateTime…)✅ ~18 string.* (email, uuid, url, ip…)✅ uuid, ulid, base64, URL, IP; no email❌ none (pattern() only)⚠️ io-ts-types: UUID, ISO dates❌ none (Template literals)❌ none (pattern() only)❌ none✅ 4: email, url, uuid, datetime17✅ 14: email, uri, uuid, ip, isoDate...✅ 18: email, url, uuid, ulid, ip, iban, jwt...✅ 50+ via validator.js (IsEmail, IsUUID, IsUrl...)✅ ajv-formats: 2618✅ 21 built in19✅ 22 via tags.Format❌ none❌ none❌ none (match: RegExp)
Date, BigInt, Map, Set✅ Date, BigInt, Map, Set, Uint8Array; BSON (./mongodb)⚠️ BigInt, Map, Set; no Date✅ Date, BigInt, Map, Set, Symbol, File✅ Date, BigInt, Map, Set, Blob, File✅ Date, bigint, Map, Set, symbol, Blob, File…✅ Date, BigInt, ReadonlyMap, ReadonlySet, Symbol…✅ Date, BigInt, Map, Set⚠️ BigInt; Date/Map/Set via io-ts-types⚠️ BigInt, Symbol; Date/Map/Set via InstanceOf⚠️ Date, BigInt⚠️ BigInt only⚠️ Date⚠️ Date, Buffer, symbol, function⚠️ Date (output), File⚠️ Date; Set/Map via each⚠️ Date via ajv-keywords or JTD⚠️ BigInt, Symbol, Undefined; no Date/Map/Set20✅ Date, bigint, Map, Set, typed arrays21⚠️ Buffer; Date, BigInt via custom types22⚠️ Uint8Array, Long; BigInt via fromObject23✅ Date, BigInt, Map, Buffer
Custom validators✅ validate on a field and a schema✅ field and schema validate24✅ .refine / .superRefine / .check✅ check (field), forward+partialCheck (object)✅ .narrow() on fields and objects✅ .check(makeFilter), refine✅ refine(struct, name, fn)✅ refinement(), brand()✅ withConstraint, withGuard, withAssertion✅ withPredicate(fn, msg)✅ assert(fn, msg), chain()✅ test() on fields and objects✅ custom() on any schema✅ createRule() + use(), any level⚠️ custom decorators per property✅ custom keywords (addKeyword)✅ Type.Refine on any type⚠️ TagBase validate expressions (strings)⚠️ only via a LogicalType❌ none✅ validate; pre('validate') + invalidate
Async validators✅ checkAsync, run concurrently❌ none✅ parseAsync; concurrent✅ *Async schemas; concurrent25❌ not supported (by design)26✅ Effect getters; concurrency option❌ none⚠️ experimental TaskDecoder, concurrent27❌ none❌ none28❌ none (non-goal)29✅ async test(), concurrent⚠️ external(): sequential, after sync rules30⚠️ async rules, in sequence✅ async constraints, concurrent31⚠️ $async schemas; awaited one by one❌ sync only32❌ none❌ none❌ none✅ async validators, concurrent
Validation results
Every issue in one run✅ all, by default✅ all by default✅ all by default✅ all by default; abortEarly✅ all by default⚠️ first by default; errors: 'all'⚠️ first; all via failures()✅ all by default✅ all by default⚠️ first; collectErrors() per object✅ all by default⚠️ first by default; abortEarly: false⚠️ first by default; abortEarly: false✅ all fields; first per field✅ all by default; stopAtFirstError⚠️ first by default; allErrors: true33⚠️ Errors(): up to 8 by default✅ validate: all; assert: first⚠️ first; all via errorHook❌ first error only✅ all paths, one per path
A cap on the number of issues✅ maxErrors❌ none❌ first (validate) or all❌ first or all❌ all only❌ first or all❌ first or all only❌ all only❌ all only❌ first or all only❌ all only❌ first or all❌ first or all❌ none❌ none❌ first or all⚠️ maxErrors (8), process-wide❌ first (assert) or all (validate)❌ first or all only❌ first only❌ none
Issues as data: code, path, params✅ code, path array, params❌ message strings34✅ code, path[], expected/minimum…✅ type, path[], expected/received/requirement✅ code, path[], expected/actual/rule✅ tagged issue tree; paths; check params⚠️ path array, type/refinement; no params⚠️ context path, expected codec, value; no codes⚠️ code, expected, received; nested details⚠️ path array + message; no codes✅ code, path array, expected/keys/bounds⚠️ type, params; path is a string✅ type, path array, context⚠️ rule, meta; path is a dot string⚠️ constraint names; nested error tree⚠️ keyword, params; path as JSON Pointer⚠️ keyword, params; path as JSON Pointer⚠️ path string, expected, value; no code⚠️ errorHook(path[], value, type)❌ string with dotted path⚠️ kind, path string, properties
Localized messages✅ en, uk; a locale per call❌ none✅ 62 locales; per-call error fn✅ @valibot/i18n (34 locales); per-call message fn35⚠️ per-kind message config; no locales⚠️ message annotations, formatter hooks; no locales❌ none (per-call message string)❌ none❌ none❌ none (per-schema messages)❌ none⚠️ global setLocale(); English only✅ messages + errors.language per call✅ messagesProvider per call; English only⚠️ message functions; none built in✅ ajv-i18n: 23 locales✅ 38 locales; global Locale.Set❌ no messages❌ none❌ none⚠️ global mongoose.Error.messages
Unknown keys✅ reject (default), ignore, strip⚠️ reject only (no option)36✅ strip (default), strict, loose✅ strip (default), strict, loose, rest✅ ignore (default), reject, delete✅ strip (default), error; keep via StructWithRest✅ object() rejects; type() ignores; mask() strips⚠️ ignore (t.type), strip (t.exact/t.strict); no reject✅ check ignores, parse strips, exact() rejects37✅ reject (default), strip✅ strict (default), strip, passthrough per call✅ keep default; exact() rejects, stripUnknown38✅ reject default; allowUnknown, stripUnknown⚠️ strip default; allowUnknownProperties()39✅ keep default; whitelist strips, forbidNonWhitelisted40✅ ignore default; reject, strip (removeAdditional)✅ ignore default; reject, Value.Clean strips✅ ignore default; equals rejects, prune strips41✅ ignore (default), reject, strip✅ ignore, strip, reject42✅ strip (default), keep, throw
A result instead of an exception✅ check, parse return a result⚠️ check() returns a result43✅ safeParse✅ safeParse, is✅ T(data) returns data | ArkErrors✅ decodeUnknownResult / Exit / Option✅ validate() → [error, value]; is()✅ decode() → Either✅ inspect() → result; guard()✅ try() → value | ValidationError✅ try() → { ok, value | issues }⚠️ isValid(), ~standard.validate✅ validate() → { value, error }✅ tryValidate() → [error, value]✅ validate() → ValidationError[]✅ validate() → boolean + .errors✅ Check + Errors; Parse throws✅ is / validate✅ isValid() -> boolean✅ verify() -> null | string✅ validateSync() returns the error44
Cyclic and very deep input✅ tracked at references, maxDepth❌ no detection; stack overflow⚠️ cycle-safe; no depth limit45❌ stack overflow, no depth limit⚠️ cycle-safe; no depth limit❌ stack overflow, no depth limit46❌ stack overflow❌ stack overflow⚠️ cycles accepted; deep input overflows⚠️ the overflow caught as an issue47❌ stack overflow❌ stack overflow✅ link.depth issue; maxRecursion(n)⚠️ finite schemas; the unknown-key copy overflows❌ stack overflow❌ RangeError; no detection or depth limit❌ RangeError; no detection or depth limit⚠️ WeakSet guard; deep input overflows48❌ stack overflow✅ depth limit 100❌ throws (circular JSON, RangeError)
Parsing
Defaults✅ default, a value or a factory❌ kept as metadata only✅ .default(), .prefault(), .catch()✅ optional(schema, default), fallback✅ 'string = "x"', .default()✅ withDecodingDefault(Key), withConstructorDefault✅ defaulted(), via create()/mask()49⚠️ io-ts-types fromNullable/withFallback✅ default(value), parse only50✅ default(value | fn)✅ optional(() => v), nullable(() => v)✅ default(), factories✅ default(), factories❌ none (parse() can fill)✅ field initializers (class-transformer)✅ useDefaults: true (in place)51⚠️ Value.Default; Decode pipeline52❌ Default<V> is metadata only✅ field default (clone, decode)✅ proto defaults, toObject({ defaults })✅ default values and functions
Transforms and decoders✅ a field's parse (decoder)❌ none✅ .transform, z.pipe, z.preprocess✅ transform, rawTransform in pipe✅ morphs: .pipe(), .to(), '=>'✅ decodeTo + SchemaTransformation✅ coerce(struct, from, fn) via create()✅ custom Type with validate/encode✅ withParser(fn), parse only✅ map(fn)✅ map(fn), chain(fn)✅ transform()✅ custom() return value, rename(), trim()...✅ transform(), parse()✅ @Transform (class-transformer)⚠️ ajv-keywords transform (strings only)✅ Type.Codec / Decode / Encode⚠️ built-in only: http, notations, llm✅ logical types (_fromValue)⚠️ conversion options only✅ set/get, trim, lowercase
Coercion of input types✅ coerce: true (call, kind or field) + ./coerce53❌ none✅ z.coerce.*, z.stringbool✅ toNumber, toDate, toBoolean… actions✅ string.numeric.parse, string.date.parse…✅ NumberFromString, DateFromString…❌ none (trimmed() only)⚠️ io-ts-types NumberFromString etc.❌ none✅ number coerce, date, bigint, enum case54❌ none✅ on by default; strict() off✅ convert: true by default55✅ form semantics by default; { strict: true }✅ @Type / implicit (class-transformer)✅ coerceTypes: true | "array"✅ Value.Convert (also inside Decode)⚠️ http.query/headers, llm.coerce⚠️ coerceBuffers only✅ fromObject casts56✅ casting on by default
Encode back (bidirectional codecs)✅ serialize❌ none✅ z.codec + z.encode🔜 planned for v2 (#658)❌ none✅ encodeSync / encodeUnknown*❌ none✅ codec.encode()❌ none❌ none❌ none❌ none❌ none❌ none✅ instanceToPlain (class-transformer)⚠️ JTD compileSerializer (to JSON)✅ Codec + Value.Encode⚠️ protobuf.encode/decode, json.stringify✅ toBuffer/fromBuffer; logical _toValue✅ binary, toObject, ProtoJSON⚠️ getters, toJSON transform
TypeScript
Static type inferred from the schema✅ Infer, InferSchema❌ none✅ z.infer✅ InferOutput✅ typeof T.infer✅ typeof S.Type✅ Infer<typeof S>✅ t.TypeOf<typeof C>✅ Static<typeof R>✅ Infer<typeof S>✅ v.Infer<typeof T>✅ InferType❌ none✅ Infer<typeof schema>⚠️ the class is the type⚠️ JTDDataType (JTD only)✅ Static<T>, also from plain JSON Schema✅ the TS type is the schema❌ none⚠️ via pbjs --dts codegen✅ InferSchemaType
Separate input and output types✅ Infer / InferOutput❌ none✅ z.input / z.output✅ InferInput / InferOutput✅ T.inferIn / T.inferOut✅ S.Type / S.Encoded❌ none✅ TypeOf, InputOf, OutputOf✅ Static vs Parsed❌ none❌ output only⚠️ defaults only, in ~standard types❌ none✅ InferInput / Infer❌ none— no transforms✅ StaticDecode / StaticEncode— no transforms or defaults❌ none⚠️ $Properties vs class (pbts)⚠️ InferRawDocType vs hydrated
Generates TypeScript source✅ toTypeScript, saveTypes⚠️ model.dts, flat scalars only57⚠️ zod-to-ts (third-party)❌ none❌ .expression is ArkType syntax, not TS✅ SchemaRepresentation.toCodeDocument❌ none❌ none❌ none❌ none❌ none❌ none⚠️ joi-to-typescript (3rd party)❌ none— schemas are TS classes⚠️ json-schema-to-typescript (third-party)⚠️ json-schema-to-typescript (third-party)— the source is TypeScript⚠️ third-party: @ovotech/avro-ts✅ pbjs --dts / pbts (protobufjs-cli)⚠️ third-party: mongoose-tsgen
Interoperability
Standard Schema✅ v1 + JSON Schema converter❌ no✅ v1 + ~standard.jsonSchema✅ v1 on every schema✅ v1 + ~standard.jsonSchema✅ v1 via toStandardSchemaV1()58❌ no❌ no❌ no❌ no✅ v1 (~standard), libraryOptions.mode✅ v1, always async59✅ v1, + Standard JSON Schema⚠️ v1 on validators; path is a string60❌ none❌ none❌ none✅ createValidate<T>() (v1)❌ no❌ no✅ Model['~standard'] v1 (9.7+)61
JSON Schema export✅ 2020-12, draft-07, OpenAPI 3.0, MongoDB❌ none✅ 2020-12, draft-07, draft-04, OpenAPI 3.0✅ draft-07, 2020-12, OpenAPI 3.0 (@valibot/to-json-schema)✅ draft 2020-12, draft-07✅ 2020-12, draft-07, draft-04, OpenAPI 3.1❌ none⚠️ io-ts-to-json-schema (third-party)❌ none❌ none❌ none⚠️ @sodaru/yup-to-json-schema (3rd party)✅ 2020-12 via ~standard.jsonSchema✅ draft-07 via toJSONSchema()⚠️ class-validator-jsonschema (3rd party)✅ the schema is JSON Schema✅ types are JSON Schema (2020-12)✅ json.schema(s): OpenAPI 3.0, 3.1❌ none❌ none✅ toJSONSchema(): JSON Schema, $jsonSchema
JSON Schema import✅ fromJSONSchema (a subset)❌ none✅ z.fromJSONSchema62⚠️ json-schema-to-valibot (third-party, codegen)⚠️ @ark/json-schema 0.0.12 (no $ref)✅ SchemaRepresentation.fromJsonSchemaDocument❌ none❌ none❌ none❌ none❌ none⚠️ json-schema-to-yup (3rd party)⚠️ enjoi (3rd party)❌ none❌ none✅ draft-07, 2019-09, 2020-12, JTD✅ Compile/Value accept drafts 3–2020-12⚠️ @typia/utils OpenApiValidator (untyped)❌ none❌ none❌ none
LLM structured-output profile✅ the strict profile❌ none⚠️ openai/helpers/zod (third-party)⚠️ @ai-sdk/valibot (third-party)❌ none✅ effect/ai toCodecOpenAI, toCodecAnthropic63❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none✅ llm.application/parameters; strict mode❌ none❌ none❌ none
Domain models
Entities with storage metadata✅ kinds: Entity, Registry, …✅ 11 kinds: scope, store, id field❌ none❌ none❌ none⚠️ schema/Model variants + SqlModel❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none✅ model(name), collection option
References: an id or the record✅ an id or the record, by kind or per call⚠️ embedded record only64❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none (named types embed)❌ none (messages embed)⚠️ ref: id or populated doc65
Indexes and unique keys✅ index, primary, unique; driver specs (./mongodb)✅ index/primary/unique arrays❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none✅ index/unique/sparse, schema.index()
Relation metadata✅ schema.relations⚠️ schema.relations { to, type }66❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none⚠️ ref, virtual populate (justOne)
Derive a schema: pick, omit, partial, extend✅ pick, omit, partial (deep), required, extend, keyof (./derive)⚠️ Projection kind (pick fields)✅ pick, omit, partial, required, extend, deepPartial✅ pick, omit, partial, required✅ pick, omit, partial, required, merge✅ mapFields + Struct.pick/omit/map✅ pick, omit, partial, assign⚠️ partial, exact, intersection; no pick/omit✅ pick, omit, extend, asPartial✅ pick, omit, partial (deep), required✅ pick, omit, partial, extend, rest✅ pick, omit, partial, shape, concat✅ keys, append, fork, tailor, concat✅ pick, omit, partial, merge(group)⚠️ class extends; @nestjs/mapped-types⚠️ allOf; ajv-merge-patch $merge/$patch✅ Pick, Omit, Partial, Required, Mapped✅ TS Pick, Omit, Partial❌ none❌ none✅ pick, omit, add, clone, discriminator
A model of many schemas✅ Model: order, missing references✅ Model: order, missing refs❌ none❌ none⚠️ scope(): resolves names, errors on missing❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none⚠️ Ajv instance as $ref registry⚠️ Type.Module (named types)❌ none⚠️ protocols, shared registry⚠️ Root: lookup, resolveAll()⚠️ mongoose.model() registry
Evolution
Snapshot of a schema or model✅ model.toJSON(), schema.toSnapshot(), fromJSON⚠️ schema.toJSON(), fields only⚠️ via JSON Schema round trip❌ export only, no restore✅ .json → type.schema()✅ SchemaRepresentation toJson / fromJson❌ none❌ none❌ none❌ none❌ none⚠️ describe(), no restore✅ describe() / Joi.build()⚠️ toJSON(), no restore❌ none✅ schemas are JSON✅ JSON.stringify; Compile reads it back⚠️ export only (json.schema, reflect.schema)✅ schema(), JSON.stringify(type)✅ root.toJSON() / Root.fromJSON()❌ none
Diff of two versions✅ diff🔜 issue #251 (migrations)❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none⚠️ Value.Diff (generic JSON edits)❌ none❌ none❌ none❌ none
Compatibility of a change✅ backward / forward / full❌ none❌ none❌ none⚠️ via .extends() / .equals()❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none⚠️ Type.Extends (assignability)❌ none✅ reader/writer resolution67❌ none❌ none
Field aliases (renames)✅ aliases, renamed by parse❌ none❌ none❌ none❌ none⚠️ Schema.encodeKeys (one encoded name)❌ none❌ none❌ none❌ none❌ none✅ object.from(old, new, alias)✅ rename(old, new, { alias })❌ none⚠️ @Expose({ name }) (class-transformer)❌ none❌ none❌ none✅ type and field aliases⚠️ renames free on the wire✅ alias option (virtual)
Deprecation markers✅ deprecated❌ none✅ .meta({ deprecated: true })❌ free-form metadata() only✅ .configure({ deprecated: true })❌ none✅ deprecated(struct, log)❌ none❌ none❌ none❌ none❌ none (meta() is free-form)❌ none (meta(), notes free-form)❌ none❌ none✅ deprecated keyword (annotation)⚠️ deprecated: true passes through✅ JSDoc @deprecated → schema❌ none⚠️ [deprecated] kept in options❌ none
Tooling
Sample values from a schema✅ sample, seeded❌ none⚠️ zod-schema-faker (third-party)⚠️ valimock (third-party)✅ @ark/fast-check (arkToArbitrary)✅ Arbitrary.schema (own engine)❌ none⚠️ fast-check-io-ts (third-party)❌ none❌ none❌ none❌ none❌ none (example() is an annotation)❌ none❌ none⚠️ json-schema-faker (third-party)⚠️ Value.Create (zero values)✅ typia.random<T>() (randexp)✅ type.random()❌ none❌ none
A schema inferred from data✅ infer❌ none⚠️ json-to-zod (third-party, 2022)❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none✅ Type.forValue, Type.forTypes68❌ none❌ none
Merge or intersect two schemas✅ merge❌ none✅ z.intersection, .extend✅ intersect✅ .merge(), .and()⚠️ fieldsAssign (struct fields only)✅ assign(), intersection()✅ intersection()✅ Intersect(...), and()✅ and() / intersection()⚠️ extend(other.shape); no intersection✅ concat()✅ concat()❌ none❌ none⚠️ allOf; ajv-merge-patch $merge✅ Intersect + Evaluate✅ TS intersection (A & B)⚠️ Type.forTypes (widening)❌ none✅ schema.add(other), new Schema([a, b])
Warnings about a definition✅ schema.warnings⚠️ model.warnings: refs, recursion❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none✅ strict mode; strictTypes logs❌ none❌ compile errors only❌ none❌ none⚠️ reserved-key, duplicate-index warnings
Streams and iterables of records✅ checkEach, transform❌ none❌ none❌ none❌ none✅ ChannelSchema.decode/encode69❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none❌ none⚠️ Node stream codecs❌ none (delimited framing only)❌ none
Compiled to code, with a fallback✅ compile(), closures fallback❌ interpreter only✅ z.compile / zod/compile; runtime fallback🔜 compiler PR by the author (#1441)✅ JIT by default; jitless fallback✅ opt-in JIT + AOT; interpreter fallback70❌ interpreter only❌ interpreter only❌ interpreter only❌ interpreter only❌ interpreter only❌ interpreter only❌ interpreter only✅ always; no interpreter fallback❌ interpreter only✅ always compiles; no interpreter✅ Compile; interpreter fallback✅ AOT at build time; no fallback✅ new Function; no fallback✅ runtime codegen; static AOT❌ interpreter only
Readable at runtime✅ fields, toJSON()✅ fields, kind, indexes, relations✅ .def, .shape, .meta()✅ plain schema objects (type, entries, pipe)✅ .json, .props, .expression, .select()✅ .ast, .fields, annotations⚠️ struct.type, struct.schema✅ _tag, props, types, name✅ tag, fields, alternatives, element⚠️ shape(); other parts private⚠️ name, shape, options; transforms opaque✅ describe(), fields, spec✅ describe(), $_terms, _rules✅ toJSON() tree, getProperties()✅ getMetadataStorage()✅ the schema is a plain object✅ JSON Schema objects; Type.Is* guards✅ reflect.schema / json.schema✅ fields, typeName, schema()✅ reflection: fields, options✅ schema.paths, path(), eachPath

✅ built in, or in an official package of the same project (named) · ⚠️ partial, opt-in, with a caveat, or through a third-party package (named) · ❌ not built in · 🔜 planned by the maintainers, no code yet · — does not apply. Every cell was checked against the source of the published package in October 2026.

  1. No release since 2024-07; MetaMask publishes a maintained fork, @metamask/superstruct.
  2. No commits since the 2025-02 release.
  3. The legacy @sinclair/typebox 0.34.52 differs materially: CJS + ESM, Type.Date / Uint8Array and a TypeRegistry for custom kinds, and a Value.Parse that cleans, defaults and converts by default (research/typebox-legacy/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.js:60-66).
  4. A minified bundle validates nothing but unknown keys: identity is checked by constructor.name === 'Type' (lib/util.js:42, lib/struct.js:34), which minifiers rename. esbuild minify without keepNames: { a: 1 } against { a: 'string' } is valid.
  5. The browser bundle has no describe()/build() ('Manifest functionality disabled'), binary() or trace(); the main entry needs Node's url and util modules.
  6. The compiled validator needs code generation, so it does not run on Cloudflare Workers or under a CSP without unsafe-eval.
  7. Runtimes that forbid eval (Cloudflare Workers, strict CSP) can only run validation code pre-generated with ajv/dist/standalone at build time.
  8. Built for browserify-era Node polyfills; and since every type is created with new Function, eval-free edge runtimes (Cloudflare Workers) cannot load it.
  9. @mongoosejs/browser 0.1.3 is a prebuilt UMD (1.1 MB, 270 KB gzip) of mongoose 9.0.0-rc1 that validates documents only; its Node entry exports the driver, not mongoose, despite its README.
  10. z.object builds a fast path with new Function when eval is allowed; a CSP probe (or z.config({ jitless: true })) switches to the interpreter. z.compile / "zod/compile" also use new Function and fall back to the runtime parser.
  11. Standard (TC39) decorators are not supported; class-transformer's implicit conversion also needs emitDecoratorMetadata.
  12. Since v13 the transform is written in Go and run by ttsc on the TypeScript 7 native compiler (the first build compiled the plugin in 85 s); typia 12 is the legacy ts-patch setup for TypeScript < 7. Bundlers need @ttsc/unplugin.
  13. After registerSchema(), validating any decorated class throws the same TypeError (MetadataStorage.js:107).
  14. optional() lets null through and drops it from the output; nullable() keeps the key required; an absent key and undefined are the same.
  15. A longer array is accepted and truncated, not rejected.
  16. The invalid_length message is garbled in 0.5.5 ("expected an arraywith items", :852-866): a missing space and no count.
  17. Regex-only: uuid() accepts versions 1-5 only (a v7 UUID fails) and datetime() does not check ranges.
  18. The core has none: in strict mode a schema with an unknown format fails to compile ("unknown format "email" ignored in schema").
  19. An unregistered format name passes silently: Format.Test returns true when the name is unknown (_registry.ts:103-106; typebox-behaviour.mjs).
  20. Type.Date and Type.Uint8Array were removed in 1.0; the migration guide rebuilds them with Type.Unsafe + Type.Refine.
  21. An invalid Date (new Date("nope")) passes as a Date, and NaN passes as a number unless the numeric/finite transform flags are set.
  22. A JS Map passes a map type vacuously (it has no own keys): new Map([['a', 'x']]) is valid for values int and encodes as an empty map (lib/types.js:1799-1830).
  23. verify() rejects a BigInt for int64 ("integer|Long expected") although fromObject and toObject accept it.
  24. A field validate runs even when the type check already failed (both messages reported).
  25. Async needs the parallel *Async API (objectAsync, pipeAsync, checkAsync). In plain JS, the sync safeParse of an async schema reports success: true for invalid input (the Promise has no issues); TypeScript forbids the call.
  26. A narrow that returns a Promise passes, because the predicate result is only tested for truthiness.
  27. TaskDecoder belongs to the experimental modules (lib/TaskDecoder.d.ts:2, "independent and backward-incompatible with stable ones").
  28. An async predicate silently passes every value (its promise is truthy).
  29. An async predicate silently passes every value.
  30. custom() given an async function passes and returns a Promise as the value (research-scripts/joi-extra.mjs).
  31. validateSync() skips async validators silently and reports the value valid (Validator.js:31; script).
  32. A refinement that returns a promise is not awaited: Value.Check returns true, and a compiled Check returns the Promise object itself, so the value is accepted.
  33. The Ajv security guide says not to use allErrors: true in production (research/ajv-src/docs/security.md:41).
  34. An unknown nested key loses its path: { nested: { extra } } reports Field "extra" (research-scripts/metarhia-basics.mjs).
  35. In @valibot/i18n 1.3.0 the CommonJS root entry does not load (index.cjs declares var import_zh-CN, a SyntaxError); ESM and per-locale imports work.
  36. Keys named like Struct.prototype members (constructor, check, proto) pass silently: this[name] finds the inherited member (lib/struct.js:29-34).
  37. The same schema keeps unknown keys under check()/guard() and drops them under parse().
  38. noUnknown() strips unknown keys instead of rejecting them unless the schema is strict() (the README warns about it); exact() rejects.
  39. No way to reject unknown keys.
  40. whitelist deletes the keys from the validated object itself; forbidUnknownValues (default true) rejects an object with no metadata, so a plain object fails.
  41. equals accepts an extra key whose value is undefined.
  42. Each policy belongs to a different API: verify ignores, fromObject strips, ProtoJSON fromJson rejects by default.
  43. check(null) throws.
  44. validateSync is deprecated in 9.x and emits a process warning on every call (research-scripts/mongoose-custom.mjs); validate() rejects; the constructor throws for strict: 'throw' and cyclic input.
  45. The classic API installs a memoizer: a self-referencing value parses (output keeps the cycle), unless the cycle closes through a transform ($ZodCyclicError). zod/mini has no memoizer unless configured. 10 000-deep input overflows the stack.
  46. The RangeError surfaces as a defect: the Result/Exit adapters throw "Result adapter can only return schema issues".
  47. The RangeError of a cyclic or 3 000-deep value is returned as an issue whose path is thousands of keys long.
  48. A cycle is accepted (coinductive), and there is no depth limit.
  49. Defaults apply only on the coercing calls (create, mask, validate with coerce: true), not in is()/assert().
  50. The default is a value, not a factory: default([]) hands every parse the same array (script).
  51. Defaults are written into the input object, and not inside anyOf/oneOf/not.
  52. Value.Parse and Compile().Parse never fill defaults (only with the global correctiveParse setting, and only for a value that fails the check); Value.Decode does, and Compile().Decode does only when the type contains a codec.
  53. Opt-in, in parse only (check never converts): strict decimal strings, true/false/1/0, ISO dates with a zone, never the empty string; a value a union branch accepts as it is is not converted; coerce() is the step alone.
  54. Lenient: number().coerce() turns "1abc" into 1, number() accepts NaN, bigint() accepts true (script).
  55. number() also rejects values beyond Number.MAX_SAFE_INTEGER unless unsafe() (types/number.js:104).
  56. fromObject does not validate: { age: 'abc' } becomes 0 and { name: 42 } "42".
  57. Only number/string/boolean are typed: a nested struct, enum, array or custom type is rendered as string (profile: string), and a reference as <name>Id: string although check validates the embedded record.
  58. Schemas do not carry ~standard themselves ('~standard' in schema is false); wrap with Schema.toStandardSchemaV1 / toStandardJSONSchemaV1.
  59. ~standard.validate always returns a promise and runs with abortEarly: false; array indexes come back as strings.
  60. ~standard.validate puts the dotted field string in path (the specification wants an array) and, for a valid falsy output (false, 0, ''), returns { issues: undefined } without the value. Schemas themselves have no ~standard; vine.create() validators do.
  61. On models only (a Schema has none), and validate always returns a promise; it casts, so { n: '5' } is valid with value n: 5.
  62. Its JSDoc calls it semi-experimental.
  63. Marked @stability unstable.
  64. An id is never accepted, while the generated dts names the field companyId: string.
  65. A populated document is not validated with its own schema (an invalid Dog passes).
  66. The labels are reversed: a many field is labelled many-to-one and a single reference one-to-many.
  67. A resolver either builds or throws for one reader/writer pair; there is no backward/forward/full classification.
  68. Any non-integer number below 2^53 is inferred as float (32-bit), which loses precision when encoded (lib/types.js:247-252).
  69. Works on Effect Channels/Streams (unstable), not on Web Streams directly.
  70. Both are @stability unstable; the interpreter is the default.

Installation

pnpm add @alexify/metaschema
# or
npm install @alexify/metaschema

Requires Node.js 18 or newer. Works in browsers through any bundler. The typings need TypeScript 5.0 or newer.

Schema syntax

Form Example Meaning
Type name title: 'string' a required string
Optional subtitle: '?string' or 'subtitle?': 'string' the key may be absent, undefined or null
Nullable parent: { type: 'string', nullable: true } the key is required, the value may be null
Long form login: { type: 'string', length: [3, 32] } a type with options, type first
Rules { type: 'integer', min: 18, max: 120 }, { type: 'string', pattern: '^[a-z]+$', length: { max: 32 } }, { type: 'number', decimals: 2 } min/max for numbers, decimals for a number, length/pattern/format for strings, length for collections
Collections { array: 'number' }, { set: 'string' }, { object: { string: 'number' } }, { map: { string: 'string' } }
Enum { enum: ['admin', 'user'] } one of the values
Flags perms: { flags: ['read', 'write'] } an object of those names, each true or false
Bytes avatar: 'bytes' a Uint8Array (a Buffer too)
Tuple point: ['number', 'number'] a fixed-length array, each element its own definition
Union id: { union: ['string', 'number'] }, { union: [...], discriminator: 'kind' } one of several definitions
Nested struct name: { first: 'string', last: 'string' } an object whose first key is none of the above
Reference company: 'Company', { many: 'Address' } another schema in the model (or the schema itself, by its own name): its id for a stored kind, the record for a memory kind
Any object payload: 'json' any non-null object
Calculated ratio: (file) => file.compressed / file.size a function, never validated

The first key of an object decides what it is. Built-in types: string, number, integer, bigint, boolean, date, bytes, null, any/unknown, enum, flags, array, set, object, map, tuple, union, json. Rules: length (with unicode: true for code points), pattern and format for strings, length for collections, min/max for numbers, decimals for a number; a rule on a type that does not take it is a definition error. Fields can add a validate(value, path) function, and a schema can have a top-level validate for rules across fields. check(value, options) takes root (the label of the error lines), maxErrors, unknown: 'ignore' (keys the schema does not have; { Struct: { unknown: 'ignore' } } makes it the schema's default), references ('kind', 'embed' or 'id') and messages (a locale); the result has issues, errors, summary, flatten(), tree() and toJSON(). schema.warnings lints the definition (a mistyped option, a pattern without length.max). See Schema Syntax, Types, Unions, nullable and null, References and Validation.

Domain models

const { Model } = require('@alexify/metaschema');

const types = {
  string: { metadata: { pg: 'varchar' } },
  number: { metadata: { pg: 'integer' } },
  boolean: { metadata: { pg: 'boolean' } },
};

const entities = new Map([
  ['Company', { Dictionary: {}, name: { type: 'string', unique: true }, addresses: { many: 'Address' } }],
  ['Address', { Entity: {}, city: 'string', street: 'string', building: '?string' }],
  ['User', { Registry: {}, login: { type: 'string', length: { min: 3, max: 32 } }, company: 'Company', active: 'boolean' }],
]);

const model = new Model(types, entities);

model.order; // Set { 'Address', 'Company', 'User' }
model.warnings; // [] (lint and consistency warnings, `Warning [code]: text`, end up here)
model.entities.get('User').check({ login: 'ab', company: 'c1', active: true }).errors;
// [ 'Field "User.login" value is too short' ]

const { toTypeScript } = require('@alexify/metaschema/generators');
console.log(toTypeScript(model));
interface Address {
  city: string;
  street: string;
  building?: string;
  addressId?: string;
}

interface Company {
  name: string;
  addressesId: string[];
  companyId?: string;
}

interface User {
  login: string;
  companyId: string;
  active: boolean;
  userId?: string;
}

saveTypes(outputFile, model, options?) of the same subpath writes the interfaces to a file ({ named: true } names enums and nested structs, and a field's description is its JSDoc); its register() adds model.dts, model.toTypeScript and schema.toInterface as methods. The first key of a definition sets its kind and metadata (Entity, Registry, Dictionary, Journal, Details, Relation, View, Struct, Form, Projection, or any custom kind). Stored kinds default to scope: 'application' and store: 'persistent', and they get an id field; a reference to a stored kind holds its id, a reference to a memory kind embeds the record, in check and in the generated types alike. See Kinds and Metadata, Custom Types and Domain Models.

String formats

format names a form a string must have; @alexify/metaschema/formats registers the 35 built-in formats and your own, once per process:

const { register } = require('@alexify/metaschema/formats');
register({ phone: { test: (value) => /^\+\d{9,15}$/.test(value), examples: ['+380501234567'] } });

const signup = Schema.from({
  email: { type: 'string', format: 'email', length: { max: 254 } },
  phone: { type: 'string', format: 'phone' },
});
signup.check({ email: 'ann@', phone: '050' }).errors;
// [ 'Field "email" is not a valid email', 'Field "phone" is not a valid phone' ]

A format the table does not know is a definition error, never a check that passes everything; toJSONSchema writes the name, fromJSONSchema reads it back, sample draws the format's examples and infer reads a format out of the strings. See String formats.

Records and streams

@alexify/metaschema/stream runs a schema over a sequence of records, lazily and in order:

const { checkEach, parseEach, transform } = require('@alexify/metaschema/stream');

for (const { index, record, result } of checkEach(user, rows)) {
  if (!result.valid) console.log(index, result.errors);
}
for (const { result } of parseEach(user, rows, { unknown: 'strip', maxInvalid: 10 })) save(result.value);

const parsed = ReadableStream.from(rows).pipeThrough(
  transform(user, { onInvalid: (record, result, index) => log(index, result.issues) }),
);

checkEachAsync and parseEachAsync take a sync or async source and keep concurrency records in flight; maxInvalid ends an iteration after that many invalid records, and a transform without onInvalid ends its stream with ERR_INVALID_RECORD. See Records and streams.

Coercion

Text from a query string, a form or a file converts to the types of the fields with coerce: true, before the check; check never converts:

require('@alexify/metaschema/coerce').register();

const Query = new Schema('Query', {
  Form: { coerce: true },
  page: { type: 'integer', min: 1 },
  all: 'boolean',
  tags: { array: 'string' },
});
Query.parse(Object.fromEntries(new URLSearchParams('page=2&all=1&tags=news'))).value;
// { page: 2, all: true, tags: [ 'news' ] }

The converters are strict (a number written as one, true/false/1/0, an ISO date with its zone, never the empty string), a union takes a value a branch accepts as it is, and coerce(schema, value) converts without checking. See Coercion.

Deriving schemas

const { omit, partial, pick } = require('@alexify/metaschema/derive');

const CreateAccount = omit(Account, ['password'], { name: 'CreateAccount' });
const UpdateAccount = partial(CreateAccount, { name: 'UpdateAccount' });
const AccountView = pick(Account, ['accountId', 'login', 'role'], { name: 'AccountView' });

A derived schema is built from the definition of its source, every rule of a field included; a stored kind becomes a struct without its id, partial leaves the defaults out (a patch keeps what is stored), extend replaces fields where merge unifies them, and register() makes the six methods (Account.omit(['password']).partial()). See Deriving schemas.

MongoDB

const mongodb = require('mongodb');
const bson = require('@alexify/metaschema/mongodb');
bson.register({ bson: mongodb });

const model = new Model({ ...bson.types }, entities);
const validators = toJSONSchema(model, { target: 'mongodb', ids: 'objectId' });
const indexes = bson.indexesOf(model); // for createIndexes

const parsed = Account.parse(body);
if (parsed.valid) await accounts.insertOne(bson.toDocument(Account, parsed.value));
const { value } = bson.fromDocument(Account, await accounts.findOne({ login: 'ann' }));
if (bson.checkUpdate(Account, update).valid) await accounts.updateOne(filter, bson.toUpdate(Account, update));

The program keeps ids as strings, the database as ObjectIds under _id; the subpath does no I/O and imports nothing of the driver. Mongoose remains the tool for populate, middleware and change tracking. See MongoDB.

Inferring a definition

infer of @alexify/metaschema/model reads a definition out of the values that exist, the reverse of sample, to bootstrap a model from the documents of a collection or the rows of a table:

const { infer } = require('@alexify/metaschema/model');

infer([
  { id: 'ord-1', total: 12.5, status: 'paid', address: { city: 'Kyiv', zip: '01001' } },
  { id: 'ord-2', total: 7, status: 'new', address: { city: 'Lviv', zip: null }, coupon: 'X1' },
]);
// {
//   Struct: {},
//   id: 'string',
//   total: 'number',
//   status: 'string',
//   address: { city: 'string', zip: { type: 'string', nullable: true } },
//   coupon: '?string',
// }

Records fuse into one struct with optional keys, a string of few recurring values is an enum, records whose keys are data are a dictionary, and the definition is checked against every sample before it comes back. See Schema inference.

TypeScript

A schema keeps its definition as a type parameter, and InferSchema turns it into the type of a value check accepts; Infer<typeof definition> does the same for a definition declared as const:

import { Schema, type InferSchema } from '@alexify/metaschema';

const user = Schema.from({
  name: 'string',
  age: '?number',
  role: { enum: ['admin', 'user'] },
  tags: { array: 'string' },
  address: { city: 'string', 'street?': 'string' },
  point: ['number', 'number'],
});

type User = InferSchema<typeof user>;
// {
//   name: string;
//   age?: number | null | undefined;
//   role: 'admin' | 'user';
//   tags: string[];
//   address: { city: string; street?: string | null | undefined };
//   point: [number, number];
// }

Every form of the language is covered: optional and nullable fields, collections, enums, tuples, unions, nested structs, references (an id, or the record through an entity map with Infer<D, E> and InferEntity<E, 'Name'>) and custom types declared through module augmentation of CustomTypes. A model renders its entities as interfaces with toTypeScript. See TypeScript.

A Schema<D> is also a StandardSchemaV1<Infer<D>, InferOutput<D>> of @standard-schema/spec: schema['~standard'] is typed as StandardProps<D>, with StandardResult<T> and StandardOptions beside it. See Standard Schema.

JSON Schema

toJSONSchema(schema, options) of @alexify/metaschema/generators renders the rules of check as a JSON Schema document, and toJSONSchema(model, options) every entity of a model as a definition:

const { toJSONSchema } = require('@alexify/metaschema/generators');

toJSONSchema(user, { target: 'draft-07' });
// {
//   $schema: 'http://json-schema.org/draft-07/schema#',
//   type: 'object',
//   properties: {
//     name: { type: 'object', properties: { first: { type: 'string' }, last: { type: 'string' } }, required: ['first', 'last'], additionalProperties: false },
//     email: { type: 'string', minLength: 5, maxLength: 64 },
//     age: { type: ['number', 'null'] },
//     roles: { type: 'array', items: { enum: ['admin', 'editor', 'viewer'] } }
//   },
//   required: ['name', 'email', 'roles'],
//   additionalProperties: false
// }

toJSONSchema(answer, { profile: 'strict' }); // the dialect of OpenAI and Anthropic structured outputs
toJSONSchema(model, { target: 'mongodb' }); // one $jsonSchema validator per entity

The targets are draft-2020-12 (the default), draft-07, openapi-3.0 and mongodb; an optional field accepts null, a reference to a stored kind is its id and a reference to a memory kind a $ref, validate functions are not exported, and a type with no form in the target (bigint, a Date on the way out) throws unless the call says unrepresentable: 'any'. The export is checked against ajv for every form of the language. register() of the subpath turns the functions into methods (schema.toJSONSchema, model.toJSONSchema) and adds the Standard JSON Schema converter to schema['~standard']. See JSON Schema.

Compiled checks

compile(schema) of @alexify/metaschema/compile replaces the check of a schema, or of every entity of a model, with one function generated from the same plan the closures walk and built with new Function; register() does it for every schema built from then on. The issues are the ones the closures record, only sooner: about three times faster on a flat struct, six on a nested one. Where code generation from strings is not allowed (a Content Security Policy, Cloudflare Workers, node --disallow-code-generation-from-strings) nothing changes: the closures stay, silently unless { jit: true } asks for an error.

const { compile, compiled, register, canEval } = require('@alexify/metaschema/compile');

compile(user); // { accelerated: true }: user.check runs generated code now
compiled(user); // true
compile(model); // every entity; { accelerated: false } where there is no eval
register(); // every schema built from here on is compiled as it is built
canEval(); // whether this environment builds functions from source

The generated source holds identifiers, numbers and JSON strings only; regular expressions, enum sets, validate functions and nested schemas reach it through a scope, never through the source text. See The JIT backend.

Exports

const {
  KIND, KIND_STORED, KIND_MEMORY, SCOPE, STORE, ALLOW,
  getKindMetadata, Schema, Model, SchemaDefinitionError, ValidationResult,
} = require('@alexify/metaschema');

const { toJSONSchema, toTypeScript, saveTypes, fromJSONSchema, register } = require('@alexify/metaschema/generators');
const { compile, compiled, canEval } = require('@alexify/metaschema/compile'); // and its own register()
const { fromJSON, diff, compatibility, compatibilityOf, sample, merge, infer } = require('@alexify/metaschema/model'); // and its own register()
const { checkEach, parseEach, checkEachAsync, parseEachAsync, transform } = require('@alexify/metaschema/stream'); // and its own register()
const { FORMATS, register: registerFormats } = require('@alexify/metaschema/formats'); // fills the table the format rule reads
const { pick, omit, partial, required, extend, keyof } = require('@alexify/metaschema/derive'); // and its own register()
const { COERCIONS, coerce, register: registerCoercion } = require('@alexify/metaschema/coerce'); // the converters parse runs
const { types, toDocument, fromDocument, fromDocumentAsync, indexesOf, checkUpdate, toUpdate } = require('@alexify/metaschema/mongodb'); // and register({ bson })
const { Schema: Definition } = require('@alexify/metaschema/definition'); // the core's Schema and Model, built and read without validation

const en = require('@alexify/metaschema/locales/en'); // the built-in messages
const uk = require('@alexify/metaschema/locales/uk'); // schema.check(value, { messages: uk })
import { Schema, Model } from '@alexify/metaschema';
import type { Infer, InferSchema, InferEntity, CustomTypes, ValidationIssue } from '@alexify/metaschema';

In the browser every export works the same, except saveTypes of the generators, which rejects because there is no file system, and compile, which keeps the closures where a Content Security Policy forbids new Function. The API reference lists every member and every exported type.

Migrating

From 2.x

3.0 has two breaking changes. The generators left the core entry: model.dts, schema.toInterface() and saveTypes are toTypeScript(target, options) and saveTypes(file, target, options) of @alexify/metaschema/generators, whose register() puts the methods back on the classes once per process. And schema['~standard'].validate is parse rather than check: a consumer of Standard Schema gets the defaults filled in and the parse functions applied, and a promise for a schema with an async function. The smaller changes: a struct rejects an array, the synchronous check and parse throw ERR_ASYNC_VALIDATOR for a promise, a graph deeper than 128 embedded records is a depth issue (maxDepth raises it), Infer makes a defaulted key optional, and a few definitions that could never work throw when the schema is built. Everything else is additive: parse, serialize, checkAsync and parseAsync, the JSON Schema export, the JIT backend of @alexify/metaschema/compile, JSDoc and named types. 3.1 and 3.2 add without breaking, with a few behaviours a 2.x program meets on the way: two fields of one key and a non-list aliases are definition errors, JSON.stringify(model) writes the snapshot, the issues of an async check come in walk order then field order, a pattern is not run past length.max, and the unexpected message lists ten keys. The step-by-step guide with before-and-after examples is Migrating from 2.x; the full list is under "Upgrading from 2.0" in the CHANGELOG.

From 1.x

2.0 changes the shape of a result (issue.path is an array of keys, issue.message has no location, unknown keys are one issue per struct), the signature of check (check(value, { root, maxErrors, unknown, references, messages })), and a few rules of the language: the first key of an object decides what it is, numbers are bounded by min/max instead of length, a reference to a stored kind is its id, a set renders as Set<T>, the relations labels follow the referencing side, and a type failure cancels the rules and validate of the field. Everything else is additive: integer, date, null, any, union, nullable, pattern, locales, the lint, Infer and Standard Schema. The step-by-step guide with before-and-after examples is Migrating from 1.x; the full list is under "Upgrading from 1.x" in the CHANGELOG.

From metaschema (metarhia)

@alexify/metaschema is a fork of metaschema 2.2 with the same schema language. What changed in 1.0:

  • No loader. createSchema, loadSchema, readDirectory and loadModel are removed along with the metavm sandbox. Use new Schema(name, require('./schemas/User.js')) and new Model(types, new Map([...]), database). Schema files written as ({ ... }) become modules (module.exports = { ... }).
  • No runtime dependencies: metautil, metavm and metaskills are gone.
  • Fixed messages, no old text kept: Field "..." instead of Filed "..." and not of expected type: object instead of is not a object.
  • detouch is renamed to detach, with no alias.
  • browser.js replaces dist.js, and an exports map closes deep imports.

The full list is in Migrating from metarhia, and the 2.0 and 3.0 changes above apply on top of it.

Changelog

See CHANGELOG.md.

License

MIT © Alexis Technologies

@alexify/metaschema is a fork of metaschema by the Metarhia contributors, originally part of the Metarhia technology stack. Their copyright is kept in LICENSE.

About

Fast, zero-dependency metadata schema and interface definition language (IDL) for Node.js and browsers — validation and parsing with a JIT backend, domain models, TypeScript and JSON Schema

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages