Skip to main content

Generation Process Overview

The ubl-builder library converts your TypeScript objects into standards-compliant UBL XML through a multi-stage process:

The parseToJson Method

Every component implements parseToJson() to convert itself into an intermediate JSON structure that xmlbuilder can process.

Basic Types

UDT types serialize their content and attributes:
// From CctAmount.ts:23
parseToJson() {
  const jsonResult: any = { '#text': this.content };
  Object.keys(this.attributes)
    .filter((att) => this.attributes[att])
    .forEach((attribute: string) => {
      jsonResult[`@${attribute}`] = this.attributes[attribute];
    });
  return jsonResult;
}
Example:
const amount = new UdtAmount('1000.00', { currencyID: 'COP' });
amount.parseToJson();
// Returns:
{
  '#text': '1000.00',
  '@currencyID': 'COP'
}
The #text key represents element content, while keys prefixed with @ represent XML attributes.

Aggregate Components

Aggregate components recursively serialize their children:
// From GenericAggregateComponent.ts:34
parseToJson() {
  const jsonResponse: any = {};
  Object.keys(this.paramsMap)
    .filter((attkey) => this.attributes[attkey] !== undefined)
    .sort((a, b) => this.paramsMap[a].order - this.paramsMap[b].order)
    .forEach((attKey) => {
      const { attributeName, max } = this.paramsMap[attKey];
      jsonResponse[attributeName] = Array.isArray(this.attributes[attKey])
        ? this.attributes[attKey].map((e: any) => e.parseToJson())
        : this.attributes[attKey].parseToJson();
    });
  return jsonResponse;
}
Key features:
  1. Filters null/undefined values - Only defined attributes are included
  2. Sorts by order - Ensures correct UBL element sequence
  3. Handles arrays - Maps array elements to multiple XML nodes
  4. Recursive serialization - Calls parseToJson() on nested components

Schema Documents

The Invoice class orchestrates the entire document:
// From Invoice.ts:1146
getXml(pretty = false, headless = false): string {
  Object.keys(INVOICE_CHILDREN_MAP)
    .filter((attKey) => this.children[attKey])
    .forEach((attKey) => {
      const { childName, max } = INVOICE_CHILDREN_MAP[attKey];
      const isChildAnArray = Array.isArray(this.children[attKey]);
      
      this.xmlRef.Invoice[childName] = isChildAnArray
        ? this.children[attKey].map((e: any) => e.parseToJson())
        : this.children[attKey].parseToJson();
    });
  
  return builder.create(this.xmlRef, { 
    encoding: 'UTF-8', 
    standalone: false, 
    headless 
  }).end({ pretty });
}

XML Builder Integration

The library uses the xmlbuilder package to generate XML from JSON:
import * as builder from 'xmlbuilder';

const xml = builder.create(jsonObject, {
  encoding: 'UTF-8',
  standalone: false,
  headless: false, // Include XML declaration
}).end({ pretty: true });

Namespace Handling

