Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

SchemaIR

CI npm PyPI License

A portable, zero-dependency contract kernel for structured data.

SchemaIR lets you define schemas once and validate, relate, and interpret JSON data across TypeScript and Python under one normative protocol. Their normalized statuses, diagnostics and witnesses are compared on the shared conformance suite; this is not a claim of universal host-object or callback equivalence. It is designed for schema-driven contracts as first-class runtime artifacts.

Install note: npm 0.7.0-dev.4 is published and includes core-4 and the TypeScript schemair/author subpath, but enforces the earlier closed-head rule. Python 0.7.0.dev4 was not on PyPI as of 2026-10-15; published dev3 predates core-4. These are historical package facts, not current release claims. The prior unpublished dev5 pair and its core-5 acceptance predate current export, portable-1, and core-6 work. The internal, unpublished pair is now npm 0.7.0-dev.6 / Python 0.7.0.dev6, covering the core-6-only baseline. Exact verification scope and local artifact provenance are recorded in the implementation guide. No publication is authorized; use a checkout or locally prepared artifacts:

pnpm install --frozen-lockfile
pnpm --dir typescript build
python -m pip install -e ./python
# In a consumer project, install the built TypeScript package:
npm install /absolute/path/to/schemair/typescript

Supported wire identity: { "schemair": "core-6" }  ·  Apache-2.0



What SchemaIR is not

  • Not a JSON Schema validator. SchemaIR implements its own core-6 protocol, not JSON Schema draft-07 or any other draft. Its Draft 2020-12 exporter is a conservative translation boundary, not JSON Schema import.
  • Not a code generator or ORM. It produces validation results and witnesses, not classes, database queries, or serialization code.
  • Not a documentation tool. Schemas are runtime artifacts, not annotations on existing code.
  • Not a network service. No HTTP, no registry, no schema resolution over DNS. Document loading is a host-supplied synchronous or async callback.
  • No canonical serialization in this release. Schemas round-trip through JSON but there is no normalized byte representation or hash.

What it does

Capability Description
Environment building Load one or more schema documents into a validated, authenticated environment. Unknown fields, duplicate IDs, dangling references and unguarded cycles are all caught at creation time.
Payload validation Validate any JSON value against a named definition, inline schema, or document root. Results are accepted, rejected (with diagnostic paths), or unknown (for example, exhausted budgets or unresolved metadata interpretation).
Relation checking Prove that every value accepted by a source schema is also accepted by a target, or report uncertainty. On incompatibility, returns a complete JSON witness — a concrete counterexample.
Standard predicates Call-local built-ins: email, integer, UUID/UUID v4, scalar bounds, array/record counts, deep uniqueness, and exact binary64 divisibility. See core-6 standard vocabulary.
Extension providers Register call-local vocabulary callbacks. Two-phase dispatch (declaration check, then payload check), budget sharing, and full mutation isolation.
Metadata relation proofs Optional exact-registration checkRelation callbacks, followed by a host metadataRelationFallback for remaining local metadata implications; no wire fields are added.
Core-6 applicators Array contains-count bounds, record key schemas, and ordered bounded pattern/value schemas, conjoined with ordinary item/field/residual rules.
JSON Schema export Draft 2020-12 structure and portable metadata, strict rejection or best-effort per-occurrence losses; formats remain annotations and only divisor 1 is emitted for multipleOf.

Why SchemaIR

Most schema libraries are language-native — they validate against JSON Schema or a similar DSL, but the schema is interpreted differently in each runtime. SchemaIR takes a different approach: one normative spec, one wire format, two independent implementations with matching normalized results on the shared conformance vectors. The protocol is the contract; binding-specific result containers and host callback behavior remain distinct.

This matters when you need schemas to survive across:

  • Service boundaries (TypeScript backend, Python ML pipeline)
  • Storage and replay (schemas as immutable versioned artifacts)
  • Static analysis (relation checking before deployment)

See docs/PHILOSOPHY.md for a fuller treatment.


Usage

Create an environment and validate a payload

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';

const env = createSchemaEnvironment([{
  schemair: 'core-6',
  id: 'user',
  definitions: [{ name: 'Root', schema: {
    kind: 'record',
    fields: [
      { key: 'name', schema: { kind: 'primitive', name: 'string' } },
      { key: 'age',  schema: { kind: 'primitive', name: 'number' } },
    ],
  }}],
  root: 'Root',
}]);

