Skip to main content

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, {} — never null and 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.