XML namespaces are set as properties on the root element:
// From Invoice.ts:199
setDefaultProperties() {
  const defaultProperties = [
    { key: 'xmlns', value: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' },
    { key: 'xmlns:cac', value: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2' },
    { key: 'xmlns:cbc', value: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2' },
    { key: 'xmlns:ds', value: 'http://www.w3.org/2000/09/xmldsig#' },
    { key: 'xmlns:ext', value: 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2' },
    { key: 'xmlns:xsi', value: 'http://www.w3.org/2001/XMLSchema-instance' },
  ];
  
  defaultProperties.forEach((item) => this.addProperty(item.key, item.value));
}
Call setDefaultProperties() before generating XML to include all required namespaces.

Complete Generation Example

Let’s trace how a tax total becomes XML:
1

Create the component

const taxTotal = new TaxTotal({
  taxAmount: new UdtAmount('190.00', { currencyID: 'COP' }),
  taxSubtotals: [
    new TaxSubtotal({
      taxableAmount: new UdtAmount('1000.00', { currencyID: 'COP' }),
      taxAmount: new UdtAmount('190.00', { currencyID: 'COP' }),
      taxCategory: new TaxCategory({
        percent: '19.00',
        taxScheme: new TaxScheme({ id: '01', name: 'IVA' }),
      }),
    }),
  ],
});
2

Add to invoice

invoice.addTaxTotal(taxTotal);
Stored in invoice.children.taxTotals array.
3

Call getXml()

const xml = invoice.getXml(true, false);
Triggers serialization cascade.
4

TaxTotal.parseToJson()

{
  "cbc:TaxAmount": {
    "#text": "190.00",
    "@currencyID": "COP"
  },
  "cac:TaxSubtotal": [
    {
      "cbc:TaxableAmount": {
        "#text": "1000.00",
        "@currencyID": "COP"
      },
      "cbc:TaxAmount": {
        "#text": "190.00",
        "@currencyID": "COP"
      },
      "cac:TaxCategory": {
        "cbc:Percent": { "#text": "19.00" },
        "cac:TaxScheme": {
          "cbc:ID": { "#text": "01" },
          "cbc:Name": { "#text": "IVA" }
        }
      }
    }
  ]
}
5

XML output

<cac:TaxTotal>
  <cbc:TaxAmount currencyID="COP">190.00</cbc:TaxAmount>
  <cac:TaxSubtotal>
    <cbc:TaxableAmount currencyID="COP">1000.00</cbc:TaxableAmount>
    <cbc:TaxAmount currencyID="COP">190.00</cbc:TaxAmount>
    <cac:TaxCategory>
      <cbc:Percent>19.00</cbc:Percent>
      <cac:TaxScheme>
        <cbc:ID>01</cbc:ID>
        <cbc:Name>IVA</cbc:Name>
      </cac:TaxScheme>
    </cac:TaxCategory>
  </cac:TaxSubtotal>
</cac:TaxTotal>

Validation

The library performs validation at multiple levels:

Constructor Validation

Components validate parameters during instantiation:
// From Invoice.ts:141
constructor(id: string, options: InvoiceOptions) {
  if (!id) throw new Error('invoice ID is required');
  if (!options) throw new Error('options object is required');
  
  if (!['1', '2'].includes(options.enviroment)) {
    throw new Error('Environment value is not allowed');
  }
  
  // ...
}

Type Validation

The validateInstanceOf method ensures correct types:
// From Invoice.ts:1123
private validateInstanceOf(value: any, classRefs: any[]): void {
  const matchList = classRefs.filter((classRef) => {
    if (typeof classRef === 'string') {
      return typeof value === classRef;
    }
    return value instanceof classRef;
  });
  
  if (matchList.length === 0) {
    const classNames = classRefs.map((cr) => cr.name || cr);
    throw new Error('Value must be instance of [ ' + classNames.join(' or ') + ']');
  }
}

Cardinality Validation

The parameter map enforces min/max occurrences:
// From GenericAggregateComponent.ts:66
if (Array.isArray(content[att])) {
  if (max !== undefined && content[att].length > max) {
    throw new Error(`${att} max occurrences is ${max}`);
  }
}
Validation errors throw exceptions. Always wrap document creation in try-catch blocks:
try {
  const xml = invoice.getXml();
} catch (error) {
  console.error('Validation failed:', error.message);
}

Document Finalization

Before generating XML, call finalizeDocument() to perform final calculations:
// From Invoice.ts:1021
finalizeDocument(): Invoice {
  // Update line count
  this.setLineCountNumeric(this.children.invoiceLines.length);
  
  // Assign sequential IDs to invoice lines
  this.children.invoiceLines.forEach((invoiceLine: InvoiceLine, index: number) => {
    invoiceLine.setId((index + 1).toString());
  });
  
  // Calculate CUFE (Colombia-specific)
  this.applyCufeCode();
  
  // Calculate QR code
  this.applyQRCode();
  
  return this;
}

CUFE Calculation (Colombia)

The CUFE (Código Único de Factura Electrónica) uniquely identifies an invoice:
// From Invoice.ts:931
applyCufeCode(): void {
  const ivaTaxAmount = this.findTaxTotalById('01'); // VAT
  const incTaxAmount = this.findTaxTotalById('04'); // INC
  const icaTaxAmount = this.findTaxTotalById('03'); // ICA
  
  const mapToHash = [
    this.children.id.content,                      // Invoice number
    this.children.issueDate.content,               // Issue date
    this.children.issueTime.content,               // Issue time
    this.children.legalMonetaryTotal.getLineExtensionAmount(), // Subtotal
    '01', ivaTaxAmount,                            // VAT
    '04', incTaxAmount,                            // INC
    '03', icaTaxAmount,                            // ICA
    this.children.legalMonetaryTotal.getPayableAmount(),       // Total
    this.children.accountingSupplierParty.getParty().getTaxSchemes()[0].getCompanyID(), // Supplier NIT
    this.children.accountingCustomerParty.getParty().getTaxSchemes()[0].getCompanyID(), // Customer NIT
    codeToHash,                                    // Technical key or PIN
    this.options.enviroment,                       // Environment
  ];
  
  const paramsToEncode = mapToHash.join('');
  const CUFE = new SHA384().getHash(paramsToEncode, 'binary', 'hex');
  
  this.setUUID(CUFE, { 
    schemeName: 'CUFE-SHA384', 
    schemeID: this.options.enviroment 
  });
}
The CUFE uses SHA-384 hashing and follows DIAN’s exact specification. The library handles this automatically.

Error Handling

Common errors and solutions:
Error: invoice ID is requiredSolution: Provide all required constructor parameters:
const invoice = new Invoice('INV-001', options);
Error: Value must be instance of [UdtIdentifier or string]Solution: Use the correct type or let the library convert:
// ✅ Both work
invoice.setID('INV-001');
invoice.setID(new UdtIdentifier('INV-001'));
Error: taxAmount max occurrences is 1Solution: Don’t pass arrays for singular elements:
// ❌ Wrong
new TaxTotal({ taxAmount: [amount1, amount2] });

// ✅ Correct
new TaxTotal({ taxAmount: amount1 });
Error: Cannot read property 'content' of undefinedSolution: Ensure all required components are set before calling getXml():
invoice.setAccountingSupplierParty(supplier);
invoice.setAccountingCustomerParty(customer);
invoice.setLegalMonetaryTotal(totals);

Pretty Printing

Control XML formatting with the pretty parameter:
// Compact (for transmission)
const compact = invoice.getXml(false);
// Output: <Invoice>...</Invoice>

// Pretty (for debugging)
const pretty = invoice.getXml(true);
// Output:
// <Invoice>
//   <cbc:ID>INV-001</cbc:ID>
//   ...
// </Invoice>

Headless Mode

Omit the XML declaration:
// With declaration (default)
const withDecl = invoice.getXml(false, false);
// <?xml version="1.0" encoding="UTF-8"?>
// <Invoice>...</Invoice>

// Without declaration
const headless = invoice.getXml(false, true);
// <Invoice>...</Invoice>
Use pretty printing during development, but generate compact XML for production to reduce file size.

Performance Considerations

Lazy Serialization

Serialization only occurs when getXml() is called:
const invoice = new Invoice('INV-001', options);
invoice.setID('INV-001');
invoice.setIssueDate('2026-03-06');
// No XML generated yet - just object construction

const xml = invoice.getXml(); // Serialization happens here

Caching Strategy

For performance, cache the XML output:
class InvoiceService {
  private xmlCache = new Map<string, string>();
  
  getInvoiceXml(invoiceId: string): string {
    if (this.xmlCache.has(invoiceId)) {
      return this.xmlCache.get(invoiceId)!;
    }
    
    const invoice = this.buildInvoice(invoiceId);
    const xml = invoice.getXml();
    this.xmlCache.set(invoiceId, xml);
    
    return xml;
  }
}

Debugging Tips

Inspect JSON Structure

Use getAsJson() to see the intermediate JSON:
const taxTotal = new TaxTotal({ /* ... */ });
console.log(JSON.stringify(taxTotal.getAsJson(), null, 2));

Pretty Print Components

Most components have getAsXml() for isolated testing:
const party = new Party({ /* ... */ });
console.log(party.getAsXml(true)); // Pretty-printed XML fragment

Validate Against Schema

Use external tools to validate generated XML:
xmllint --schema UBL-Invoice-2.1.xsd invoice.xml

Next Steps

Build a Complete Invoice

Follow the quickstart guide to create your first invoice

API Reference

Explore detailed API documentation

Document Structure

Review how components are organized

UBL Standard

Learn more about UBL 2.1

Build docs developers (and LLMs) love