NV ToolkitFormat · Validate · Convert

Code Generators

XML to TypeScript Converter

Paste an XML sample, get typed TypeScript models — names, nesting and nullability inferred for you.

In short

XML to TypeScript parses an XML sample into a data model — attributes included, element text under a text field — and infers interfaces from it with quicktype, in your browser. The root element becomes a nested type under Root, repeated elements become Item[], element text such as total (228.99) is typed number, and a Convert class validates payloads against the result.

By NaveenKumar T · Updated

Learn: XML Tutorial: Structure, Syntax & Survival Skills

  • Typed models from one sample
  • Nested objects become types
  • Nullability inferred

Runs entirely in your browser — nothing you paste or open here is uploaded, logged, or stored.How we handle data →

Example

Example: XML order payload to TypeScript

The same order as XML, with data split between attributes (id, status, paid, sku, qty, price) and element text (name, email, total) — the split that decides which TypeScript fields are typed as numbers and which stay strings. The right-hand side is the exact output of this tool.

Input · XML
<order id="ord_9F2KX" status="processing" paid="true">
  <customer>
    <name>Ada Lovelace</name>
    <email>ada@example.com</email>
  </customer>
  <items>
    <item sku="KB-75" qty="1" price="149.99"/>
    <item sku="MS-12" qty="2" price="39.5"/>
  </items>
  <total>228.99</total>
</order>
Generated · TypeScript
// To parse this data:
//
//   import { Convert, Root } from "./file";
//
//   const root = Convert.toRoot(json);
//
// These functions will throw an error if the JSON doesn't
// match the expected interface, even if the JSON is valid.

export interface Root {
    order: Order;
}

export interface Order {
    customer: Customer;
    items:    Items;
    total:    number;
    id:       string;
    status:   string;
    paid:     string;
}

export interface Customer {
    name:  string;
    email: string;
}

export interface Items {
    item: Item[];
}

export interface Item {
    sku:   string;
    qty:   string;
    price: string;
}

// Converts JSON strings to/from your types
// and asserts the results of JSON.parse at runtime
export class Convert {
    public static toRoot(json: string): Root {
        return cast(JSON.parse(json), r("Root"));
    }

    public static rootToJson(value: Root): string {
        return JSON.stringify(uncast(value, r("Root")), null, 2);
    }
}

function invalidValue(typ: any, val: any, key: any, parent: any = ''): never {
    const prettyTyp = prettyTypeName(typ);
    const parentText = parent ? ` on ${parent}` : '';
    const keyText = key ? ` for key "${key}"` : '';
    throw Error(`Invalid value${keyText}${parentText}. Expected ${prettyTyp} but got ${JSON.stringify(val)}`);
}

function prettyTypeName(typ: any): string {
    if (Array.isArray(typ)) {
        if (typ.length === 2 && typ[0] === undefined) {
            return `an optional ${prettyTypeName(typ[1])}`;
        } else {
            return `one of [${typ.map(a => { return prettyTypeName(a); }).join(", ")}]`;
        }
    } else if (typeof typ === "object" && typ.literal !== undefined) {
        return typ.literal;
    } else {
        return typeof typ;
    }
}

function jsonToJSProps(typ: any): any {
    if (typ.jsonToJS === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.json] = { key: p.js, typ: p.typ });
        typ.jsonToJS = map;
    }
    return typ.jsonToJS;
}

function jsToJSONProps(typ: any): any {
    if (typ.jsToJSON === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.js] = { key: p.json, typ: p.typ });
        typ.jsToJSON = map;
    }
    return typ.jsToJSON;
}

function transform(val: any, typ: any, getProps: any, key: any = '', parent: any = ''): any {
    function transformPrimitive(typ: string, val: any): any {
        if (typeof typ === typeof val) return val;
        return invalidValue(typ, val, key, parent);
    }

    function transformUnion(typs: any[], val: any): any {
        // val must validate against one typ in typs
        const l = typs.length;
        for (let i = 0; i < l; i++) {
            const typ = typs[i];
            try {
                return transform(val, typ, getProps);
            } catch (_) {}
        }
        return invalidValue(typs, val, key, parent);
    }

    function transformEnum(cases: string[], val: any): any {
        if (cases.indexOf(val) !== -1) return val;
        return invalidValue(cases.map(a => { return l(a); }), val, key, parent);
    }

    function transformArray(typ: any, val: any): any {
        // val must be an array with no invalid elements
        if (!Array.isArray(val)) return invalidValue(l("array"), val, key, parent);
        return val.map(el => transform(el, typ, getProps));
    }

    function transformDate(val: any): any {
        if (val === null) {
            return null;
        }
        const d = new Date(val);
        if (isNaN(d.valueOf())) {
            return invalidValue(l("Date"), val, key, parent);
        }
        return d;
    }

    function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
        if (val === null || typeof val !== "object" || Array.isArray(val)) {
            return invalidValue(l(ref || "object"), val, key, parent);
        }
        const result: any = {};
        Object.getOwnPropertyNames(props).forEach(key => {
            const prop = props[key];
            const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
            result[prop.key] = transform(v, prop.typ, getProps, key, ref);
        });
        Object.getOwnPropertyNames(val).forEach(key => {
            if (!Object.prototype.hasOwnProperty.call(props, key)) {
                result[key] = transform(val[key], additional, getProps, key, ref);
            }
        });
        return result;
    }

    if (typ === "any") return val;
    if (typ === null) {
        if (val === null) return val;
        return invalidValue(typ, val, key, parent);
    }
    if (typ === false) return invalidValue(typ, val, key, parent);
    let ref: any = undefined;
    while (typeof typ === "object" && typ.ref !== undefined) {
        ref = typ.ref;
        typ = typeMap[typ.ref];
    }
    if (Array.isArray(typ)) return transformEnum(typ, val);
    if (typeof typ === "object") {
        return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val)
            : typ.hasOwnProperty("arrayItems")    ? transformArray(typ.arrayItems, val)
            : typ.hasOwnProperty("props")         ? transformObject(getProps(typ), typ.additional, val)
            : invalidValue(typ, val, key, parent);
    }
    // Numbers can be parsed by Date but shouldn't be.
    if (typ === Date && typeof val !== "number") return transformDate(val);
    return transformPrimitive(typ, val);
}

