Skip to content

Converter - Core Concepts

This guide covers the fundamental concepts you need to understand to use the StAX-XML converter effectively.

The x object is the primary interface for creating XML schemas. It provides factory methods for all schema types:

import { x } from 'stax-xml/converter';
// Create schemas
const stringSchema = x.string();
const numberSchema = x.number();
const objectSchema = x.object({...});
const arraySchema = x.array(elementSchema, xpath);

All schema methods return new schema instances, making the API immutable and chainable:

const schema = x.number()
.xpath('//price')
.min(0)
.max(1000);
// Each method returns a new schema
const baseNumber = x.number();
const withXPath = baseNumber.xpath('//value'); // New instance
const withMin = withXPath.min(0); // New instance
// Original remains unchanged
console.log(baseNumber.options.xpath); // undefined
console.log(withXPath.options.xpath); // '//value'

This immutability ensures schemas are reusable and composable:

// Base price schema
const priceSchema = x.number().min(0);
// Reuse with different XPath
const retailPrice = priceSchema.xpath('//retailPrice');
const wholesalePrice = priceSchema.xpath('//wholesalePrice');

The converter provides multiple parsing methods for different use cases:

Use parseSync() for synchronous, blocking parsing:

const schema = x.string().xpath('//title');
const result = schema.parseSync(xmlString);
// Returns: string

When to use:

  • XML is already in memory as a string
  • You’re in a synchronous context
  • Performance is critical (slightly faster than async)

Use parse() for asynchronous parsing:

const schema = x.object({
name: x.string().xpath('//name'),
value: x.number().xpath('//value')
});
const result = await schema.parse(xmlString);
// Returns: Promise<{ name: string; value: number; }>

When to use:

  • Parsing large XML documents
  • You’re already in an async context
  • You want non-blocking execution

Safe parsing methods return a result object instead of throwing errors:

const schema = x.number().xpath('//age').min(0).max(120);
// Safe sync parsing
const result = schema.safeParseSync('<age>150</age>');
if (result.success) {
console.log(result.data); // number
} else {
console.log(result.issues); // Array of error objects
}
// Safe async parsing
const result = await schema.safeParse('<age>150</age>');

ParseResult type:

type ParseResult<T> =
| { success: true; data: T }
| { success: false; issues: Array<{ message: string; path?: string }> };

When to use:

  • You want to handle errors explicitly
  • You’re validating user input
  • You don’t want to use try/catch blocks

The converter provides automatic TypeScript type inference using the Infer utility type:

import { x, type Infer } from 'stax-xml/converter';
const userSchema = x.object({
id: x.number().xpath('//id'),
username: x.string().xpath('//username'),
email: x.string().xpath('//email'),
active: x.string().xpath('//active').transform(v => v === 'true')
});
// Automatically infer the type
type User = Infer<typeof userSchema>;
// {
// id: number;
// username: string;
// email: string;
// active: boolean; // Note: transformed type!
// }
// Use the inferred type
const users: User[] = [];

Type inference works with nested structures and transformations:

const productSchema = x.object({
name: x.string().xpath('//name'),
price: x.number().xpath('//price'),
tags: x.array(x.string(), '//tag'),
specs: x.object({
weight: x.number().xpath('./weight'),
dimensions: x.string().xpath('./dimensions')
}).xpath('//specs').optional()
});
type Product = Infer<typeof productSchema>;
// {
// name: string;
// price: number;
// tags: string[];
// specs: { weight: number; dimensions: string; } | undefined;
// }
  1. Define schemas as const:

    const schema = x.object({...}) as const;
  2. Extract types early:

    type MyData = Infer<typeof mySchema>;
  3. Use in function signatures:

    function processData(data: Infer<typeof schema>) {
    // TypeScript knows the exact shape
    }

XPath is the primary method for selecting elements in XML documents.

// Absolute path from root
x.string().xpath('/root/element/child')
// Descendant search (anywhere in document)
x.string().xpath('//element')
// Attribute access
x.string().xpath('/@id')
x.string().xpath('//@href')
// Combined
x.string().xpath('/root/item/@name')

String and Number schemas:

const title = x.string().xpath('/book/title');
const price = x.number().xpath('/book/price');

Object schemas:

// XPath on individual fields
const book = x.object({
title: x.string().xpath('/book/title'),
author: x.string().xpath('/book/author')
});
// XPath on entire object (scopes child XPaths)
const book = x.object({
title: x.string().xpath('./title'), // Relative to /book
author: x.string().xpath('./author') // Relative to /book
}).xpath('/book');

Array schemas:

// XPath is REQUIRED for arrays
const items = x.array(
x.string(),
'//item' // Selects all <item> elements
);
// Array of objects
const books = x.array(
x.object({
title: x.string().xpath('./title'),
price: x.number().xpath('./price')
}),
'//book' // Each book becomes an object
);

Use predicates to filter elements:

// By attribute value
x.array(
x.object({...}),
'//book[@category="fiction"]'
)
// By position
x.string().xpath('//item[1]') // First item
// By element content
x.array(
x.object({...}),
'//product[@available="true"]'
)

The converter provides detailed error information when parsing fails:

import { XmlParseError } from 'stax-xml/converter';
try {
const result = schema.parseSync(invalidXml);
} catch (error) {
if (error instanceof XmlParseError) {
console.log(error.message); // Human-readable message
console.log(error.issues); // Array of specific issues
}
}

Common error scenarios:

1. Validation Errors

const schema = x.number().xpath('//age').min(18);
schema.parseSync('<age>15</age>');
// Error: Value 15 is less than minimum 18

2. Type Conversion Errors

const schema = x.number().xpath('//count');
schema.parseSync('<count>abc</count>');
// Error: Expected number, got NaN

3. Missing Required Fields

const schema = x.object({
required: x.string().xpath('//required')
});
schema.parseSync('<root></root>');
// Returns: { required: '' } (empty string, not error)

4. XPath Errors

const schema = x.array(x.string()); // Missing xpath
schema.parseSync('<root></root>');
// Error: Array schema requires xpath

1. Use Optional

const schema = x.object({
id: x.number().xpath('//id'),
optional: x.string().xpath('//optional').optional()
});
// Returns: { id: number; optional: string | undefined }

2. Use Safe Parsing

const result = schema.safeParseSync(xml);
if (!result.success) {
// Handle errors gracefully
return defaultValue;
}
return result.data;

3. Use Transform for Defaults

const schema = x.string()
.xpath('//value')
.transform(v => v || 'default');

Schemas can be composed and reused:

// Define reusable schemas
const priceSchema = x.number().min(0);
const idSchema = x.number().int().min(1);
// Compose in objects
const productSchema = x.object({
id: idSchema.xpath('//id'),
price: priceSchema.xpath('//price'),
salePrice: priceSchema.xpath('//salePrice').optional()
});
const addressSchema = x.object({
street: x.string().xpath('./street'),
city: x.string().xpath('./city'),
zip: x.string().xpath('./zip')
}).xpath('/address');
const personSchema = x.object({
name: x.string().xpath('/person/name'),
address: addressSchema // Nested object schema
});
const tagSchema = x.string();
const tagsArray = x.array(tagSchema, '//tag');
const articleSchema = x.object({
title: x.string().xpath('//title'),
tags: tagsArray
});
// Synchronous - slightly faster for small documents
const result = schema.parseSync(smallXml);
// Asynchronous - better for large documents
const result = await schema.parse(largeXml);
// ❌ Slow - searches entire document multiple times
const schema = x.object({
a: x.string().xpath('//a'),
b: x.string().xpath('//b'),
c: x.string().xpath('//c')
});
// ✅ Better - single root XPath, relative children
const schema = x.object({
a: x.string().xpath('./a'),
b: x.string().xpath('./b'),
c: x.string().xpath('./c')
}).xpath('/root');

Use compile() when you parse many documents with the same schema:

const personSchema = x.object({
id: x.number().xpath('./@id').int(),
name: x.string().xpath('./name'),
age: x.number().xpath('./age').int(),
birthday: x.string().xpath('./birthday'),
married: x.string()
.xpath('./married')
.transform(value => value === 'true'),
firstTime: x.string().xpath('./married/@firstTime'),
nickname: x.string().xpath('./nickname').optional(),
address: x.object({
city: x.string().xpath('./city'),
zip: x.string().xpath('./zip/text()')
}).xpath('./address'),
aliases: x.array(x.string(), './alias'),
contacts: x.array(
x.object({
type: x.string().xpath('./@type'),
value: x.string().xpath('./value/text()')
}),
'./contact'
)
});
const compiledSchema = x.object({
datasetId: x.string().xpath('/dataset/@id'),
title: x.string().xpath('/dataset/title/text()'),
metadata: x.object({
source: x.string().xpath('./source'),
generatedAt: x.string().xpath('./generatedAt')
}).xpath('/dataset/metadata'),
labels: x.array(x.string(), '/dataset/labels/label'),
people: x.array(personSchema, '//person')
}).compile();
const result = compiledSchema.parseSync(xml);

compile() keeps the same public API, but the fastest path is available only for schema shapes that can be lowered to fixed XML event dispatch.

The schema above combines the common fast-path shapes in one compiled schema: absolute element and attribute selectors, direct text() selectors, a descendant array boundary with //person, relative selectors inside each person item, nested objects, scalar arrays, object arrays, optional fields, and transforms. Numeric validation like .int() is still applied after text extraction.

Fast-path friendly shapes:

ShapeExample
Absolute element paths/catalog/book/title
Descendant element paths//book
Absolute or relative attributes/catalog/book/@id, ./@id
Relative fields inside object or array items./title, ./author/name
Direct text selection/message/text(), ./title/text()
Objects with scalar fieldsx.object({ title: x.string().xpath('./title') }).xpath('/book')
Arrays of scalars or objectsx.array(x.string(), '/tags/tag'), x.array(bookSchema, '/catalog/book')
Nested objects, optional fields, transformsx.object({...}).optional().transform(...)

Shapes that fall back to the normal converter path:

ShapeExample
Wildcards/catalog/*
Predicates//book[@id="1"], //book[1]
Ambiguous relative paths without ./title
Nested arraysx.array(x.array(x.string(), './value'), '/group')
Arrays that define both an array XPath and an element XPathx.array(x.string().xpath('./title'), '/book')
Custom or unsupported schema wrappersUser-defined schema subclasses

Fallback preserves behavior, so these schemas still parse correctly after compile(). They just do not get the dispatch fast path.

// ❌ Creates new schema every time
function parseUser(xml: string) {
const schema = x.object({...});
return schema.parseSync(xml);
}
// ✅ Define schema once, reuse
const userSchema = x.object({...});
function parseUser(xml: string) {
return userSchema.parseSync(xml);
}