JAXB-like XML binding for TypeScript.
xmlbind-ts is a TypeScript library that provides JAXB-style XML data binding using decorators. It allows you to:
- Map TypeScript classes to XML documents using decorators
- Marshal (serialize) TypeScript objects to XML
- Unmarshal (deserialize) XML to TypeScript objects
- Generate TypeScript classes from XSD schemas
npm install @neumaennl/xmlbind-tsThis library supports both legacy decorators (TypeScript's experimental decorators) and Stage 3 decorators (the TC39 standard):
- Legacy decorators: Used when
experimentalDecorators: trueis set in yourtsconfig.json - Stage 3 decorators: Used when
experimentalDecoratorsis not enabled (TypeScript 5.0+)
Both decorator formats are fully supported and the library automatically detects which format is being used. You can use either configuration:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// ... other options
}
}{
"compilerOptions": {
"target": "ES2022",
// ... other options
// Note: experimentalDecorators is NOT set
}
}Both configurations work seamlessly with all decorators (@XmlRoot, @XmlElement, @XmlAttribute, etc.).
Here's a simple example of defining a class and marshalling/unmarshalling XML:
import {
XmlRoot,
XmlElement,
XmlAttribute,
marshal,
unmarshal,
} from "@neumaennl/xmlbind-ts";
@XmlRoot("Person", { namespace: "http://example.com/ns" })
class Person {
@XmlAttribute("id")
id!: number;
@XmlElement("name", { type: String })
name!: string;
@XmlElement("age", { type: Number })
age!: number;
@XmlElement("alias", { type: String, array: true })
alias!: string[];
}
// Unmarshal XML to object
const xml = `<?xml version="1.0"?>
<Person xmlns="http://example.com/ns" id="42">
<name>John Doe</name>
<age>30</age>
<alias>J</alias>
<alias>Johnny</alias>
</Person>`;
const person = unmarshal(Person, xml);
console.log(person.name); // "John Doe"
console.log(person.age); // 30
console.log(person.alias); // ["J", "Johnny"]
// Marshal object to XML
const xmlOutput = marshal(person);
console.log(xmlOutput);Marks a class as an XML root element.
@XmlRoot(name?: string, options?: { namespace?: string; prefixes?: Record<string, string> })Parameters:
name(optional): The XML element name. Defaults to the class name.options.namespace(optional): The XML namespace URI.options.prefixes(optional): Map of namespace URIs to preferred prefixes
Example:
@XmlRoot("Book", { namespace: "http://example.com/library" })
class Book {
// ...
}Maps a class property to an XML element.
@XmlElement(name?: string, options?: {
type?: any;
array?: boolean;
namespace?: string;
nillable?: boolean;
})Parameters:
name(optional): The XML element name. Defaults to the property name.options.type(optional): The type constructor (String, Number, Boolean, or a custom class/enum).options.array(optional): If true, the property represents an array of elements.options.namespace(optional): The XML namespace for this element.options.nillable(optional): If true, allows null/nil values.
Example:
class Library {
@XmlElement("book", { type: Book, array: true })
books?: Book[];
@XmlElement("description", { type: String })
description?: string;
}Maps a class property to an XML attribute.
@XmlAttribute(name?: string, options?: { namespace?: string })Parameters:
name(optional): The XML attribute name. Defaults to the property name.options.namespace(optional): The XML namespace for this attribute.
Example:
class Book {
@XmlAttribute("isbn")
isbn?: string;
@XmlAttribute("id")
id!: number;
}Maps a class property to the text content of an XML element.
@XmlText()Example:
@XmlRoot("Comment")
class Comment {
@XmlAttribute("author")
author?: string;
@XmlText()
content?: string;
}
// Produces: <Comment author="John">This is a comment</Comment>Maps a class property to capture wildcard XML elements (xs:any).
@XmlAnyElement()This property will capture any XML child elements that are not explicitly mapped by other @XmlElement decorators. The property should be typed as unknown[].
Example:
@XmlRoot("FlexibleContainer")
class FlexibleContainer {
@XmlElement("knownField", { type: String })
knownField?: string;
@XmlAnyElement()
additionalElements?: unknown[];
}Maps a class property to capture wildcard XML attributes (xs:anyAttribute).
@XmlAnyAttribute()This property will capture any XML attributes that are not explicitly mapped by other @XmlAttribute decorators. The property should be typed as { [name: string]: string }.
Example:
@XmlRoot("FlexibleElement")
class FlexibleElement {
@XmlAttribute("id")
id?: string;
@XmlAnyAttribute()
additionalAttributes?: { [name: string]: string };
}Converts a TypeScript object to an XML string.
function marshal(obj: any): string;Example:
const person = new Person();
person.name = "Jane Doe";
person.age = 25;
const xml = marshal(person);Converts an XML string to a TypeScript object.
function unmarshal<T>(ctor: new () => T, xml: string): T;Example:
const xml = "<Person><name>Jane Doe</name><age>25</age></Person>";
const person = unmarshal(Person, xml);Here's a more complex example with nested objects:
@XmlRoot("Address")
class Address {
@XmlElement("street", { type: String })
street!: string;
@XmlElement("city", { type: String })
city!: string;
@XmlElement("zipCode", { type: String })
zipCode!: string;
}
@XmlRoot("Person", { namespace: "http://example.com/ns" })
class Person {
@XmlAttribute("id")
id!: number;
@XmlElement("name", { type: String })
name!: string;
@XmlElement("age", { type: Number })
age?: number;
@XmlElement("address", { type: Address })
address?: Address;
@XmlElement("phoneNumbers", { type: String, array: true })
phoneNumbers?: string[];
}
const xml = `<?xml version="1.0"?>
<Person xmlns="http://example.com/ns" id="1">
<name>John Doe</name>
<age>30</age>
<address>
<street>123 Main St</street>
<city>Springfield</city>
<zipCode>12345</zipCode>
</address>
<phoneNumbers>555-1234</phoneNumbers>
<phoneNumbers>555-5678</phoneNumbers>
</Person>`;
const person = unmarshal(Person, xml);
console.log(person.address?.city); // "Springfield"
console.log(person.phoneNumbers); // ["555-1234", "555-5678"]The library includes a command-line tool to generate TypeScript classes from XSD schemas.
xsd2ts input.xsd output-directoryimport { generateFromXsd } from "@neumaennl/xmlbind-ts";
const xsd = `<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/ns"
elementFormDefault="qualified">
<xsd:complexType name="Person">
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="age" type="xsd:int"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:int"/>
</xsd:complexType>
</xsd:schema>`;
generateFromXsd(xsd, "./output");
// Generates Person.ts with appropriate decoratorsThe XSD generator supports a comprehensive set of XML Schema features:
- Complex Types: Full support for named and anonymous complex types
- Simple Types: Enumerations, restrictions, unions, and lists
- Elements and Attributes: With references, namespaces, and form qualifications
- Compositors:
xs:sequence,xs:choice, andxs:all - Content Models: simpleContent, complexContent, mixed content
- Groups:
xs:groupandxs:attributeGroupreferences with proper expansion - Wildcards:
xs:any(generates@XmlAnyElement()) andxs:anyAttribute(generates@XmlAnyAttribute()) - References: Element and attribute references with proper namespace handling
- Type Derivation: Extension and restriction of complex types
- Union Types: Generates TypeScript union types (
string | number) - List Types: Generates TypeScript array types (
string[]) - Imports: Basic support for schema imports with namespace prefixes
- Reserved Words: Automatically sanitizes TypeScript reserved keywords with
_suffix - Namespace Prefixes: Automatically detects and handles namespace prefixes
- Collision Avoidance: Resolves naming conflicts between types and elements
- Mixed Content: Generates
@XmlText()properties for elements with mixed content
Important: The XSD generator reflects XSD requiredness in generated TypeScript properties:
- Required elements (default
minOccurs="1") generate non-optional properties with definite assignment assertion (prop!: Type) - Optional elements (
minOccurs="0") generate optional properties (prop?: Type) - Required attributes (
use="required") generate non-optional properties (prop!: Type) - Optional attributes (default or
use="optional") generate optional properties (prop?: Type) - Elements inside xs:choice are always optional, regardless of
minOccurs - Arrays (
maxOccurs > 1) remain arrays; optionality is based onminOccurs
Example:
<xsd:complexType name="Example">
<xsd:sequence>
<xsd:element name="required" type="xsd:string"/>
<xsd:element name="optional" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="requiredAttr" type="xsd:string" use="required"/>
<xsd:attribute name="optionalAttr" type="xsd:string"/>
</xsd:complexType>Generates:
export class Example {
@XmlAttribute("requiredAttr")
requiredAttr!: String; // Non-optional (use="required")
@XmlAttribute("optionalAttr")
optionalAttr?: String; // Optional (default)
@XmlElement("required", { type: String, namespace: "..." })
required!: String; // Non-optional (minOccurs=1 by default)
@XmlElement("optional", { type: String, namespace: "..." })
optional?: String; // Optional (minOccurs=0)
}The definite assignment assertion (!) tells TypeScript that the property will be initialized by the unmarshalling framework, avoiding strictPropertyInitialization errors.
The library automatically handles type conversions between XML and TypeScript:
| XSD Type | TypeScript Type |
|---|---|
| xsd:string | String |
| xsd:int, xsd:integer | Number |
| xsd:float, xsd:double, xsd:decimal | Number |
| xsd:boolean | Boolean |
| xsd:date, xsd:dateTime | Date |
The library supports XML enumerations through XSD simpleType restrictions. When generating TypeScript from XSD, enum types are automatically created and used in the generated classes.
When you have an XSD with enumeration restrictions:
<xsd:simpleType name="ColorType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="red"/>
<xsd:enumeration value="green"/>
<xsd:enumeration value="blue"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Product">
<xsd:sequence>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="color" type="ColorType"/>
</xsd:sequence>
</xsd:complexType>The generator will create:
// ColorType.ts
export enum ColorType {
red = "red",
green = "green",
blue = "blue",
}
// Product.ts
import { ColorType } from "./ColorType";
@XmlRoot("Product")
export class Product {
@XmlElement("name", { type: String })
name!: String;
@XmlElement("color", { type: ColorType })
color!: ColorType;
}You can also use enums manually in your code:
enum StatusEnum {
pending = "pending",
approved = "approved",
rejected = "rejected",
}
@XmlRoot("Task")
class Task {
@XmlElement("status", { type: String })
status?: StatusEnum;
}
const task = new Task();
task.status = StatusEnum.approved;
const xml = marshal(task); // <Task><status>approved</status></Task>
const unmarshalled = unmarshal(Task, xml);
console.log(unmarshalled.status); // "approved"- Named enums: Defined as top-level
xsd:simpleTypewith restrictions - Inline enums: Anonymous enums defined within elements
- Enum arrays: Support for
maxOccurs="unbounded"with enum types - Special characters: Enum values with special characters are handled (keys are sanitized, values preserved)
- Marshalling/Unmarshalling: Enum values are properly serialized to and from XML strings
GPL-3.0-only
Contributions are welcome! Please feel free to submit a Pull Request.