if (env.status === 'ok') {
  const result = validateDocumentPayload(
    env.environment,
    { document: 'user' },
    { name: 'Alice', age: 30 },
  );
  // result.status === 'accepted'
}
from schemair import create_environment, validate_document_payload, Limits

env = create_environment([{
    "schemair": "core-6", "id": "user",
    "definitions": [{"name": "Root", "schema": {
        "kind": "record",
        "fields": [
            {"key": "name", "schema": {"kind": "primitive", "name": "string"}},
            {"key": "age",  "schema": {"kind": "primitive", "name": "number"}},
        ],
    }}],
    "root": "Root",
}], limits=Limits())

if env.status == "ok":
    result = validate_document_payload(
        env.value,
        {"document": "user"},
        {"name": "Alice", "age": 30},
    )
    # result.status == "accepted"

Author schemas in TypeScript

The optional schemair/author subpath constructs ordinary wire values; no emitter or field() helper is needed. document() defaults to core-6; use the current default or pass schemair: 'core-6' explicitly. Core-6 is required for applicator fields.

The published npm 0.7.0-dev.4 includes this subpath, but predates core-6. For the current contract, start at a local checkout with Node 22 and pnpm 10.6.5; build and install the package itself (not a separate schemair/author package):

pnpm install --frozen-lockfile
pnpm --dir typescript build
# Then, in your consumer project:
npm install /absolute/path/to/schemair/typescript
import { createSchemaEnvironment } from 'schemair';
import { primitive, record, document } from 'schemair/author';
import type { Infer } from 'schemair/author';

const User = record({
  name: primitive('string'),
  age: { schema: primitive('number'), optional: true, description: 'Years' },
});
type UserValue = Infer<typeof User>; // { name: string; age?: number }
const userDocument = document({
  schemair: 'core-6',
  id: 'user',
  definitions: { User },
  root: 'User',
});
const env = createSchemaEnvironment([userDocument]);

Constructors cover all eleven schema kinds, full record/variant descriptions and document definitions. Infer preserves literals, keys and tuple positions, but references and broadly typed schemas remain unknown. It is static structural inference, not validation: metadata is ignored, and open-record index signatures overapproximate residual values by including explicit field types. Construction is shallow and does not validate or authenticate inputs; see authoring API.

General unions and admission profiles

The retired core-5 contract adds { kind: 'union', anyOf: [...] }: at least two complete schema nodes, with a payload accepted when any operand accepts it. Current core-6 retains this union behavior. Branches run in wire order without coercion; earlier branch rejections and non-limit unknowns are provisional and are discarded if a later branch accepts. Rejection aggregates branch diagnostics only after all branches reject. Exhaustion returns unknown / limit with bounded context, not a branch issue tree, unless an independent enclosing definite rejection was already established; a provisional branch rejection does not qualify. The default restricted profile rejects every union node; general admits them. Current documents use the explicit core-6 marker; core-5 and core-4 are historical and rejected. The profile is fixed in the environment and inherited by inline targets, payload validation, and relation checking; calls cannot override it.

TypeScript takes an environment options object; Python takes keyword-only profile and limits parameters. Select the profile on create/load, not on payload or relation calls:

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';

const choice = createSchemaEnvironment([{
  schemair: 'core-6', id: 'choice',
  definitions: [{ name: 'Root', schema: { kind: 'union', anyOf: [
    { kind: 'primitive', name: 'string' },
    { kind: 'primitive', name: 'number' },
  ] }}],
  root: 'Root',
}], { profile: 'general' });
if (choice.status === 'ok') {
  validateDocumentPayload(choice.environment, { document: 'choice' }, 42); // accepted
}
from schemair import create_environment, validate_document_payload

choice = create_environment([{
    "schemair": "core-6", "id": "choice",
    "definitions": [{"name": "Root", "schema": {"kind": "union", "anyOf": [
        {"kind": "primitive", "name": "string"},
        {"kind": "primitive", "name": "number"},
    ]}}],
    "root": "Root",
}], profile='general')
if choice.status == "ok":
    validate_document_payload(choice.value, {"document": "choice"}, 42)  # accepted

For union relation behavior, every source alternative must fit the complete target. Fitting one target alternative suffices, but collective coverage can conservatively remain unknown; branch mismatches alone do not prove incompatibility. See the relation tutorial for proof and witness limitations.

Explicit targets