function cast<T>(val: any, typ: any): T {
    return transform(val, typ, jsonToJSProps);
}

function uncast<T>(val: T, typ: any): any {
    return transform(val, typ, jsToJSONProps);
}

function l(typ: any) {
    return { literal: typ };
}

function a(typ: any) {
    return { arrayItems: typ };
}

function u(...typs: any[]) {
    return { unionMembers: typs };
}

function o(props: any[], additional: any) {
    return { props, additional };
}

function m(additional: any) {
    return { props: [], additional };
}

function r(name: string) {
    return { ref: name };
}

const typeMap: any = {
    "Root": o([
        { json: "order", js: "order", typ: r("Order") },
    ], false),
    "Order": o([
        { json: "customer", js: "customer", typ: r("Customer") },
        { json: "items", js: "items", typ: r("Items") },
        { json: "total", js: "total", typ: 3.14 },
        { json: "id", js: "id", typ: "" },
        { json: "status", js: "status", typ: "" },
        { json: "paid", js: "paid", typ: "" },
    ], false),
    "Customer": o([
        { json: "name", js: "name", typ: "" },
        { json: "email", js: "email", typ: "" },
    ], false),
    "Items": o([
        { json: "item", js: "item", typ: a(r("Item")) },
    ], false),
    "Item": o([
        { json: "sku", js: "sku", typ: "" },
        { json: "qty", js: "qty", typ: "" },
        { json: "price", js: "price", typ: "" },
    ], false),
};

Compare "total" (element text, typed number) with "price" and "qty" (attributes, kept as string). The root <order> element became a nested Order type under Root.

Learn more

From XML sample to TypeScript types

Hand-writing model classes from an XML payload means deciding, field by field, which parser reads which node — this page automates that decision. quicktype first parses your document into a data model, attributes included, then runs the same type inference it uses for JSON against the result.

number is a 64-bit float, so an integer ID above 2^53 rounds silently — and the runtime Convert cast will not catch it, because it checks the type, not the range. Keep 64-bit IDs as strings on the wire, or parse with a reviver that turns them into BigInt, and change the interface field to string or bigint by hand.

How XML elements and attributes become TypeScript types

The XML is parsed into a data model before inference (attributes included, text nodes named "text"), so the root element becomes a nested type: <order> is a field of type Order on the top-level Root rather than the top level itself. Child elements become fields, and repeated elements — the two <item> siblings — are collected into Item[] under an Items wrapper type that mirrors the <items> container.

Attributes and element text are typed differently, and this matters: element text is type-inferred, so <total>228.99</total> becomes number, while attribute values are kept as they were written — qty="1" and price="149.99" stay string in TypeScript. Prefer element text for numeric data you want typed, or cast attribute values after decoding. One more trap: a single <item> would not be recognized as a list, so make sure repeated elements appear at least twice in the sample.

Reading the XML in TypeScript with the generated interfaces

This page parses your XML with fast-xml-parser before inference runs, so the quickest way to make the generated interfaces true in your own code is to use the same parser with the same two options: keep attributes, and give their names no prefix. Stringify the parsed object, hand it to Convert.toRoot, and you get the compile-time types and the runtime check together. Remember the single-element trap: an <items> element with one <item> child parses as an object rather than an array, and Convert will throw on it.

TypeScript · fast-xml-parser
// npm i fast-xml-parser
import { XMLParser } from "fast-xml-parser";
import { Convert, type Root } from "./Root";

// Same options this page uses: attributes kept, names unprefixed.
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "" });
const parsed = parser.parse(xmlText);

const root: Root = Convert.toRoot(JSON.stringify(parsed));
console.log(root.order.items.item[0].sku); // "KB-75"

Help

Frequently asked questions

Everything you need to know about the XML to TypeScript.

01How do I generate TypeScript classes from XML?

Paste an XML document and press Convert. It is first parsed into a data model — attributes included — then typed: the root element becomes a nested type, repeated elements become Item[], and the result downloads as Root.ts.

02How do I get the same object shape from XML at runtime?

Use fast-xml-parser with the two options this page uses — ignoreAttributes: false and attributeNamePrefix: "" — then JSON.stringify the result and pass it to Convert.toRoot, which validates it against the generated interfaces.

03Why does Convert.toRoot throw on a document with a single item?

fast-xml-parser returns one child as an object, not a one-element array, so items.item no longer matches Item[] and the runtime cast rejects it. Pass isArray: (name) => name === "item" to the parser, or ensure the element is always repeated.

04Does Convert.toRoot catch every mismatch between the sample and a live XML document?

Only shape and type mismatches, not the array trap: a document whose only <item> collapses to an object instead of a one-element array is a shape TypeScript can validate, but fast-xml-parser produces that shape happily, so Convert.toRoot throws only after hitting the resulting object where Item[] was expected. Pass an isArray callback to the parser to prevent the case rather than catch it downstream.

Keep working

Related tools

Learn the format

Related guides

All guides →