Skip to main content

Schemas

A map's source and target are JSON Schemas: they say what fields there are to map, which repeat, and which are required. The editor draws them as its two trees, and the runtimes build the output in the target's field order.

Real schemas come from all sorts of places, so Crosswalk reads them in a few ways. Each ends up as a plain JSON Schema in the map, so a map always stands alone and runs the same in JavaScript and .NET.

What it reads in a JSON Schema​

Crosswalk reads the parts of JSON Schema that describe shape, and ignores rules about values, such as pattern or minimum.

KeywordRead as
typeobject, array, string, number, integer, boolean or null. A list, such as ["string", "null"], is its first type that isn't null. Left out, it's a record if there are properties, an array if there are items, the type of the values for const or enum, and otherwise a record.
properties, items, requiredThe fields, what an array holds, and which fields a record must have. A required target field with nothing mapped to it is a warning.
title, description, formatKept, for showing.
$refTo the schema's own $defs or definitions: what it points at, with anything beside it added. A schema that refers back to one it's inside, such as a category of categories, is followed once. For other files, see below.
allOfMerged in: the schema's own fields, then each part's in turn, with required everything any part requires. The usual way of saying "an Order is a NewOrder with an id".
oneOf, anyOfOne of several shapes. Crosswalk reads them as one record with every alternative's fields, each required only if every alternative requires it, so you can map whichever the data turns out to be; a field that isn't there is left out. null alternatives are dropped, so anyOf: [X, { type: "null" }] is X.

From an OpenAPI document​

Most APIs publish an OpenAPI document rather than separate schemas. openApiSchemas lists every schema one describes: each operation's JSON request and response bodies, named like POST /orders request or GET /invoices/{id} response 200, then the named component schemas. Each comes with the document's other schemas in $defs, so it stands alone. OpenAPI 3.0 and 3.1 are read, and Swagger 2.0.

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

const schemas = openApiSchemas(api);
const map = {
version: 1,
source: schemas.find((s) => s.name === "POST /orders request")!.schema,
target: schemas.find((s) => s.name === "GET /invoices/{id} response 200")!.schema,
nodes: [],
links: [],
};

Here, pick any two of the API's schemas to map between:

main.js
import { openApiSchemas } from "@crosswalkjs/core";
import { createMapper } from "@crosswalkjs/canvas";

const pickers = { source: document.getElementById("source"), target: document.getElementById("target") };
let mapper;

// Every schema the API describes: request and response bodies, then its components.
fetch("/api/shop.json")
.then((response) => response.json())
.then((api) => {
const schemas = openApiSchemas(api);
for (const picker of Object.values(pickers)) picker.append(...schemas.map((s) => new Option(s.name, s.name)));
pickers.source.value = "POST /orders request";
pickers.target.value = "GET /invoices/{id} response 200";

const show = () => {
const schema = (picker) => schemas.find((s) => s.name === picker.value).schema;
mapper?.destroy();
mapper = createMapper(document.getElementById("mapper"), {
document: { version: 1, source: schema(pickers.source), target: schema(pickers.target), nodes: [], links: [] },
toolboxCollapsed: true,
// The buttons in each tree's heading replace its schema: try pasting a sample.
editSchemas: true,
});
};
pickers.source.onchange = pickers.target.onchange = show;
show();
});
index.html
<style>
.pick { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 8px; font: 13px system-ui, sans-serif; }
</style>
<div class="pick">
<label>Source <select id="source"></select></label>
<label>Target <select id="target"></select></label>
</div>
<div id="mapper" style="height: 380px"></div>
api/shop.json
{
"openapi": "3.1.0",
"info": { "title": "Shop", "version": "1.0" },
"paths": {
"/orders": {
"post": {
"summary": "Place an order",
"requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NewOrder" } } } },
"responses": { "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } } }
}
},
"/invoices/{id}": {
"get": { "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } } } } }
}
},
"components": {
"schemas": {
"NewOrder": {
"type": "object",
"required": ["customer", "lines"],
"properties": {
"customer": { "$ref": "#/components/schemas/Customer" },
"lines": { "type": "array", "items": { "$ref": "#/components/schemas/Line" } },
"payment": {
"oneOf": [
{ "type": "object", "properties": { "method": { "const": "card" }, "last4": { "type": "string" } } },
{ "type": "object", "properties": { "method": { "const": "invoice" }, "poNumber": { "type": "string" } } }
]
},
"note": { "type": ["string", "null"] }
}
},
"Order": { "allOf": [{ "$ref": "#/components/schemas/NewOrder" }, { "type": "object", "properties": { "id": { "type": "string" }, "placedAt": { "type": "string", "format": "date-time" } } }] },
"Customer": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } } },
"Line": { "type": "object", "properties": { "sku": { "type": "string" }, "qty": { "type": "integer" }, "price": { "type": "number" } } },
"Invoice": {
"type": "object",
"properties": {
"number": { "type": "string" },
"billTo": { "type": "string" },
"items": { "type": "array", "items": { "type": "object", "properties": { "code": { "type": "string" }, "quantity": { "type": "integer" }, "amount": { "type": "number" } } } },
"total": { "type": "number" }
}
}
}
}
}

