Skip to main content

XML

Maps work on JSON, so XML is converted at the edges: read into JSON before the map runs, and written back out after. The map, the editor and the runtimes don't change, and the same map can take JSON one day and XML the next.

Here an order is read from XML, mapped, and written as an invoice. Both schemas come from XSDs. Change the map and the XML underneath follows:

main.js
import { createTransform, jsonToXml, xmlToJson, xsdSchemas } from "@crosswalkjs/core";
import { createMapper } from "@crosswalkjs/canvas";

const text = (path) => fetch(path).then((response) => response.text());
const output = document.getElementById("output");

Promise.all([text("/api/order.xsd"), text("/api/invoice.xsd"), text("/api/order.xml")]).then(([orderXsd, invoiceXsd, orderXml]) => {
const source = xsdSchemas(orderXsd)[0].schema;
const target = xsdSchemas(invoiceXsd)[0].schema;
// The order, read into the JSON a map takes: number and Qty come out as numbers, Line as an array.
const input = xmlToJson(orderXml, source);

// XML in, through the map, XML out: run every time the map changes.
const run = (map) => {
try {
output.textContent = jsonToXml(createTransform(map)(input), target);
} catch (error) {
output.textContent = error.message;
}
};

const map = {
version: 1,
source,
target,
nodes: [],
links: [
{ from: { source: "number" }, to: { target: "ref" } },
{ from: { source: "Customer" }, to: { target: "BillTo" } },
{ from: { source: "Line[].Sku" }, to: { target: "Item[].Code" } },
],
loops: [{ source: "Line[]", target: "Item[]" }],
};
// Try linking Qty to Units, or summing Price into Total.
createMapper(document.getElementById("mapper"), { document: map, toolboxCollapsed: true, onChange: run });
run(map);
});
index.html
<div id="mapper" style="height: 360px"></div>
<pre id="output"></pre>
api/order.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:example:orders" elementFormDefault="qualified">
<xs:element name="Order">
<xs:complexType>
<xs:sequence>
<xs:element name="Customer" type="xs:string"/>
<xs:element name="Line" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Sku" type="xs:string"/>
<xs:element name="Qty" type="xs:int"/>
<xs:element name="Price" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="number" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
api/invoice.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:example:invoices" elementFormDefault="qualified">
<xs:element name="Invoice">
<xs:complexType>
<xs:sequence>
<xs:element name="BillTo" type="xs:string"/>
<xs:element name="Item" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Code" type="xs:string"/>
<xs:element name="Units" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Total" type="xs:decimal"/>
</xs:sequence>
<xs:attribute name="ref" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:schema>
api/order.xml
<?xml version="1.0" encoding="UTF-8"?>
<Order xmlns="urn:example:orders" number="SO-1042">
<Customer>Ana Silva</Customer>
<Line><Sku>TEA-12</Sku><Qty>2</Qty><Price>4.50</Price></Line>
<Line><Sku>MUG-01</Sku><Qty>1</Qty><Price>8.00</Price></Line>
</Order>

Reading and writing​

xmlToJson reads a document by the map's source schema, and jsonToXml writes the output by its target schema:

import { createTransform, jsonToXml, xmlToJson } from "@crosswalkjs/core";

const transform = createTransform(map);
const invoiceXml = jsonToXml(transform(xmlToJson(orderXml, map.source)), map.target);

The schema decides how each field sits in XML, using the xml keyword OpenAPI defines:

In XMLAs JSONIn the schema
The root elementThe root recordxml: { name, namespace } on the root, for writing it back
A child elementA field, by its name without a prefixNothing extra
An element that repeatsAn arraytype: "array", with the element's own schema as items
Repeated elements inside one of their own, such as <Items><Item/>…</Items>An arrayxml: { wrapped: true }, and xml: { name: "Item" } on the items
An attributeA fieldxml: { attribute: true }
Text beside attributes, such as <Price currency="GBP">4.50</Price>A #text fieldxml: { nodeType: "text" }
A name that isn't a field's, such as order-idA field of another namexml: { name: "order-id" }
xsi:nil="true"nullNothing extra

Values take the schema's type, since everything in XML is text: "4.50" is 4.5 for a number, and "1" is true for a boolean. A value that doesn't read as its type stays text. Without a schema, every field is text and an element that repeats is an array.

Writing follows the target schema's order, attributes first, and leaves out fields that are missing or null. jsonToXml takes { indent, declaration, root } to change the layout.

A DOCTYPE is skipped and never expanded, so a document from anywhere can't make the reader fetch files or blow up in memory. A document that relies on its own entities is refused.

Schemas from XSD​

xsdSchemas gives a schema for each element an XSD declares at the top level, with the xml hints filled in:

import { xsdSchemas } from "@crosswalkjs/core";

const order = xsdSchemas(orderXsd).find((s) => s.name === "Order")!.schema;

It reads sequences, choices (whose elements are all optional), all, attributes and attribute groups, element groups, extensions of other types, simple content, minOccurs and maxOccurs, enumerations and the built-in types: xs:int is an integer, xs:decimal a number, xs:dateTime text with a date-time format. Named complex types become $defs, so recursive types work. Other XSD files it includes or imports aren't followed yet.

With only a sample document, inferXmlSchema makes a schema from it: every attribute and element it sees, with elements that repeat as arrays. Every value is text, since XML doesn't say otherwise.

The editor's schema form takes an XML sample or an XSD too.

On .NET​

XmlJson.Read and XmlJson.Write in Crosswalk.Core read and write exactly as the JavaScript functions do, by the same schema, so a map authored here can take and produce XML on a .NET server. See .NET.