← All posts
Deep Dive · 12 min read

SDF vs ZUGFeRD: Why JSON Wins for Enterprise Document Exchange

Y
Yunus YILDIZ
Founder, Etapsky

ZUGFeRD and its French sibling FACTUR-X embed XML into PDFs. SDF bundles JSON. That's not just a different serialization format — it reflects a completely different set of assumptions about who will implement the standard, what tooling they will use, and what a good developer experience looks like in 2026.

A brief history of ZUGFeRD

ZUGFeRD — Zentraler User Guide des Forums elektronische Rechnung Deutschland — was published in 2014 as a way to exchange structured invoice data alongside a human-readable PDF. The approach was clever: embed a machine-readable XML file into the PDF itself, using the PDF/A-3 standard which allows arbitrary binary attachments inside a PDF. The receiver gets one file, and that file is both printable and parseable.

FACTUR-X, the French equivalent, followed in 2017 and is now jointly maintained with ZUGFeRD under a unified specification. XRechnung, mandated for German public procurement since 2020, dropped the PDF layer entirely — it's pure XML. The EN 16931 European e-invoicing standard underpins all three, and all three are increasingly mandated by EU member states for B2G and, progressively, B2B transactions.

None of these formats were designed with modern application developers in mind. They were designed by standards committees, for ERP vendors, in an era when XML was the uncontested lingua franca of enterprise data exchange.

The XML problem

Here's what you write to read a ZUGFeRD XML invoice's total payable amount in Python:

zugferd_parse.py
import lxml.etree as ET

ns = {
    'rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
    'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
    'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
}

tree = ET.parse('factur-x.xml')
root = tree.getroot()

total = root.find(
    './/rsm:SupplyChainTradeTransaction'
    '/ram:ApplicableHeaderTradeSettlement'
    '/ram:SpecifiedTradeSettlementHeaderMonetarySummation'
    '/ram:DuePayableAmount',
    ns
)
amount = float(total.text) if total is not None else 0.0

Here's the equivalent for an SDF document:

sdf_parse.py
from etapsky_sdf import SdfReader

doc = SdfReader.open('invoice.sdf')
amount = doc.data['payment']['dueAmount']

This isn't an artificial example constructed to flatter SDF. The XPath expression above is the real path you'd traverse in a ZUGFeRD EN 16931 Comfort profile document. The namespace prefixes differ between ZUGFeRD versions (1.0 uses rsm, some implementations use aliases). The namespace URIs are 80+ characters long. Miss a namespace registration and you get None silently.

The schema situation

ZUGFeRD validation is handled by XML Schema Definition (XSD) files published by the standard body. To validate a document, you download the XSD bundle, load it into an XML library, and validate. The XSD for EN 16931 Comfort profile spans multiple files with cross-file references and requires an XSD-aware validator — you can't use a generic JSON Schema tool.

SDF uses JSON Schema Draft 2020-12. The schema for an invoice is a single schema.json file bundled inside the .sdf archive. Validation uses ajv in Node.js or jsonschema in Python — tools every developer already has installed. The schema travels with the document, so a recipient can validate an invoice against the exact schema the producer intended, even if that schema is a custom extension of the base SDF invoice schema.

validate.ts
import { SdfKit } from '@etapsky/sdf-kit'

const result = await SdfKit.validate('invoice.sdf')
if (!result.valid) {
  console.error(result.errors) // [{}]
}

Document types: invoices only vs. everything

ZUGFeRD is an invoicing standard. Full stop. The data model is purpose-built for invoices: line items, tax codes, payment terms, references to purchase orders. If you want to exchange a purchase order, a delivery note, or a government permit application in a machine-readable format, ZUGFeRD is not the answer. You'd reach for a different standard — PEPPOL BIS 3.0 for purchase orders, perhaps, each with its own XML schema dialect.

SDF makes no assumptions about document type. The container format is generic. The schema is application-defined. Etapsky ships reference schemas for 7 document types and the community can publish their own to the schema registry. The same toolchain — @etapsky/sdf-kit, the CLI, the Python SDK — works for every document type.

The container model

ZUGFeRD's embedding-in-PDF approach has a subtle problem: the PDF is the container. To extract the XML, you need a PDF parser. PDF is not a trivial format — the specification runs to over 700 pages. If you want to process ZUGFeRD invoices programmatically, you need a PDF library in your stack even if you never intend to render the visual layer.

SDF's container is a ZIP file. ZIP parsing is trivial — it's in the standard library of every language. The .sdf extension is a convention; the file is a valid ZIP archive. You can open it with any ZIP tool and inspect the contents without any SDF-specific tooling.

Feature comparison

Feature ZUGFeRD / FACTUR-X SDF
Data format XML (UN/CEFACT) JSON
Container PDF/A-3 attachment ZIP archive
Schema format XSD (multi-file) JSON Schema 2020-12 (bundled)
Document types Invoice only Any business document
Signing PAdES (PDF signatures) ECDSA-P256 / RSA-2048 (JSON)
Offline validation Requires XSD download Schema bundled in archive
EU mandate coverage B2G in DE, FR, IT, ES Not mandated (yet)
ERP ecosystem SAP, Oracle, Sage built-in Connectors via sdf-server-core
Toolchain complexity PDF lib + XML lib + XSD validator ZIP + JSON + ajv
Browser support Limited (PDF manipulation) Full (Web Crypto, native ZIP)

When ZUGFeRD is still the right answer

ZUGFeRD and XRechnung are not going away, and in several contexts they remain the correct choice:

The SDF roadmap includes an EN 16931 compatibility layer — a validator that checks whether an SDF invoice's data.json satisfies the semantic constraints of EN 16931. This would make SDF documents convertible to ZUGFeRD for regulatory submission while preserving the superior developer experience for internal processing. That work is on the roadmap for v0.3.

Interoperability in practice

The realistic adoption path for SDF is not replacement of ZUGFeRD but addition to it. An sdf-server-core deployment can receive an incoming ZUGFeRD invoice, extract the XML payload, transform it to SDF's JSON data model, and re-pack it as an .sdf file for internal processing — replacing the error-prone OCR pipeline that most mid-market companies currently rely on for non-EDI counterparties.

In the other direction, sdf-kit's convert API can emit a ZUGFeRD-compatible XML payload from an SDF invoice's data model, enabling SDF-native systems to comply with regulatory requirements without maintaining two separate document production workflows.

JSON doesn't win because XML is wrong. JSON wins because the developers building the next generation of enterprise tooling think in JSON, test in JSON, and should not have to learn XPATH to read an invoice amount.

Previous post Next post