Skip to main content

The map document

A map is a plain JSON document. The editor reads and writes it, the engine runs it, and you can write one by hand.

{
"version": 1,
"source": { "type": "object", "properties": { "...": {} } },
"target": { "type": "object", "properties": { "...": {} } },
"nodes": [
{ "id": "fullName", "operator": "text.concat", "constants": { "1": " " }, "position": { "x": 300, "y": 60 } }
],
"links": [
{ "from": { "source": "FirstName" }, "to": { "node": "fullName", "input": 0 } },
{ "from": { "source": "LastName" }, "to": { "node": "fullName", "input": 2 } },
{ "from": { "node": "fullName" }, "to": { "target": "FullName" } }
],
"loops": [{ "source": "PhoneCalls[]", "target": "Calls[]" }]
}
FieldWhat it holds
versionAlways 1 for now. A future format will change it.
source, targetA JSON Schema for each side. The root of each must be an object.
nodesThe operators on the grid.
linksEvery line: from a source field or a node, to a target field or a node's input.
loopsWhich source array each target array repeats over, and how. Optional.
submapsSmaller maps, by name, for mapping one record to another. Optional.
notesNotes for people reading the map. Runtimes ignore them. Optional.
pagesPages the editor splits a busy map's operators across. Runtimes ignore them. Optional.

Schemas​

source and target are JSON Schemas: the fields there are to map, which repeat, and which are required. Crosswalk reads the parts that describe shape, including $ref, allOf, oneOf and anyOf, and can take them from an OpenAPI document, schemas split across files, or a sample of the data. Schemas has the detail.

required on the target says which fields a record must have. The editor marks them with *, and a required field with nothing mapped to it is a warning.

Paths​

Fields are addressed by path. Object keys are joined with ., and a field that repeats is written with []:

PathMeans
FirstNameThe top-level FirstName.
Billing.TotalTotal inside the Billing object.
PhoneCalls[]One item of the PhoneCalls array.
PhoneCalls[].CostThe Cost of each item in PhoneCalls.
Tags[]Each value in an array of plain values.

An array is always written with [], so PhoneCalls.Cost isn't a valid path.

A key that contains ., [ or ] has a \ before each of them, so the key unit.price is written unit\.price and tags[] is tags\[\]. A [] after it still means its array: order\.lines[].unit\.price. In JSON the backslash itself is escaped, so a link reads { "source": "unit\\.price" }. The editor writes these for you.

Nodes​

{ "id": "total", "operator": "aggregate.sum", "scope": "PhoneCalls[]", "position": { "x": 380, "y": 250 } }
FieldWhat it holds
idUnique within the map. Links refer to the node by it.
operatorThe id of an operator in the registry, such as text.concat. See Built-in operators.
constantsFixed values for inputs that aren't linked, keyed by input index.
scopeFor an aggregate operator: the repeating record it aggregates over.
positionWhere the editor draws the node. The engine ignores it.
pageThe id of the page the editor draws the node on. The engine ignores it.

A link goes from a source field or a node's output, to a target field or one input of a node:

{ "from": { "source": "PhoneCalls[].Cost" }, "to": { "node": "total", "input": 0 } }
{ "from": { "node": "total" }, "to": { "target": "Billing.Total" } }

Each target field and each node input takes at most one link. A node's inputs are numbered from 0 with no gaps: every input up to the last one used needs either a link or a constant.

A link straight from a source field to a target field can have a page, the id of the page the editor draws it on. A link to or from a node is drawn on the node's page.

A constant can be any JSON value. Look up takes an object as its table:

{ "id": "status", "operator": "convert.lookup", "constants": { "1": { "P": "Paid", "S": "Shipped" }, "2": "Unknown" } }

A link can also join a source record to a target record, to fill the target record whole:

{ "from": { "source": "Billing" }, "to": { "target": "BillTo" } }
{ "from": { "source": "Shipping" }, "to": { "target": "ShipTo" }, "submap": "Address" }

Without submap it copies the source record as it is. With one, it maps the record with that sub-map. A record link between a loop's source and target, such as Lines[] to Products[], maps each item. Nothing else may write inside a record that a record link fills.

Loops​

A loop makes a target array repeat once per item of a source array:

{ "source": "PhoneCalls[]", "target": "Calls[]" }

Inside the loop, PhoneCalls[].Number means the current call's number. Loops nest: a loop over Orders[].Lines[] sits inside one over Orders[]. How a map runs explains repetition in full.

A loop can also sort, group and leave out repeats:

{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [{ "path": "Lines[].Date", "descending": true }],
"groupBy": "Lines[].Sku",
"distinct": true
}
FieldWhat it does
sortByOrders the source items by these fields of the item, first key first. descending reverses one key.
groupByMakes one target item per distinct value of this field, rather than one per source item.
distinctLeaves out a target item that's the same as an earlier one.
pageThe id of the page the editor draws the loop on. The engine ignores it.

Sorting, grouping and repeats covers what each does in detail.

Sub-maps​

A sub-map maps one record to another, and has the same parts as a map: source and target schemas for the two records, then nodes, links, loops, notes and pages. Record links use it by name, so one sub-map can serve every place the same kind of record turns up:

"submaps": {
"Address": {
"source": { "type": "object", "properties": { "Street": { "type": "string" }, "City": { "type": "string" } } },
"target": { "type": "object", "properties": { "Line1": { "type": "string" }, "Town": { "type": "string" } } },
"nodes": [],
"links": [
{ "from": { "source": "Street" }, "to": { "target": "Line1" } },
{ "from": { "source": "City" }, "to": { "target": "Town" } }
]
}
}

Paths inside a sub-map start from its own records, so Street rather than Billing.Street. A sub-map can use other sub-maps, from the same submaps list, but not itself. An optional title names it for people.

Notes​

{ "id": "note1", "text": "Check the rounding with finance.", "position": { "x": 260, "y": 200 } }
{ "id": "note2", "text": "Units add up across the SKU's lines.", "position": { "x": 24, "y": 22 }, "on": { "node": "units" } }

Notes sit on the editor's grid, in a map or a sub-map, to explain it to the next person. Runtimes ignore them.

A note can be pinned to what it's about with on, so it stays beside it when things move. Its position is then measured from that thing rather than from the grid's corner.

onPins the note to
{ "node": "units" }An operator.
{ "node": "fullName", "input": 2 }One of an operator's inputs, and the line into it.
{ "target": "Products[].Units" }A target field, and the line or loop into it.
{ "source": "Lines[].Qty" }A source field.

A note's page says which page it's on, like an operator's. A note pinned to an operator is on that operator's page, whatever its own page says.

Pages​

"pages": [
{ "id": "page1", "name": "Header" },
{ "id": "page2", "name": "Lines" }
]

A busy map can be split across pages, each a tab along the bottom of the editor's grid. Each operator, note, loop and link straight across names its page with page; one with no page, or a page that isn't listed, is on the first. A link to or from an operator is on the operator's page. The source and target trees are on every page.

Pages only change what the editor draws together. The map runs the same however its operators are split, so a link can join operators on different pages. A sub-map has pages of its own.