Every record field descriptor has key and schema, and may set optional: true to permit absence. An omitted or false optional is required presence. The old core-1 required descriptor is rejected; this does not rename unrelated JSON Schema required keywords. Every call takes a target with a required document home and at most one of name (named definition) or value (inline schema). When neither target field is present, the document root is selected.

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';

const targets = createSchemaEnvironment([{
  schemair: 'core-6', id: 'main',
  definitions: [{ name: 'Address', schema: { kind: 'primitive', name: 'string' } }],
}]); // A definition library needs no root.
if (targets.status === 'ok') {
  validateDocumentPayload(targets.environment, { document: 'main', name: 'Address' }, 'Paris');
  validateDocumentPayload(targets.environment, {
    document: 'main', value: { kind: 'primitive', name: 'string' },
  }, 'Paris');
}

Standard predicates

Pass standard: true (Python: standard=True) to activate built-in semantic predicates and scalar/container constraints. Algorithms and budget accounting are specified across bindings rather than delegated to native regex or arithmetic.

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';

const ages = createSchemaEnvironment([{
  schemair: 'core-6', id: 'ages',
  definitions: [{ name: 'Root', schema: {
    kind: 'primitive', name: 'number',
    constraints: [
      { constraintPath: ['std', 'maximum'], args: 120 },
    ],
  }}],
  root: 'Root',
}]);
if (ages.status === 'ok') {
  const result = validateDocumentPayload(ages.environment, { document: 'ages' }, 200, { standard: true });
  // result.status === 'rejected'
}
from schemair import create_environment, validate_document_payload

ages = create_environment([{
    "schemair": "core-6", "id": "ages",
    "definitions": [{"name": "Root", "schema": {
        "kind": "primitive", "name": "number",
        "constraints": [{"constraintPath": ["std", "maximum"], "args": 120}],
    }}],
    "root": "Root",
}])
if ages.status == "ok":
    result = validate_document_payload(ages.value, {"document": "ages"}, 200, standard=True)
    # result.status == "rejected"

Portable-1 includes email, integer (finite integral binary64, not safe-integer only), generic uuid and uuid/v4, numeric/string bounds, minItems/maxItems, minProperties/maxProperties, argument-free uniqueItems, and positive-divisor multipleOf. The latter is exact dyadic binary64 arithmetic, not tolerance: 0.3 is not a multiple of 0.1. Date/time, URI/IP, and std/pattern remain unsupported. See the standard-predicate tutorial.

Core-6 and JSON Schema export

Core-6 provides array contains / minContains / maxContains, record propertyNames, and ordered patternProperties: [{ pattern, schema }]. These are first-class schema edges, not metadata arguments. Pattern matches do not exempt undeclared keys from additionalFields. The retired core-5 contract historically rejected these fields and remains negative compatibility evidence. Current closures contain only core-6 documents, and inline targets use their core-6 home document. No implicit upgrade or mixed-version current closure is supported.

exportJsonSchema(env, target, options?) and export_json_schema(env, target, *, mode='strict', standard=False, annotations=True, limits=None) produce ordinary Draft 2020-12 JSON Schema. Strict mode rejects omitted or nonportable assertions; best-effort returns a complete output with anchored losses. UUID/email are annotation-only, and multipleOf export supports only divisor 1 even though payload validation supports every positive finite binary64 divisor. Results name structural, normalized-domain, or best-effort fidelity; they are not a claim of arbitrary-validator equivalence. See the API contract.

Sharing a budget across calls

Create a Budget from schemair/extension and pass it through sharedBudget to make work and reference limits cumulative across payload and relation checks.

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';
import { Budget } from 'schemair/extension';

const budgetEnv = createSchemaEnvironment([{
  schemair: 'core-6', id: 'number',
  definitions: [{ name: 'Root', schema: { kind: 'primitive', name: 'number' } }],
  root: 'Root',
}]);
const budget = new Budget({ maxWork: 100, maxReferenceExpansions: 10, maxDepth: 256, maxNodes: 1000 });
if (budgetEnv.status === 'ok') {
  validateDocumentPayload(budgetEnv.environment, { document: 'number' }, 200, { sharedBudget: budget });
}

Python public calls accept per-call limits, not a shared budget.

Relation checking

checkDocumentRelation decides whether the source schema's value set is a subset of the target's. On incompatibility it returns a complete JSON witness that is simultaneously accepted by the source and rejected by the target.

