Skip to content

neumaennl/xmlbind-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

183 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

xmlbind-ts

CI

JAXB-like XML binding for TypeScript.

Overview

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

Installation

npm install @neumaennl/xmlbind-ts

TypeScript Decorator Support

This library supports both legacy decorators (TypeScript's experimental decorators) and Stage 3 decorators (the TC39 standard):

  • Legacy decorators: Used when experimentalDecorators: true is set in your tsconfig.json
  • Stage 3 decorators: Used when experimentalDecorators is 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:

With Legacy Decorators (experimentalDecorators)

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // ... other options
  }
}

With Stage 3 Decorators (no experimentalDecorators)

{
  "compilerOptions": {
    "target": "ES2022",
    // ... other options
    // Note: experimentalDecorators is NOT set
  }
}

Both configurations work seamlessly with all decorators (@XmlRoot, @XmlElement, @XmlAttribute, etc.).

Quick Start

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);

Decorators

@XmlRoot

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 {
  // ...
}

@XmlElement

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;
}

@XmlAttribute

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;
}

@XmlText

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>

@XmlAnyElement

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[];
}

@XmlAnyAttribute

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 };
}

Marshalling and Unmarshalling

marshal

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);

unmarshal

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);

Namespace Prefix Mappings

When unmarshalling an XML document, the prefix-to-namespace-URI mappings declared on the root element are automatically stored on the unmarshalled object as _namespacePrefixes. This lets you inspect which namespace each prefix refers to (e.g. to locate the schema that validates a given sub-tree).

@XmlRoot("Catalog", {
  namespace: "http://example.com/catalog",
  prefixes: { "http://example.com/ext": "ext" },
})
class Catalog {
  _namespacePrefixes?: Record<string, string>;

  @XmlElement("item", { type: String, namespace: "http://example.com/ext" })
  item?: string;
}

const xml = `<Catalog xmlns="http://example.com/catalog" xmlns:ext="http://example.com/ext">
  <ext:item>Widget</ext:item>
</Catalog>`;

const catalog = unmarshal(Catalog, xml);
console.log(catalog._namespacePrefixes);
// { ext: "http://example.com/ext" }

The default namespace (the URI behind xmlns="...") is not included in _namespacePrefixes because it is already accessible via the @XmlRoot decorator's namespace option; only explicitly prefixed declarations appear in the map.

Modifying namespace prefixes before marshalling

You can modify _namespacePrefixes before calling marshal. The modified map is used as the authoritative source for all xmlns:prefix declarations in the output XML.

// Rename the "ext" prefix to "e" before serialising
catalog._namespacePrefixes = { e: "http://example.com/ext" };
const output = marshal(catalog);
// <Catalog xmlns="http://example.com/catalog" xmlns:e="http://example.com/ext">
//   <e:item>Widget</e:item>
// </Catalog>

When _namespacePrefixes is set (even if empty), it is the authoritative source for all xmlns:prefix declarations that were present when the document was last unmarshalled. If you add content that uses a namespace URI not already in the map, the marshaller will still auto-declare a generated prefix (e.g. xmlns:ns1="...") for it. When _namespacePrefixes is undefined (e.g. on a freshly constructed object), the prefixes option from the @XmlRoot decorator is used as the fallback.

Namespace prefixes in generated code

Classes generated from top-level XSD elements automatically include a _namespacePrefixes property declaration:

@XmlRoot("Catalog", { namespace: "http://example.com/catalog" })
export class Catalog {
  _namespacePrefixes?: Record<string, string>;

  // … generated element and attribute fields
}

This makes the property discoverable and type-safe without requiring a cast.

Complex Example

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"]

XSD to TypeScript Generator

The library includes a command-line tool to generate TypeScript classes from XSD schemas.

CLI Usage

xsd2ts input.xsd output-directory

Programmatic Usage

import { 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 decorators

Supported XSD Features

The XSD generator supports a comprehensive set of XML Schema features:

Core 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, and xs:all
  • Content Models: simpleContent, complexContent, mixed content

Advanced Features

  • Groups: xs:group and xs:attributeGroup references with proper expansion
  • Wildcards: xs:any (generates @XmlAnyElement()) and xs: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

Special Handling

  • 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

Property Requiredness in Generated Code

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 on minOccurs

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.

Type Mapping

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

Enum Support

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.

Using Enums with XSD

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;
}

Manual Enum Usage

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"

Features

  • Named enums: Defined as top-level xsd:simpleType with 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

License

GPL-3.0-only

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

something like JAXB from Java, but for Typescript

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors 4

  •  
  •  
  •  
  •