High-performance XML Schema (XSD) validation for JavaScript, powered by Apache Xerces-C++ compiled to WebAssembly.
Installation · Quick Start · API Reference · Benchmarks · Architecture · Playground
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.
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.
- 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:includeandxs:importacross 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.
npm install xerces-wasmRequires Node.js 18 or later. It also runs in browsers and web workers; see Browsers and web workers.
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();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. |
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? }.
Convenience wrapper that reads the XML and XSD from disk before validating. Node.js only.
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.
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.
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);-
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
fileargument, as innew URL(`./${file}`, import.meta.url): Vite rewrites that pattern into a lookup among your own source files, which returnsundefined. -
webpack 5 needs two settings. It does not apply the
browserfield tonode:imports, and in a browser build it replaces__filenamewith"/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.
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.
- 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.
Xerces-C++ validation has two distinct phases: compiling schemas, which is expensive, and validating documents, which is fast.
- Schema compilation (one time). XSD files are parsed and compiled into DFA structures, stored in an
XMLGrammarPool. - Validation (per document). The XML input is streamed through the pre-compiled grammar using a transient
SAXParser.
xerces-wasm separates these concerns explicitly:
- Persistent state. Each project compiles its schemas once into a locked
XMLGrammarPoolin the WASM heap, isolated from all other projects. - Transient engine. Every
validate()call creates a disposableSAXParser, attaches it to the project's pool, validates, and destroys itself.
- Schema-Aware XML, a VS Code extension for XML. Its XSD validation is powered by
xerces-wasm, throughxml-language-serviceandxml-language-server. xml-language-service, an editor-agnostic XML language service that validates against XSDs with this package.
An interactive browser playground is available for trying the validator without installing anything. Source: xerces-playground.
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 testFor reproducible WASM builds, see docs/reproducible-wasm-build.md. Release history is tracked in the changelog.
Released under the MIT License.
This package includes Apache Xerces-C++, licensed under Apache-2.0. See LICENSE-APACHE and NOTICE.



