Skip to content

Parsing, validating and serializing

The pipeline has four steps, each in its own package: parse() turns XML into the model, validate() checks a document against a ruleset, createInvoice() builds one, and serialize() writes XML. Only validate() checks rules.

Parse

ts
import { parse } from '@facturometro/einvoice';

import { xml } from '../first-invoice/create.ts';

const { document, diagnostics, ok } = parse(xml);

const kind = document.kind; // 'Invoice'
const payable = document.legalMonetaryTotal?.payableAmount?.value; // '3500.00': text, as written

parse() detects whether the document is an invoice or a credit note; parseInvoice() and parseCreditNote() narrow the result. The document is always returned, however broken, so one parse and one validation report every problem at once. Values stay text, exactly as written: the parser never trims or converts them.

diagnostics lists what is wrong with the XML's structure, under the EN 16931 syntax rule ids (UBL-CR-* for elements the Peppol subset does not use, UBL-SR-* for elements that occur too often, UBL-DT-* for attributes that are not allowed) or, where no rule covers it, under this library's EINV-XML-* codes. ok is false when one of them is fatal.

Three errors are thrown instead, because they are about the call rather than the document:

ErrorWhen
XmlSyntaxErrorThe text is not well-formed XML.
UnsupportedDocumentErrorThe root element is neither a UBL Invoice nor a UBL CreditNote.
DocumentKindErrorparseInvoice() got a credit note, or parseCreditNote() an invoice.

Validate

ts
import type { Invoice } from '@facturometro/einvoice';

import { assertValid, parseInvoice, validate } from '@facturometro/einvoice';

import { xml } from '../first-invoice/create.ts';
import { document } from './parse.ts';

const result = validate(document);

if (result.ok) {
  // result.document is a canonical Invoice | CreditNote from here on
  console.log(result.document.legalMonetaryTotal.payableAmount.value);
} else {
  for (const issue of result.fatal) {
    console.error(`${issue.id} at ${issue.xpath}: ${issue.message}`);
  }
}

validate() takes a parsed or a built document and returns every issue: issues in evaluation order, split into fatal and warnings. ok is true when nothing is fatal, and only then does result.document hold the document, typed as the canonical Invoice or CreditNote.

Rule failures are never thrown. A rule that crashes becomes one fatal EINV-RULE-ERROR issue, so a run always completes. Without a ruleset option, the rules come from the customization id; see which rules run by default.

A decimal, boolean or date the UBL schema would reject (12,50, TRUE, 2017-13-01) is reported once, as a fatal EINV-XSD-LEXICAL issue, and the rules that would read the value are skipped for it. That mirrors the official pipeline, which validates against the XSD before it runs the rules.

To fail fast instead, assertValid() returns the canonical document or throws a ValidationError whose issues list what was found:

ts
function readInvoice(text: string): Invoice {
  return assertValid(parseInvoice(text).document); // throws a ValidationError when not valid
}

Parse and validate in one call

ts
import { validateXml } from '@facturometro/einvoice';

import { xml } from '../first-invoice/create.ts';

const result = validateXml(xml);

const valid = result.ok; // no fatal issue from the parser or from a rule
const report = result.issues.map((issue) => `${issue.severity} ${issue.id} at ${issue.xpath}`);
const parserDiagnostics = result.diagnostics; // what the parser reported, unfiltered

validateXml() from @facturometro/einvoice parses the XML and validates the document against the same composition validate() would pick. The parser's diagnostics come first in issues and count towards ok, so a document with a fatal structural problem is not valid even when every rule passes. Your ruleset's disable and override of structural ids apply to them; diagnostics keeps the parser's report unfiltered.

Serialize

ts
import { serialize } from '@facturometro/einvoice';

import { invoice } from '../first-invoice/create.ts';

const pretty = serialize(invoice); // 2-space indent, with the XML declaration
const tabs = serialize(invoice, { indent: '\t' });
const compact = serialize(invoice, { indent: false, declaration: false }); // one line

serialize() accepts a canonical or a parsed document and writes the elements in schema order. It applies no defaults and computes nothing: pass the document through createInvoice() or createCreditNote() for that. A value that is not a string where the schema expects text, such as a number, throws a TypeError naming the element's XPath.

MIT licensed. Specification artefacts belong to OpenPEPPOL, CEN and OASIS.