import { createSchemaEnvironment, checkDocumentRelation } from 'schemair';

const versions = createSchemaEnvironment([
  { schemair: 'core-6', id: 'v1',
    definitions: [{ name: 'Root', schema: { kind: 'primitive', name: 'string' } }],
    root: 'Root' },
  { schemair: 'core-6', id: 'v2',
    definitions: [{ name: 'Root', schema: { kind: 'literal', literal: 'ok' } }],
    root: 'Root' },
]);
if (versions.status === 'ok') {
  const rel = checkDocumentRelation(versions.environment, { document: 'v1' }, { document: 'v2' });
  if (rel.status === 'incompatible') {
    console.log(rel.issues[0].details?.witness); // source accepts; target rejects
  }
}
from schemair import create_environment, check_document_relation

versions = create_environment([
    {"schemair": "core-6", "id": "v1",
     "definitions": [{"name": "Root", "schema": {"kind": "primitive", "name": "string"}}],
     "root": "Root"},
    {"schemair": "core-6", "id": "v2",
     "definitions": [{"name": "Root", "schema": {"kind": "literal", "literal": "ok"}}],
     "root": "Root"},
])
if versions.status == "ok":
    rel = check_document_relation(versions.value, {"document": "v1"}, {"document": "v2"})
    if rel.status == "incompatible":
        print(rel.issues[0].details["witness"])

Relation statuses are assignable, incompatible, unknown, or rejected. Only assignable proves containment. Applicable custom-provider checkRelation callbacks and the host metadataRelationFallback can prove local metadata implications; custom metadata is not inherently unsupported. Missing proofs, unresolved interpretations, conservative collective union coverage, or exhausted budgets can produce unknown; this is not incompatibility. Typed residuals are checked structurally, while some open residual comparisons remain undecided. See relation support and callback limits.

Extension providers

Register call-local vocabulary callbacks for custom predicates. Providers run in two phases (declaration check, then payload check) with budget sharing and mutation isolation — the host cannot alter the validated document.

import { createSchemaEnvironment, validateDocumentPayload } from 'schemair';
import type { PayloadProviderRegistration } from 'schemair';

function rangeArgs(args: unknown): { min: number; max: number } | undefined {
  if (args === null || typeof args !== 'object' || Array.isArray(args)) return;
  if (!('min' in args) || !('max' in args)) return;
  const { min, max } = args;
  if (typeof min === 'number' && Number.isFinite(min) &&
      typeof max === 'number' && Number.isFinite(max) && min <= max) {
    return { min, max };
  }
}

const rangeProvider: PayloadProviderRegistration = {
  vocabulary: 'constraint', path: ['ext', 'x-range'], carriers: ['number'],
  checkDeclaration(node, application) {
    return application.vocabulary === 'constraint' && rangeArgs(application.args)
      ? { status: 'valid' }
      : { status: 'invalid', issues: [{ path: [], message: 'expected finite min ≤ max' }] };
  },
  checkPayload(node, application, value) {
    const args = application.vocabulary === 'constraint' ? rangeArgs(application.args) : undefined;
    return args && typeof value === 'number' && Number.isFinite(value) &&
      value >= args.min && value <= args.max
      ? { status: 'accepted' }
      : { status: 'rejected', issues: [{ path: [], message: 'out of range' }] };
  },
};

const scores = createSchemaEnvironment([{
  schemair: 'core-6', id: 'score',
  definitions: [{ name: 'Root', schema: {
    kind: 'primitive', name: 'number',
    constraints: [{ constraintPath: ['ext', 'x-range'], args: { min: 0, max: 100 } }],
  }}],
  root: 'Root',
}]);
if (scores.status === 'ok') {
  validateDocumentPayload(scores.environment, { document: 'score' }, 42, { providers: [rangeProvider] });
}
import math
from schemair import create_environment, validate_document_payload

def finite_number(value):
    # bool is an int subclass in Python, but is not a JSON number.
    return type(value) in (int, float) and math.isfinite(value)

def range_args(args):
    if not isinstance(args, dict):
        return None
    lo, hi = args.get("min"), args.get("max")
    if finite_number(lo) and finite_number(hi) and lo <= hi:
        return lo, hi
    return None

def check_range_declaration(node, app):
    if range_args(app.get("args")) is not None:
        return {"status": "valid"}
    return {"status": "invalid", "issues": [{"path": [], "message": "expected finite min ≤ max"}]}