Schemas in several files​

A big schema is often split into files that refer to each other, such as { "$ref": "common/types.json#/$defs/Address" }. bundleSchema follows those references and brings what they point at into $defs, so the result stands alone. You give it a load function that fetches a document by its address, so it works with URLs, files on disk or anything else; each document is loaded once, and references inside it are followed relative to it.

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

const load = (address: string) => fetch(address).then((response) => response.json());
const order = await bundleSchema(await load(url), load, url);

On a server, read files instead: (path) => JSON.parse(readFileSync(path, "utf8")). Bundle once, when the schema comes in, and keep the result in the map.

main.js
import { bundleSchema, inferSchema } from "@crosswalkjs/core";
import { createMapper } from "@crosswalkjs/canvas";

const load = (address) => fetch(address).then((response) => response.json());
const output = document.getElementById("output");

// order.json refers to customer.json and common/types.json, which refer on.
load("/api/schemas/order.json")
.then((order) => bundleSchema(order, load, "/api/schemas/order.json"))
.then((source) => {
output.textContent = `Brought into $defs: ${Object.keys(source.$defs).join(", ")}\n\n${JSON.stringify(source, null, 2)}`;
createMapper(document.getElementById("mapper"), {
document: { version: 1, source, target: inferSchema({ Reference: "", Buyer: "", Town: "" }), nodes: [], links: [] },
toolboxCollapsed: true,
});
});
index.html
<div id="mapper" style="height: 360px"></div>
<pre id="output"></pre>
api/schemas/order.json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"customer": { "$ref": "customer.json" },
"lines": { "type": "array", "items": { "$ref": "common/types.json#/$defs/Line" } },
"total": { "$ref": "common/types.json#/$defs/Money" }
}
}
api/schemas/customer.json
{
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "$ref": "common/types.json#/$defs/Address" }
}
}
api/schemas/common/types.json
{
"$defs": {
"Address": { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" } } },
"Line": { "type": "object", "properties": { "sku": { "type": "string" }, "price": { "$ref": "#/$defs/Money" } } },
"Money": { "type": "number" }
}
}

From XSD​

An XSD gives a schema for each element it declares, ready for reading and writing XML. See XML.

From a CSV sample​

A few lines of a CSV file give a schema too. See Flat files.

From a sample​

With an example of the data and no schema, inferSchema makes one: every field it sees, with arrays' items merged, so fields that only some items have are included.

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

const source = inferSchema(sampleOrder);

In the editor​

With editSchemas: true, each tree's heading has a button to replace its schema: from a sample, a JSON Schema, or one picked from an OpenAPI document, pasted in. It's an edit like any other, so undo puts the old schema back, and links to fields the new schema doesn't have show as issues to fix. It's off unless you turn it on, since many apps own their schemas. The OpenAPI example above has it on: try the buttons beside each tree's magnifier.