XML postprocessor

Transform extracted data into a custom XML output schema

Define your own XML output with a JsonLogic-based postprocessor. For example, use a postprocessor if your app or API consumes data using an XML schema, and you don't want to integrate using Sensible's output schema.

In detail, Sensible's parsed_document API output schema represents extracted document data as typed fields:

{
  "parsed_document": {
    "contract_date": {
      "value": "2023-01-01T00:00:00.000Z",
      "type": "date"
    },
    "customer_name": {
      "type": "string",
      "value": "John Smith"
    }
  }
}

Using a postprocessor, you can transform the extracted data into an XML output, for example:

{
  "postprocessorOutput": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<invoice>\n  <contract_date type=\"date\">2023-01-01T00:00:00.000Z</contract_date>\n  <customer_name type=\"string\">John Smith</customer_name>\n</invoice>"
}

The following rule produces that output:

/* Sensible uses JSON5 to support in-line comments*/
{
  "eachKey": { /* builds an element object; its properties define the XML output */
    "tag": "invoice", /* XML element name */
    "content": [ /* array — produces a sequence of child elements */
      {
        "eachKey": {
          "tag": "contract_date", /* XML element name */
          "attrs": { /* key-value pairs that become XML attributes, e.g. type="date" */
            "eachKey": { "type": { "var": "contract_date.type" } } /* "var" returns "date" from parsed_document */
          },
          "content": { "var": "contract_date.value" } /* extracted value, e.g. "2023-01-01T00:00:00.000Z", becomes text content */
        }
      },
      {
        "eachKey": {
          "tag": "customer_name", /* XML element name */
          "attrs": { /* key-value pairs that become XML attributes, e.g. type="string" */
            "eachKey": { "type": { "var": "customer_name.type" } } /* "var" returns "string" from parsed_document */
          },
          "content": { "var": "customer_name.value" } /* extracted value, e.g. "John Smith", becomes text content */
        }
      }
    ]
  }
}

Find postprocessor output in the postprocessorOutput string value in the API response and in the Postprocessed tab in the SenseML editor:

Click to enlarge

Postprocessor output isn't available in Excel output.

Parameters

keyvaluedescription
type (required)xmlTransform extracted data into an XML output.
rule (required)JsonLogic objectDefine the XML output using a JsonLogic rule. See Defining XML output for the rule syntax.
declarationboolean. default: falseIf true, Sensible prepends an XML declaration (<?xml version="1.0" encoding="UTF-8"?>) to the output.
selfCloseEmptyTagsboolean. default: trueIf true, Sensible renders elements with no content as self-closing tags (for example, <tag/>). If false, Sensible renders them as an open-and-close tag pair (for example, <tag></tag>). Null fields appear as empty elements, for example, <null_field/> when true or <null_field></null_field> when false.
keepParsedDocumentboolean. default: trueIf false, Sensible suppresses the parsed_document object in the output and disables Excel output and human review. Set to false to reduce the size of large output when you only need the postprocessor output.

Defining XML output

In the rule parameter, define your XML output using JsonLogic. The rule must evaluate to an element object. A minimal rule looks like this:

/* Sensible uses JSON5 to support in-line comments*/
{
  "eachKey": { /* builds an element object; its properties define the XML output */
    "tag": "invoice", /* XML element name */
    "attrs": { /* key-value pairs that become XML attributes */
      "eachKey": {
        "currency": "USD" /* hardcoded attribute value */
      }
    },
    "content": [ /* array — produces a sequence of child elements */
      {
        "eachKey": {
          "tag": "total", /* XML element name */
          "content": { "var": "total.value" } /* extracted value becomes text content */
        }
      }
    ]
  }
}

If you extract $4,500 from an invoice for the total field, then the output looks like this:

{
  "postprocessorOutput": "<invoice currency=\"USD\">\n  <total>4500</total>\n</invoice>"
}

The element object has the following properties:

propertyvaluedescription
tag (required)stringThe XML element name.
attrsobjectKey-value pairs that become XML attributes on the element. Use the Each Key operation to build the object. Values must be scalars (string, number, boolean, or null).
contentscalar, element object, or array of eitherThe element's content. A scalar value (string, number, boolean, or null) becomes text content. An element object becomes a nested child element. An array produces a sequence of child elements.

