Skip to content

Validation & XSD

sepa-xml-ts provides two layers of validation that can be used independently or together.

Business validation runs against the Zod schema, which is the single source of truth for the model. It enforces all semantic constraints:

  • IBAN mod-97 checksum
  • BIC format (when provided)
  • SEPA charset EPC217-08 on all string fields
  • Amount constraints: max 2 decimal places, and the EPC AT-06 range of 0.01 to 999,999,999.99 EUR per transaction
  • Identifier slash rules: MsgId, PmtInfId, and EndToEndId must not start or end with / and must not contain //
  • Date format YYYY-MM-DD for execution and collection dates
  • SEPA Creditor Identifier check digits (ISO 7064 MOD 97-10, strict), with German DE identifiers required to be exactly 18 characters

These refinements live on the Zod schema, so they apply to every entry point: validate*, the writers (which validate before emitting), and parse (which validates what it reads). The amount cap and identifier rules are derived from the EPC implementation guidelines, not copied from any third-party library.

Validates any unknown input as a CreditTransferDocument. Returns a typed result. Accepts an optional { profile } option to run bank-profile checks on top of the base Zod rules.

import { validateCreditTransfer, requireBic } from 'sepa-xml-ts'
const result = validateCreditTransfer(doc)
if (!result.ok) {
// result.errors: ZodIssue[]
for (const err of result.errors) {
console.error(err.path.join('.'), err.message)
}
} else {
// result.data: CreditTransferDocument (validated)
const xml = writeCreditTransfer(result.data)
}
// With a bank profile:
const profileResult = validateCreditTransfer(doc, { profile: requireBic })
if (!profileResult.ok) {
// result.errors: ZodIssue[] (base schema failures)
// result.profileIssues: ProfileIssue[] (bank-profile failures)
console.error(profileResult.errors, profileResult.profileIssues)
}

Same for DirectDebitDocument.

import { validateDirectDebit, requireBic } from 'sepa-xml-ts'
const result = validateDirectDebit(doc, { profile: requireBic })

Both validate functions return a ValidationResult:

type ValidationResult<T> =
| { ok: true; data: T }
| { ok: false; errors: ZodIssue[]; profileIssues?: ProfileIssue[] }

profileIssues is only present when a profile was supplied.

writeCreditTransfer and writeDirectDebit run the same validation internally. If the model fails, they throw a ValidationError with the same issue list. You do not have to call validateCreditTransfer separately before writing unless you want the typed result shape.

XSD validation checks the serialized XML against the official EPC ISO 20022 schema using libxml2-wasm. It is an opt-in extra that lives behind the sepa-xml-ts/xsd subpath.

import { validateXsd } from 'sepa-xml-ts/xsd'
const xsdResult = await validateXsd(xml)
if (!xsdResult.valid) {
for (const err of xsdResult.errors) {
console.error(err)
}
}

validateXsd auto-detects the message type and namespace from the XML and selects the correct XSD. It covers all seven schemas:

  • pain.001.001.09
  • pain.001.003.03 (German DK)
  • pain.001.001.03
  • pain.008.001.08
  • pain.008.003.02 (German DK)
  • pain.008.001.02

You generally do not need XSD validation in production write paths. The library already enforces every constraint via the Zod schema and the writer. XSD validation is most useful for:

  • Verifying files produced by a third party before processing
  • Belt-and-suspenders CI checks after generating files
  • Debugging: when the XSD rejects a file, the error message points at the exact element

The library’s own CI uses XSD validation as an oracle in property tests: for every build, it generates hundreds of random valid models, writes them to XML, and asserts each file passes the official EPC XSD. This is what “XSD-as-oracle” means in the documentation.