Skip to content

About

npm Package - Fast XML schema validator using Apache Xerces-C++ compiled to WebAssembly with in-memory XSD caching.

Resources

Stars

18 stars

Watchers

2 watching

Forks

Repository files navigation

xerces-wasm

High-performance XML Schema (XSD) validation for JavaScript, powered by Apache Xerces-C++ compiled to WebAssembly.

npm version npm downloads node license

Installation · Quick Start · API Reference · Benchmarks · Architecture · Playground


Try it in your browser

xerces-wasm playground: an XSD on the left, an XML document on the right, validated live

Paste or drop your own XSD and XML into the playground and see the Xerces diagnostics instantly. It runs entirely in your browser, so nothing is uploaded.

Overview

xerces-wasm brings the validation engine of Apache Xerces-C++ to Node.js and the browser. Schemas are compiled once into an in-memory grammar pool and reused across every subsequent validation, which removes the per-call schema parsing cost that dominates most WebAssembly XML validators.

Features

  • Reference-grade validation — the same Xerces-C++ engine used across the industry, with full XSD 1.0 support.
  • Compile once, validate many — schemas are cached in WebAssembly memory and reused on every call.
  • Modular schemas — native support for xs:include and xs:import across multiple files.
  • Project isolation — each validator owns its own grammar pool, so identical XML can be valid in one project and invalid in another.
  • Structured diagnostics — separate well-formedness and schema errors, each with line, column, and severity.
  • Typed API — ships with TypeScript declarations.

Installation

npm install xerces-wasm

Requires Node.js 18 or later. It also runs in browsers and web workers; see Browsers and web workers.

Quick Start

import { createProjectValidator } from "xerces-wasm";

// 1. Compile the schemas once. The grammar pool is cached in WASM memory.
const validator = await createProjectValidator({
  entry: "main.xsd",
  files, // Record<string, string>: { filename: xsdText }
});

// 2. Validate as many documents as needed against the cached pool.
const result = await validator.validate(`<log level="full"/>`);

if (!result.valid) {
  for (const e of [...result.parseErrors, ...result.schemaErrors]) {
    console.error(`${e.line}:${e.column} [${e.severity}] ${e.message}`);
  }
}

// 3. Release the native allocations when the validator is no longer needed.
validator.destroy();

API Reference

createProjectValidator(options)

Compiles a set of XSD files into a reusable validator. Recommended for any workload that validates more than one document.

Option Type Description
entry string Root XSD filename. Must be a key of files.
files Record<string, XmlInput> All schema files, keyed by bare filename.
targetNamespace string (optional) Target namespace. Detected from the entry schema if omitted.

Returns a ProjectValidator:

Method Description
validate(xml) Validates a document against the cached grammar pool. Returns Promise<ValidationResult>.
reload(files) Recompiles the grammar pool from an updated set of schema files.
destroy() Frees the grammar pool and all associated WASM memory.

validate(xml, xsd, targetNamespace?)

One-off validation. The schema is parsed on every call, so prefer createProjectValidator for repeated use. xsd accepts a single schema or a bundle: { entry, imports? }.

validateFiles(xmlPath, xsd)

Convenience wrapper that reads the XML and XSD from disk before validating. Node.js only.

initialize(options?)

Loads the WebAssembly module ahead of time and says where its file comes from. Calling it is optional: without it, the module loads on the first validation and looks for xerces_validator.wasm next to the script. In a browser that default does not always work; see Browsers and web workers.

Option Description
wasmBinary The contents of xerces_validator.wasm as an ArrayBuffer or Uint8Array. Nothing is read or fetched.
locateFile (file, scriptDirectory) => string. Returns where to load xerces_validator.wasm from: a URL in a browser or web worker, which is fetched; a file path or file: URL on Node.js, which is read from disk (an http(s) URL does not work there).

Call it once, before any validation. It rejects if the module is already loaded or loading. If loading fails, the next call tries again.

ValidationResult

interface ValidationResult {
  valid: boolean;
  parseErrors: Diagnostic[];   // well-formedness errors
  schemaErrors: Diagnostic[];  // XSD rule violations
}

interface Diagnostic {
  message: string;
  line: number;
  column: number;
  severity: "warning" | "error" | "fatal";
}

XmlInput accepts string, Buffer, Blob, or File.

Browsers and web workers

A bundler that targets the browser picks a browser build of the package through the browser field in package.json. It has the same API without validateFiles(), and it imports no Node.js built-in.

The WebAssembly file is not part of your bundle, so the module has to find node_modules/xerces-wasm/wasm/xerces_validator.wasm at run time. Where it looks by default depends on how your bundle is loaded:

Bundle loaded as Default location of xerces_validator.wasm
A web worker (classic or module) Next to the worker script.
A classic <script> Next to the script.
A <script type="module"> Relative to the page, not the bundle. Module scripts have no document.currentScript, so the module cannot tell where the bundle is.

Unless your bundle is a worker or a classic script with the file copied beside it, tell the module where the file is with initialize(), before the first validation. Give it a URL that does not depend on the bundler, for example a fixed path you copy the file to:

import { initialize, validate } from "xerces-wasm";

await initialize({ locateFile: () => "/assets/xerces_validator.wasm" });

// Or hand over bytes you already have:
// await initialize({ wasmBinary: await (await fetch(wasmUrl)).arrayBuffer() });

const result = await validate(xmlText, xsdText);

Bundler notes

  • esbuild works with no configuration.

  • Vite works with no configuration. Let Vite copy the file and give you its URL with an asset import:

    import wasmUrl from "xerces-wasm/wasm/xerces_validator.wasm?url";
    await initialize({ locateFile: () => wasmUrl });

    Do not build the URL from the file argument, as in new URL(`./${file}`, import.meta.url): Vite rewrites that pattern into a lookup among your own source files, which returns undefined.

  • webpack 5 needs two settings. It does not apply the browser field to node: imports, and in a browser build it replaces __filename with "/index.js", which hides the script location from the module:

    // webpack.config.js
    module.exports = {
      externals: { "node:fs": "{}", "node:crypto": "{}" },
      node: { __filename: false, __dirname: false },
    };

These setups are checked in headless Chrome, with esbuild 0.25, Vite 7.1 and webpack 5.101.

Performance

Stateless validators re-parse the schema on every call. xerces-wasm parses it once, so per-document cost is limited to validation itself.

The results below compare xerces-wasm with xmllint-wasm, measured on an Apple M4 with Node.js v22 over five interleaved trials.

Scenario xmllint-wasm xerces-wasm Improvement
Single schema, warm loop (1,000 runs) 22,908 ms 41.7 ms 549×
Multi-file modular schema (4 XSDs) 23,916 ms 69.1 ms 346×
Small payload latency (1.2 KB) 23.19 ms / call 0.06 ms / call 362×
Medium payload throughput (100 KB) 3.59 MB/s 34.70 MB/s 9.6×
Schema-invalid XML 24.59 ms / call 0.07 ms / call 355×
Memory delta (1,000 runs) +2.25 MB +0.41 MB ~5.5× lower
50 parallel validations (Promise.all) 221.58 ms 3.57 ms 62×

Methodology and reproducible code: xml-val-benchmark.

Usage guidance

  • Best fit: services that validate many documents against a fixed set of schemas, such as APIs, message pipelines, and editor tooling.
  • Large documents: for payloads above roughly 5 MB, validation becomes compute-bound and streaming validators can reach higher throughput.
  • Threading: validation runs synchronously on the calling thread. For batches of multi-megabyte documents, run validation in a worker thread or background queue.

Architecture

Xerces-C++ validation has two distinct phases: compiling schemas, which is expensive, and validating documents, which is fast.

Xerces validation phases

Xerces validation lifecycle

  1. Schema compilation (one time). XSD files are parsed and compiled into DFA structures, stored in an XMLGrammarPool.
  2. Validation (per document). The XML input is streamed through the pre-compiled grammar using a transient SAXParser.

xerces-wasm separates these concerns explicitly:

xerces-wasm architecture

  • Persistent state. Each project compiles its schemas once into a locked XMLGrammarPool in the WASM heap, isolated from all other projects.
  • Transient engine. Every validate() call creates a disposable SAXParser, attaches it to the project's pool, validates, and destroys itself.

Used by

Playground

An interactive browser playground is available for trying the validator without installing anything. Source: xerces-playground.

Development

Requires Git, Node.js, and an internet connection. Emscripten and Xerces-C++ are fetched automatically.

git clone --recurse-submodules https://github.com/harshanacz/xerces-wasm-validator
cd xerces-wasm-validator

npm install
npm run build:wasm   # Compile Xerces-C++ to wasm/xerces_validator.{js,wasm}
npm run build:ts     # Compile TypeScript to dist/
npm test

For reproducible WASM builds, see docs/reproducible-wasm-build.md. Release history is tracked in the changelog.

License

Released under the MIT License.

This package includes Apache Xerces-C++, licensed under Apache-2.0. See LICENSE-APACHE and NOTICE.

About

npm Package - Fast XML schema validator using Apache Xerces-C++ compiled to WebAssembly with in-memory XSD caching.

Resources

Stars

18 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages