A comprehensive XML validator that validates against custom rule schemas and reports all failures at once, not just the first error encountered.
Unlike XSD validators, this module uses an intuitive XML-based rule format that mirrors your data structure. It performs syntax checking via fast-xml-parser, then validates frequency, type, range, length, pattern matching, ordering, uniqueness, and null constraints.
- ✅ Complete error reporting — Reports all validation failures in one pass
- ✅ Intuitive rule syntax — XML-based rules that mirror your data structure
- ✅ Rich validation types — String patterns, numeric ranges, custom validators
- ✅ Flexible constraints — Required fields, repeatable elements, occurrence limits
- ✅ Custom validators — Register your own validation functions
- ✅ Type safety — Built-in types: integers, decimals, dates, booleans, maps
- ✅ Ordering constraints — Enforce
before/afterpositional ordering between sibling tags - ✅ Date bounds —
min,max, andrangeontype="date"fields - ✅ Relational constraints —
sameAs,notSameAs,lessThan,moreThanacross sibling fields - ✅ Uniqueness constraints —
unique="true"(sibling scope) andunique="global"(document scope) - ✅ Range shorthand —
range="min..max"as readable sugar formin+max - ✅ TypeScript types — Bundled
.d.tsdeclarations - ✅ ES Modules — Full
import/exportsyntax; bundle with your own webpack/rollup config
npm install detailed-xml-validatorimport Validator from "detailed-xml-validator";
const rules = `<?xml version="1.0"?>
<students nillable="false">
<student repeatable minOccurs="1">
<firstname minLength="3" maxLength="10" nillable="false"></firstname>
<age type="positiveInteger" range="9..19"></age>
</student>
</students>`;
const xmlData = `<?xml version="1.0"?>
<students>
<student>
<firstname>Jo</firstname>
<age>25</age>
</student>
</students>`;
const validator = new Validator(rules);
const failures = validator.validate(xmlData);
if (failures.length > 0) {
console.log(`Found ${failures.length} validation issues:`);
failures.forEach(f => console.log(f));
} else {
console.log("Validation passed!");
const data = validator.data; // Access parsed XML as JS object
}Rules are written in XML format that mirrors your expected data structure:
<?xml version="1.0"?>
<root>
<element attribute="constraint" anotherAttribute="value">
<nestedElement></nestedElement>
</element>
</root>Use the special <:a> tag to define validation rules for XML attributes:
<student repeatable>
<:a>
<id length="6"></id>
<status pattern="active|inactive"></status>
</:a>
<n></n>
</student>By default, all elements are optional (nillable).
<!-- Optional -->
<nickname></nickname>
<!-- Required -->
<email nillable="false"></email><students>
<student repeatable minOccurs="1" maxOccurs="100">
<n></n>
</student>
</students>repeatable— Marks this as a list elementminOccurs— Minimum occurrences (default: 0)maxOccurs— Maximum occurrences (default: unlimited)
<age type="positiveInteger"></age>
<price type="positiveDecimal"></price>
<temperature type="integer"></temperature>
<rating type="decimal"></rating>
<count type="number"></count>
<birthdate type="date"></birthdate>
<name type="string"></name>
<metadata type="map"></metadata>Supported types: positiveInteger, positiveDecimal, integer, decimal, number, date, string (default), map.
<!-- Explicit min/max -->
<age type="integer" min="18" max="65"></age>
<!-- Range shorthand — equivalent to the above -->
<age type="integer" range="18..65"></age>
<!-- Works with decimals too -->
<price type="number" range="0.01..999.99"></price>range="min..max" is syntactic sugar for min + max. If you specify all three, the explicit min/max attributes win.
<username minLength="3" maxLength="20"></username>
<zipcode length="5"></zipcode>
<email pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}"></email>
<phone pattern_i="^\d{3}-\d{3}-\d{4}$"></phone>
<status in="active,inactive,pending"></status>
<country fixed="USA"></country>Pattern modifiers: pattern (default), pattern_i (case-insensitive), pattern_m (multiline), pattern_im / pattern_mi.
Enforce positional ordering between sibling tags in the XML document. Both attributes reference the name of a sibling element. This is a structural check — it validates tag order, not field values. Works on any tag type, not just dates.
<event>
<title></title>
<!-- startDate must appear after title in the XML -->
<startDate after="title"></startDate>
<!-- endDate must appear after startDate -->
<endDate after="startDate"></endDate>
</event>
<order>
<orderDate></orderDate>
<!-- shipDate must come after orderDate AND before deliveryDate -->
<shipDate after="orderDate" before="deliveryDate"></shipDate>
<deliveryDate></deliveryDate>
</order>Failure shape:
{ code: "after", path: "order.shipDate", actual: "shipDate", expected: "orderDate" }
{ code: "before", path: "order.shipDate", actual: "shipDate", expected: "deliveryDate" }- The check is skipped silently when the referenced sibling is absent from the data.
- To enforce that a reference field must be present, mark it
nillable="false".
Constrain type="date" fields to a specific date range. Accepts ISO 8601 date strings.
<!-- Explicit min/max -->
<birthDate type="date" min="1900-01-01" max="2010-12-31"></birthDate>
<!-- Range shorthand — equivalent to the above -->
<eventDate type="date" range="2024-01-01..2024-12-31"></eventDate>
<!-- Only a lower bound -->
<startDate type="date" min="2020-01-01"></startDate>range="min..max" works the same as for numeric fields. Explicit min/max take precedence over range when all three are set. Bounds are inclusive.
Failure shape:
{ code: "min", path: "event.startDate", actual: "2019-06-01", expected: "2020-01-01" }
{ code: "max", path: "event.birthDate", actual: "2025-01-01", expected: "2010-12-31" }The bounds check is skipped when the date value is itself invalid (a type error is reported instead).
Compare a field's value against another sibling field's value. All four attributes take the name of a sibling element as their value.
<order>
<originalPrice type="number"></originalPrice>
<!-- discountedPrice must be strictly less than originalPrice -->
<discountedPrice type="number" lessThan="originalPrice"></discountedPrice>
<!-- tax must be strictly less than discountedPrice -->
<tax type="number" lessThan="discountedPrice"></tax>
</order>
<event>
<!-- startDate value must be before endDate value -->
<startDate type="date" lessThan="endDate"></startDate>
<endDate type="date"></endDate>
</event>
<form>
<password></password>
<!-- confirmPassword must equal password -->
<confirmPassword sameAs="password"></confirmPassword>
</form>
<user>
<username></username>
<!-- password must not equal username -->
<password notSameAs="username"></password>
</user>| Attribute | Passes when |
|---|---|
lessThan="ref" |
this value < ref value (strictly) |
moreThan="ref" |
this value > ref value (strictly) |
sameAs="ref" |
this value == ref value |
notSameAs="ref" |
this value != ref value |
Comparison is type-aware based on the field's declared type:
type="date"→Date.parse()comparison- numeric types (
integer,number, etc.) →Number()comparison string/ no type → lexicographic comparison
Failure shape:
{ code: "lessThan", path: "order.discountedPrice", actual: "120", expected: "originalPrice" }
{ code: "sameAs", path: "form.confirmPassword", actual: "wrong", expected: "password" }Skipped silently when:
- The referenced sibling is absent from the data (use
nillable="false"on the ref field if you want that enforced separately) - Either value is a map/object rather than a primitive
- The ref value cannot be coerced to the expected type
Values must be unique within the repeatable collection they belong to.
<students>
<student repeatable>
<email unique="true"></email>
<studentId unique="true"></studentId>
</student>
</students>The same value is allowed in a different collection (a separate <students> block elsewhere in the document).
Values must be unique across the entire document, regardless of where the field appears.
<root>
<groupA>
<transactionId unique="global"></transactionId>
</groupA>
<groupB>
<transactionId unique="global"></transactionId>
</groupB>
</root>Failure shape:
{ code: "unique", path: "students.student[2].email", value: "dup@example.com" }Only the second and subsequent occurrences of a duplicate produce a failure. The first occurrence is always accepted.
const validator = new Validator(rules, {
unknownAllow: true, // default: true
boolean: ["true", "false", "yes", "no"], // default: ["true", "false"]
});unknownAllow— Whenfalse, reports an"unknown"failure for any element not defined in rules.boolean— Array of strings considered valid fortype="boolean"fields.
validator.register("isEmail", (value, path) => {
if (!value.includes("@")) {
return { code: "invalid-email", path, value };
}
});Reference by name in rules:
<email checkBy="isEmail" nillable="false"></email>Return any object to push a failure, or nothing (/ undefined / null) to pass.
<?xml version="1.0"?>
<students nillable="false">
<student repeatable minOccurs="1">
<:a>
<id length="6"></id>
</:a>
<firstname minLength="3" maxLength="10" nillable="false"></firstname>
<email pattern="[a-z0-9]+@school\.org" nillable="false" unique="true"></email>
<age type="positiveInteger" range="9..19"></age>
<enrolledOn type="date" min="2000-01-01"></enrolledOn>
<!-- graduatesOn must appear after enrolledOn in XML, and its value must be later -->
<graduatesOn type="date" after="enrolledOn" moreThan="enrolledOn"></graduatesOn>
<marks>
<subject repeatable minOccurs="5" maxOccurs="6">
<name pattern="math|hindi|english|science|history"></name>
<score type="positiveDecimal" range="0..100"></score>
</subject>
</marks>
</student>
</students>import Validator from "detailed-xml-validator";
import { readFileSync } from "fs";
const rules = readFileSync("rules.xml", "utf8");
const xmlData = readFileSync("data.xml", "utf8");
const validator = new Validator(rules, { unknownAllow: false });
const failures = validator.validate(xmlData);
if (failures.length > 0) {
failures.forEach(f => console.error(`[${f.code}] ${f.path}`));
process.exit(1);
}
// validator.data contains the parsed XML as a plain JS objectcode |
Meaning | Extra fields |
|---|---|---|
missing |
Required element absent | — |
unknown |
Element not in rules (when unknownAllow: false) |
— |
unexpected sequence |
Array where scalar expected | — |
unexpected value in a map |
Scalar where map expected | value |
not a <type> |
Value fails type check | value |
min / max |
Numeric or date out of range | actual, expected |
minOccurs / maxOccurs |
Occurrence count out of range | actual, expected |
minLength / maxLength / length |
String length violation | actual, expected |
pattern |
Regex mismatch | actual, expected |
fixed / in |
Value not in allowed set | actual, expected |
after |
Tag does not appear after referenced sibling in XML | actual (tag name), expected (ref tag name) |
before |
Tag does not appear before referenced sibling in XML | actual (tag name), expected (ref tag name) |
lessThan |
Field value is not strictly less than sibling value | actual, expected (ref field name) |
moreThan |
Field value is not strictly greater than sibling value | actual, expected (ref field name) |
sameAs |
Field value does not equal sibling value | actual, expected (ref field name) |
notSameAs |
Field value equals sibling value (should differ) | actual, expected (ref field name) |
unique |
Duplicate value violates uniqueness constraint | value |
Type declarations are bundled at src/index.d.ts. Add to your tsconfig.json:
{
"compilerOptions": {
"typeRoots": ["./node_modules/detailed-xml-validator/src"]
}
}Or import the types directly:
import Validator, { ValidationFailure, OrderingFailure, RelationalFailure, DateBoundsFailure, UniqueFailure } from "detailed-xml-validator";rules— XML string containing validation rules. Throws if empty, non-string, or malformed.options— OptionalValidatorOptionsobject.
Returns ValidationFailure[]. Empty array means the document is valid. Throws if xmlData is empty, non-string, or malformed.
Registers a custom validator. fn(value, path) should return a failure object or falsy.
The parsed XML as a plain JS object after the last validate() call. null before first call.
- Simpler syntax — Rules look like your data, not a separate schema language
- All errors at once — No stopping at the first failure
- Business-logic validators —
checkByfor custom JS validation - Ordering & uniqueness — Constraints XSD cannot express cleanly
- JavaScript-native — No external tools, works in Node.js directly
MIT — see LICENSE