XML to JSON conversion (rl-xml-json-v1)
An xmlTransform step reads an XML document and converts it into typed JSON
every later step in the ruleset can address. The conversion follows one
fixed, deterministic convention — the same document always produces the same
JSON, and the JSON's shape is a function of the authored ruleset rather than
of whichever system happened to send the document. This page states every
rule the conversion follows.
Namespaces: the ruleset's own prefix wins
An xmlTransform step declares a namespace prefix map — which prefix the
ruleset wants each namespace URI's elements and attributes to emit under:
{
"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2": "cbc",
"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2": "cac"
}
An element in that namespace emits a key of the declared prefix and the
element's local name — cbc:ID — using the RULESET's prefix, regardless of
what prefix the incoming document actually used for that namespace. Two
documents that declare the same namespace under different prefixes (one
calls it cbc, another calls it invoice) still convert to identical JSON,
because the JSON key comes from the ruleset's own map, never from the
document.
An element with no namespace at all emits its bare local name, with no prefix.
A namespace URI the map does not cover is refused when the step actually
runs, naming the URI — never silently dropped and never given an invented
prefix. Two different namespaces that happen to share a local name (for
example, two different ID elements) always convert to two distinct keys,
because each carries its own declared prefix — they can never collide.
Every element child is an array
Regardless of how many times an element occurs in a given document, its
converted value is always an array. An element that occurs once still
converts to a one-element array. This is the source of the [0] you will
see in every expression that reads an XML input:
input['cac:Line'][0]['cbc:ID'][0]
The cost is that single-valued read — but the benefit is that a future revision of the pinned schema, which might change how many times an element is allowed to occur, can never silently change the shape of a field a ruleset already reads.
Attributes
An attribute emits as a key of a declared attribute prefix (commonly
@) followed by the attribute's own name — @currencyID — placed in the
same object as the element's own child keys. A namespaced attribute uses the
attribute prefix, then the declared namespace prefix, then the attribute's
local name: @cbc:schemeID.
A namespace declaration itself (xmlns="...", xmlns:cbc="...") is never
emitted as business data — it configures how the document's own namespaces
map to URIs and carries no information a ruleset would read.
What an element converts to
Every element converts according to one of four shapes, decided by the step's pinned schema — not by inspecting what a particular document happens to contain:
- A simple-type element is its scalar value directly — not an object,
not an array element with attributes attached.
<total>42</total>becomes"42". - A complex element is an object of its attributes and its children, each child grouped under its own always-array key.
- An empty element is an empty object,
{}— nevernulland never an empty string. It may still carry attributes, which appear as keys in that otherwise-empty object. - An element whose pinned schema allows it to hold both text and child elements at once is refused, naming the element, rather than converted with some part of it silently discarded. Nothing about this convention aggregates interleaved text and elements under one key — it is refused outright, and a future, separately versioned conversion convention may address it without changing rulesets already saved under this one.
Simple values are text
Every simple XML value has type string, including values whose XSD declares
date, decimal or boolean. For example, 2026-10-09, 42.50 and true
arrive as text. Attributes are also strings. The checker uses the same types
as the runtime conversion.
Parse text explicitly where a rule needs a number or date:
PARSENUMBER(steps["process/ubl"]["cbc:Amount"][0], 'plain')
PARSEDATE(steps["process/ubl"]["cbc:IssueDate"][0], 'yyyy-MM-dd')
steps["process/ubl"]["cbc:Flag"][0] == 'true'
Choose the number format and date pattern for the document's lexical form.
Passing raw text to a numeric, date or boolean argument produces
TYPE_STATIC_MISMATCH; explicit parsing retains the existing parse failures.
For saved rules that relied on XSD scalar promotion in the checker, add the
conversion at the use site. XML conversion itself always supplies text.
CDATA and whitespace
A CDATA section unwraps to its plain text — it reads exactly the same as
the equivalent text written with the ordinary XML escaping. Whitespace
interior to an element's own text is preserved exactly; only the leading and
trailing whitespace on a simple-type element's text is trimmed.
Optional source text and attributes
Add "sourceOutput": "xmlSource" to an xmlTransform when a rule needs a
simple element's attributes or text before boundary trimming. It adds one
field beside the converted root fields and violationsOutput:
steps['parse']['xmlSource'].children['p:Qty'][0].text
steps['parse']['xmlSource'].children['p:Qty'][0].attributes['unitCode']
steps['parse']['xmlSource'].children['p:Qty'][0].attributes['a:schemeID']
The source value is the root node itself. Each node has exact attributes
and children dictionaries. Child groups are always arrays in document
order. Simple nodes also have text, including an empty string when there
is no character content. Complex and empty nodes have no text field;
complex formatting whitespace is ignored. Missing attributes and children
are absent keys and follow the usual missing-read behavior.
Source text and attributes are always STRING, including numeric/date XSD
types. Text concatenates the parser's text/CDATA runs with leading,
trailing and internal whitespace retained. This preserves parsed character
content, not XML bytes, entity spelling or CDATA delimiters. Use explicit
PARSENUMBER/PARSEDATE when needed. Source attribute keys have no @ prefix;
namespace keys still use the authored URI-to-prefix map. Namespace
declarations are excluded, and every other attribute namespace, including
xml:space, must be declared in that map.
sourceOutput must be nonempty and differ from violationsOutput and the
converted root's field names. Analysis refuses unknown source fields and
name collisions. On XSD violations the source field is absent. The source
view uses the same validated parser result, with ordinary clock/memory
limits and output accounting. Omit it to retain the existing conversion
contract, including trimmed simple text and discarded simple attributes.
Each transform is analyzed against its own pinned schema and namespace map, including transforms in an untaken branch or a foreach body. Two branch arms may reuse a child step ID and select different schemas or prefixes. A missing artifact in either arm blocks activation; the other arm's schema cannot supply its types.
Hosts calling analyzeRuleset can retain the optional singleton
PinnedXmlSchemaMap, or supply XmlSchemaAnalysisInput with a
byOccurrenceKey dictionary. Each PinnedXmlSchemaForOccurrence entry includes
xsd, schema, and namespaces. Keys follow the published step walker:
parse, route/cases[0]/parse, route/otherwise/parse, and loop/body/parse.
An entry must match that occurrence's ref, version and URI-to-prefix pairs.
The server derives and reuses maps by this complete identity, independently
of invoice syntax or profile values.
Validation: the schema, and the second artifact
An xmlTransform step's xsd field declares the pinned schema its input is
validated against — every structural violation is reported as branchable
data on the step's own declared violations field, never as a run failure. A
document that violates the schema still lets the run succeed when the
ruleset routes on those violations; only a parse failure or a security
refusal (an unresolvable entity, an out-of-bundle schema reference) fails
the step outright.
The step's schema also accepts an optional second validation artifact,
schematron, naming a Schematron ruleset alongside the XSD. A step
declaring schematron is refused when the ruleset is saved — the
declaration does not save with a warning, and it never activates with the
artifact silently unenforced. Business rules are expressed as condition
(check) steps in the ruleset or template; see check steps.
What this means for an author
Every rule on this page is pinned by a committed test fixture — nothing here is aspirational or subject to silent change. If a document's shape does not match what you expect from this page, the step's declared namespace prefix map or its pinned schema version is the place to look first.