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:
Filters null/undefined values - Only defined attributes are included
Sorts by order - Ensures correct UBL element sequence
Handles arrays - Maps array elements to multiple XML nodes
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:
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' }),
}),
}),
],
});
Add to invoice
invoice . addTaxTotal ( taxTotal );
Stored in invoice.children.taxTotals array.
Call getXml()
const xml = invoice . getXml ( true , false );
Triggers serialization cascade.
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" }
}
}
}
]
}
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:
Missing required elements
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.
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