Skip to content

Validation ​

Validate generated XML invoices against EN 16931 and XRechnung business rules before sending them to customers or embedding them into PDFs.

When to Validate ​

During development — validate every generated invoice to catch missing fields or structural errors early. This requires Java and Saxon HE.

In production — once your invoice generation logic is verified, you can skip validation for performance. If users create invoices interactively (variable input), keep the validator active.

Setup ​

Install Java and download the validation artifacts:

bash
# Debian / Ubuntu
sudo apt-get install default-jre-headless

# Arch Linux
sudo pacman -S jre-openjdk-headless

# macOS
brew install openjdk

# Download Saxon, XSD schemas, and Schematron XSLT files
bin/setup-schemas

bin/setup-schemas downloads:

  • Saxon HE 12.5 + xmlresolver (XSLT processor)
  • UBL 2.1 XSD schemas (OASIS)
  • CII D16B XSD schemas (UN/CEFACT)
  • CEN EN16931 Schematron XSLT (business rules)
  • XRechnung Schematron XSLT (German CIUS rules)

Schematron Validation ​

The Schematron validator checks EN 16931 business rules (e.g. "payable amount must match line totals plus tax") and XRechnung-specific rules (e.g. "seller contact is required").

ruby
require "zugpferd"
require "zugpferd/validation"  # explicit opt-in, requires Java + Saxon

xml = Zugpferd::CII::Writer.new.write(invoice)

validator = Zugpferd::Validation::SchematronValidator.new(
  schemas_path: "vendor/schemas"
)

# Validate against CEN EN16931 rules
errors = validator.validate(xml, rule_set: :cen_cii)

# Validate against XRechnung rules
errors = validator.validate(xml, rule_set: :xrechnung_cii)

# Validate against both at once
errors = validator.validate_all(xml, rule_sets: [:cen_cii, :xrechnung_cii])

Rule Sets ​

KeyRulesSyntax
:cen_ublEN 16931 (BR-*)UBL
:cen_ciiEN 16931 (BR-*)CII
:xrechnung_ublXRechnung (BR-DE-*)UBL
:xrechnung_ciiXRechnung (BR-DE-*)CII

Interpreting Results ​

Each error has an id, text, location, and flag:

ruby
errors.each do |error|
  puts "[#{error.flag}] #{error.id}: #{error.text}"
  # => [fatal] BR-CO-25: In case the Amount due for payment (BT-115)
  #    is positive, either the Payment due date (BT-9) or the
  #    Payment terms (BT-20) shall be present.
end

# Fatal errors = the invoice is invalid
fatals = errors.select { |e| e.flag == "fatal" }

# Warnings / information = should be fixed but not blocking
warnings = errors.reject { |e| e.flag == "fatal" }

The error ID (e.g. BR-CO-25, BR-DE-2) references a specific business rule in the EN 16931 or XRechnung specification.

XSD Schema Validation ​

The schema validator checks structural XML correctness against the XSD schemas. This catches malformed elements, wrong namespaces, or missing required XML elements — but not business logic errors.

ruby
require "zugpferd/validation"

validator = Zugpferd::Validation::SchemaValidator.new(
  schemas_path: "vendor/schemas"
)

errors = validator.validate(xml, schema_key: :ubl_invoice)
# => [] if valid, or ["Element 'foo': not expected"] etc.

Schema Keys ​

KeySchema
:ubl_invoiceUBL 2.1 Invoice XSD
:ubl_credit_noteUBL 2.1 Credit Note XSD
:ciiCII D16B XSD

XSD validation does not require Java — it uses Nokogiri's built-in XML Schema support.

Typical Workflow ​

ruby
require "zugpferd"
require "zugpferd/validation"

# 1. Build the invoice
invoice = build_invoice(params)

# 2. Serialize to XML
xml = Zugpferd::CII::Writer.new.write(invoice)

# 3. Validate (during development or for interactive input)
validator = Zugpferd::Validation::SchematronValidator.new(
  schemas_path: "vendor/schemas"
)
errors = validator.validate_all(xml, rule_sets: [:cen_cii, :xrechnung_cii])
fatals = errors.select { |e| e.flag == "fatal" }

if fatals.any?
  # Show errors to the user or log them
  fatals.each { |e| puts "[#{e.id}] #{e.text}" }
  raise "Invoice validation failed"
end

# 4. Embed into PDF (optional)
require "zugpferd/pdf"
Zugpferd::PDF::Embedder.new.embed(
  pdf_path: "input.pdf",
  xml: xml,
  output_path: "zugferd.pdf",
  conformance_level: "XRECHNUNG"  # must match the CIUS in customization_id
)