Sensible automatically escapes reserved XML characters (<, >, &, ", ') in text content and attribute values. For example, "content": "1 < 2 & 3 > 0" renders as 1 &lt; 2 &amp; 3 &gt; 0.

Dynamic field mapping

To generate one XML element per extracted field without naming each field individually in the rule, use the Map Object operation with {"var": ""} to iterate over the entire parsed_document. Because the rule must evaluate to an element object, nest Map Object inside the content property of a root element:

/* Sensible uses JSON5 to support in-line comments*/
{
  "fields": [
    /*
      In practice, you extract load_id and rate from a document.
      This example uses constant fields to input hardcoded values
      so you can run it in the SenseML editor without a document.
    */
    {
      "id": "load_id",
      "type": "number",
      "method": { "id": "constant", "value": "328298459" }
    },
    {
      "id": "rate",
      "type": "number",
      "method": { "id": "constant", "value": "4500" }
    }
  ],
  "postprocessor": {
    "type": "xml",
    "rule": {
      "eachKey": {
        "tag": "document", /* root element wrapping all fields */
        "content": {
          "mapObject": [ /* iterates over each field in parsed_document and operates on its key and value */
            { "var": "" }, /* current context: the entire parsed_document */
            {
              "eachKey": { /* builds an element object for each field */
                "tag": { "var": "key" }, /* current field's ID becomes the XML element name */
                "content": { "var": "value.value" } /* current field's extracted value becomes text content */
              }
            }
          ]
        }
      }
    }
  }
}

This produces:

{
  "postprocessorOutput": "<document><load_id>328298459</load_id><rate>4500</rate></document>"
}

Any field you add to the config automatically appears in the XML output without updating the postprocessor rule. See JsonLogic for Sensible's full operator reference.

Examples

Example 1

Config

/* Sensible uses JSON5 to support in-line comments*/
{
  "fields": [
    {
      "id": "load_id", /* user-friendly ID for extracted target data */
      "type": "number", /* Sensible formats extracted data as this data type, or returns null if it doesn't recognize extracted data as the specified type */
      "method": { "id": "passthrough" },
      "anchor": { /* an anchor is text that always occurs in the same position relative to your target data. */
        "match": [ /* array of Match objects. Sensible matches the last element
                      if each element matches a successive line in the document */
          {
            "text": "confirmation - #", /* string to match */
            "type": "includes" /* match anywhere in line. */
          }
        ]
      }
    },
    {
      "id": "_trailer_type_raw", /* user-friendly ID for extracted target data */
      "method": {
        "id": "row", /* target data to extract is distributed on same horizontal line as anchor */
        "position": "right", /* default: right. target data is to left or right of anchor. enums: left | right. */
        "tiebreaker": "first"
      },
      "anchor": { /* an anchor is text that always occurs in the same position relative to your target data. */
        "match": [ /* array of Match objects. Sensible matches the last element
                      if each element matches a successive line in the document */
          {
            "text": "Equipment:", /* string to match */
            "type": "startsWith" /* line must start with the match */
          }
        ]
      }
    },
    {
      "id": "trailer_type", /* user-friendly ID for extracted target data */
      "method": {
        "id": "split",
        "separator": " - ",
        "source_id": "_trailer_type_raw",
        "index": 0
      }
    },
    {
      "id": "hide_fields", /* user-friendly ID for extracted target data */
      "method": { "id": "suppressOutput", "source_ids": ["_trailer_type_raw"] }
    },
    {
      "id": "test_field_xml_escapes_&_<stuff>", /* user-friendly ID for extracted target data */
      "method": {
        "id": "constant",
        "value": "blah 'blah' \"blah\" & <blah></blah>"
      }
    },
    {
      "id": "test_field_null", /* user-friendly ID for extracted target data */
      "method": { "id": "constant", "value": "" }
    }
  ],
  "postprocessor": {
    "type": "xml",
    "declaration": true,
    "selfCloseEmptyTags": false,
    "rule": {
      "eachKey": { /* builds an element object; its properties define the XML output */
        "tag": "DOCUMENTS", /* XML element name */
        "content": [ /* array — produces a sequence of child elements */
          {
            "eachKey": {
              "tag": "DOCUMENT", /* XML element name */
              "content": [
                {
                  "eachKey": {
                    "tag": "FORM", /* XML element name */
                    "content": "AU Tax Invoice Precise" /* hardcoded text content */
                  }
                },
                {
                  "eachKey": {
                    "tag": "FIELDS", /* XML element name */
                    "content": {
                      "mapObject": [ /* iterates over each field in parsed_document and operates on its key and value */
                        { "var": "" }, /* current context: the entire parsed_document */
                        {
                          "eachKey": { /* builds an element object for each field */
                            "tag": "FIELD", /* XML element name */
                            "attrs": { /* key-value pairs that become XML attributes, e.g. name="load_id" type="number" */
                              "eachKey": {
                                "name": { "var": "key" }, /* current field's ID becomes the attribute value */
                                "type": { "var": "value.type" } /* current field's data type becomes the attribute value */
                              }
                            },
                            "content": { "var": "value.value" } /* current field's extracted value becomes text content */
                          }
                        }
                      ]
                    }
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

Example document
The following image shows the example document used with this example config:

Click to enlarge
Example documentDownload link

Output

/* POSTPROCESSED OUTPUT */
/* postprocessorOutput is a JSON string; see below for formatted XML */

{
  "postprocessorOutput": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>..."
}
<!-- postprocessorOutput value, formatted for readability -->

<?xml version="1.0" encoding="UTF-8"?>
<DOCUMENTS>
  <DOCUMENT>
    <FORM>AU Tax Invoice Precise</FORM>
    <FIELDS>
      <FIELD name="load_id" type="number">328298459</FIELD>
      <FIELD name="trailer_type" type="string">Van</FIELD>
      <FIELD name="test_field_xml_escapes_&amp;_&lt;stuff&gt;" type="string">blah 'blah' "blah" &amp; &lt;blah&gt;&lt;/blah&gt;</FIELD>
      <FIELD name="test_field_null" type=""></FIELD>
    </FIELDS>
  </DOCUMENT>
</DOCUMENTS>
/* PARSED DOCUMENT OUTPUT */

{
  "load_id": {
    "source": "328298459",
    "value": 328298459,
    "type": "number"
  },
  "trailer_type": {
    "value": "Van",
    "type": "string"
  },
  "test_field_xml_escapes_&_<stuff>": {
    "value": "blah 'blah' \"blah\" & <blah></blah>",
    "type": "string"
  },
  "test_field_null": null
}

Did this page help you?