def check_range_payload(node, app, value):
    bounds = range_args(app.get("args"))
    if bounds is not None and finite_number(value) and bounds[0] <= value <= bounds[1]:
        return {"status": "accepted"}
    return {"status": "rejected", "issues": [{"path": [], "message": "out of range"}]}

range_provider = {
    "vocabulary": "constraint", "path": ["ext", "x-range"], "carriers": ["number"],
    "check_declaration": check_range_declaration, "check_payload": check_range_payload,
}
scores = create_environment([{
    "schemair": "core-6", "id": "score",
    "definitions": [{"name": "Root", "schema": {
        "kind": "primitive", "name": "number",
        "constraints": [{"constraintPath": ["ext", "x-range"], "args": {"min": 0, "max": 100}}],
    }}],
    "root": "Root",
}])
if scores.status == "ok":
    validate_document_payload(scores.value, {"document": "score"}, 42, providers=[range_provider])

Schema node kinds

Core-6 defines eleven schema node kinds:

Kind What it matches
primitive string, number, boolean, or null
literal Exactly one non-null scalar value
literalUnion Two or more distinct literals in one scalar domain
record Object with typed fields; closed by default
array Homogeneous sequence
tuple Fixed-length, positionally typed sequence
nullable null or any value matching the inner schema
union Any value accepted by one of at least two schema operands; requires the general profile
taggedUnion Discriminated union of record heads, keyed on a required string literal; heads may be open or typed-residual
localRef Reference to a definition in the same document
ref Reference to a named definition in any loaded document

Guarantees

  • No runtime dependencies. Neither package pulls in external code.
  • Bounded execution. Every call has configurable work, depth, node, and reference budgets. Default limits are safe for untrusted input. Budgets cannot be silently exceeded.
  • Snapshot isolation. Wire documents, targets, and payload values are snapshotted; later mutations cannot change the stored document environment. Callback functions/host I/O are not copied or sandboxed, and total host CPU, async wait time, and copying are not bounded by the kernel's work counter.
  • No global schema registry. Environments are process-local authenticated instances. Persist raw documents and rebuild; clones/deserialized environments are not authenticated. There is no built-in network/document cache.
  • Cross-binding parity. Both runtimes produce identical statuses, diagnostic codes, paths, and witness values on all conformance vectors, verified on every CI run.

Development

Requirements: Node 22, pnpm 10.6.5, Python 3.11.

pnpm install --frozen-lockfile
python -m venv .venv
.venv/bin/python -m pip install -r packaging/python-dev-requirements.txt

On Windows, use .venv\Scripts\python.exe instead of .venv/bin/python. The commands below use the POSIX path; they do not depend on shell activation.

# Build/install the local TypeScript package, including schemair/author
pnpm --dir typescript build
# In a consumer project: npm install /absolute/path/to/schemair/typescript

# TypeScript
pnpm --dir typescript typecheck
pnpm --dir typescript test

# Python
PYTHONPATH=python .venv/bin/python -m unittest discover -s python/tests -v

# Cross-binding parity (conformance only)
node scripts/check-runtime-parity.mjs --profile core-6-c1
node scripts/check-runtime-parity.mjs --profile core-6-stable

See CONTRIBUTING.md for the full workflow.

Metadata

Release files for schemair 0.7.0.dev6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for schemair 0.7.0.dev6
File Size Uploaded
schemair-0.7.0.dev6.tar.gz 77.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for schemair 0.7.0.dev6
File Interpreter ABI Platform
schemair-0.7.0.dev6-py3-none-any.whl Python 3 none any Details

Total release size: 146.6 kB

Release files / schemair-0.7.0.dev6.tar.gz

Download URL schemair-0.7.0.dev6.tar.gz
Size 77.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5ac4f4c21dfd000c41997550814216ac51322027f319f87cbed7bf6bf97f5b69
BLAKE2b-256 checksum
How to use checksums
21fbde82b3616c9ebcfe1556214e15d57abc44cbfe4251376b3460b9b54a7b99
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / schemair-0.7.0.dev6-py3-none-any.whl

Download URL schemair-0.7.0.dev6-py3-none-any.whl
Size 69.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
12e4734fee602a02f6d2f82080ec4b1779816626662da78f5fc518ad619df149
BLAKE2b-256 checksum
How to use checksums
a39d1825a53a0be263f7c3d39bb0446752be43b280156d4c3fb7777f5998ccde
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page