- 1 What is Cardinality?
- 2 Cardinality Notation
- 3 Cardinality in Practice: pacs.008 Example
- 4 XML Schema Representation
- 5 Common Cardinality Patterns in ISO 20022
- 6 Cardinality in Data Modeling
- 7 Validation and Error Handling
- 8 Common Pitfalls and How to Avoid Them
- 9 Cardinality Quick Reference Card
- 10 Summary
Cardinality rules are among the most critical yet frequently misunderstood aspects of ISO 20022 message validation. This guide provides a comprehensive explanation of what cardinality means, how it’s expressed, and how to handle it in RTGS implementations.
1 What is Cardinality?
1.1 Definition
Cardinality defines how many times an element or attribute can or must appear within a message structure. It answers two fundamental questions:
| Question | Cardinality Aspect |
|---|---|
| Is this element required? | Minimum occurrence (0 or 1) |
| How many can appear? | Maximum occurrence (1 or many) |
1.2 Why Cardinality Matters
Real-World Impact:
| Scenario | Cardinality Error | Consequence |
|---|---|---|
| Missing mandatory field | MsgId not provided |
Message rejected at gateway |
| Extra repeating element | 5th address line when max is 4 | Data truncation or rejection |
| Wrong occurrence | Single element appears twice | Schema validation failure |
| Optional treated as required | Code assumes element exists | NullPointerException, crashes |
2 Cardinality Notation
2.1 ISO 20022 Notation Format
ISO 20022 uses a standard notation with two numbers separated by dots:
<minimum>..<maximum>
Examples:
1..1 ← Exactly one (mandatory, single)
0..1 ← Zero or one (optional, single)
1..n ← One or more (mandatory, repeating)
0..n ← Zero or more (optional, repeating)
2.2 Complete Notation Reference
| Notation | Name | Meaning | XML Schema Equivalent |
|---|---|---|---|
| 1…1 | Mandatory Single | Must appear exactly once | minOccurs="1" maxOccurs="1" |
| 0…1 | Optional Single | May appear zero or one time | minOccurs="0" maxOccurs="1" |
| 1…n | Mandatory Repeating | Must appear at least once, can repeat | minOccurs="1" maxOccurs="unbounded" |
| 0…n | Optional Repeating | May appear zero or more times | minOccurs="0" maxOccurs="unbounded" |
| 1…5 | Bounded Repeating | Must appear 1 to 5 times | minOccurs="1" maxOccurs="5" |
| 0…5 | Optional Bounded | May appear 0 to 5 times | minOccurs="0" maxOccurs="5" |
2.3 Visual Representation in Documentation
ISO 20022 documentation often uses visual indicators:
┌─────────────────────────────────────────────────────────────┐
│ Element Name │ Cardinality │ Type │
├─────────────────────────────────────────────────────────────┤
│ MsgId │ 1 │ Max35Text │
│ CreDtTm │ 1 │ ISODateTime │
│ NbOfTxs │ 0..1 │ Max15NumericText │
│ CdtTrfTxInf │ 1..n │ Transaction │
│ RmtInf │ 0..1 │ RemittanceInfo │
│ PstlAdr │ 0..1 │ PostalAddress │
│ AddrLine │ 0..7 │ Max70Text │
└─────────────────────────────────────────────────────────────┘
Legend:
1 = Mandatory, exactly one (same as 1..1)
0..1 = Optional, at most one
1..n = Mandatory, can repeat unlimited
0..n = Optional, can repeat unlimited
0..7 = Optional, up to 7 times
3 Cardinality in Practice: pacs.008 Example
3.1 Full Structure with Cardinality
Let’s examine the complete pacs.008.001.08 structure with cardinality for each element:
3.2 Detailed Element Breakdown
Document [1..1]
└── FIToFICstmrCdtTrf [1..1]
├── GrpHdr [1..1] ← Mandatory, exactly one
│ ├── MsgId [1..1] ← Mandatory: Your message ID
│ ├── CreDtTm [1..1] ← Mandatory: Creation timestamp
│ ├── NbOfTxs [0..1] ← Optional: Transaction count
│ ├── CtrlSum [0..1] ← Optional: Control sum
│ └── SttlmInf [1..1] ← Mandatory: Settlement info
│
└── CdtTrfTxInf [1..n] ← Mandatory, repeating!
├── PmtId [1..1] ← Mandatory: Payment IDs
│ ├── InstrId [0..1] ← Optional: Instruction ID
│ └── TxId [1..1] ← Mandatory: Transaction ID
├── PmtTpInf [0..1] ← Optional: Payment type
├── IntrBkSttlmAmt [1..1] ← Mandatory: Amount
├── IntrBkSttlmDt [1..1] ← Mandatory: Settlement date
├── DbtrAgt [1..1] ← Mandatory: Debtor agent
├── CdtrAgt [1..1] ← Mandatory: Creditor agent
├── UltmtDbtr [0..1] ← Optional: Ultimate debtor
├── UltmtCdtr [0..1] ← Optional: Ultimate creditor
└── RmtInf [0..1] ← Optional: Remittance info
3.3 Valid vs. Invalid Examples
- Note: The XML snippets below are proposed examples of valid and invalid
pacs.008messages to illustrate cardinality rules. These are simplified and do not represent completepacs.008messages.
✅ Valid pacs.008 (Minimal):
<Document>
<FIToFICstmrCdtTrf>
<GrpHdr>
<MsgId>MSG-001</MsgId>
<CreDtTm>2025-12-10T10:30:00Z</CreDtTm>
<SttlmInf>
<SttlmMtd>INDA</SttlmMtd>
</SttlmInf>
</GrpHdr>
<CdtTrfTxInf>
<PmtId>
<TxId>TXN-001</TxId>
</PmtId>
<IntrBkSttlmAmt Ccy="USD">1000000.00</IntrBkSttlmAmt>
<IntrBkSttlmDt>2025-12-10</IntrBkSttlmDt>
<DbtrAgt>...</DbtrAgt>
<CdtrAgt>...</CdtrAgt>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
❌ Invalid pacs.008 (Missing Mandatory):
<Document>
<FIToFICstmrCdtTrf>
<GrpHdr>
<CreDtTm>2025-12-10T10:30:00Z</CreDtTm>
<SttlmInf>
<SttlmMtd>INDA</SttlmMtd>
</SttlmInf>
</GrpHdr>
<CdtTrfTxInf>
<PmtId>
<InstrId>INSTR-001</InstrId>
</PmtId>
<IntrBkSttlmDt>2025-12-10</IntrBkSttlmDt>
<DbtrAgt>...</DbtrAgt>
<CdtrAgt>...</CdtrAgt>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
❌ Invalid pacs.008 (Cardinality Violation):
<Document>
<FIToFICstmrCdtTrf>
<GrpHdr>
<MsgId>MSG-001</MsgId>
<MsgId>MSG-002</MsgId>
<CreDtTm>2025-12-10T10:30:00Z</CreDtTm>
<SttlmInf>
<SttlmMtd>INDA</SttlmMtd>
</SttlmInf>
</GrpHdr>
<CdtTrfTxInf>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
4 XML Schema Representation
4.1 How Cardinality is Expressed in XSD
ISO 20022 XML Schemas (XSD) use minOccurs and maxOccurs attributes:
<xs:element name="MsgId" type="Max35Text" minOccurs="1" maxOccurs="1"/>
<xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0" maxOccurs="1"/>
<xs:element name="CdtTrfTxInf" type="CreditTransferTransactionInformation"
minOccurs="1" maxOccurs="unbounded"/>
<xs:element name="AddrLine" type="Max70Text" minOccurs="0" maxOccurs="7"/>
4.2 Default Values
When minOccurs and maxOccurs are not specified:
| Attribute | Default Value |
|---|---|
minOccurs |
1 (mandatory) |
maxOccurs |
1 (single) |
So this:
<xs:element name="MsgId" type="Max35Text"/>
Is equivalent to:
<xs:element name="MsgId" type="Max35Text" minOccurs="1" maxOccurs="1"/>
4.3 Reading XSD for Cardinality
Here’s how to extract cardinality information from an XSD file:
<xs:complexType name="GroupHeader">
<xs:sequence>
<xs:element name="MsgId" type="Max35Text"/>
<xs:element name="CreDtTm" type="ISODateTime"/>
<xs:element name="NbOfTxs" type="Max15NumericText" minOccurs="0"/>
<xs:element name="CtrlSum" type="DecimalNumber" minOccurs="0"/>
<xs:element name="CdtTrfTxInf" type="CreditTransferTransactionInformation"
maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
5 Common Cardinality Patterns in ISO 20022
5.1 Pattern 1: Message Wrapper
Document [1..1]
└── [RootMessage] [1..1]
└── ...
Purpose: Every XML document has exactly one root element containing exactly one message.
Examples:
Document/FIToFICstmrCdtTrf(pacs.008)Document/FIToFIPmtStsRpt(pacs.002)Document/BkToCstmrAcctRpt(camt.052)
5.2 Pattern 2: Header + Transactions
Message [1..1]
├── GroupHeader [1..1]
└── TransactionInformation [1..n]
Purpose: One header with metadata, one or more transaction details.
Examples:
- pacs.008:
GrpHdr+CdtTrfTxInf[] - pacs.009:
GrpHdr+FIToFICdtTrf[] - pacs.002:
GrpHdr+TxInfAndSts[]
5.3 Pattern 3: Optional Party Information
Transaction [1..1]
├── DebtorAgent [1..1]
├── CreditorAgent [1..1]
├── UltimateDebtor [0..1]
└── UltimateCreditor [0..1]
Purpose: Intermediaries (agents) are mandatory; ultimate parties are optional (may be same as direct parties).
Implementation Note: Don’t assume optional elements exist—always check for null/nil.
5.4 Pattern 4: Address Lines
PostalAddress [1..1]
├── StreetName [0..1]
├── BuildingNumber [0..1]
├── PostCode [0..1]
├── TownName [0..1]
├── Country [0..1]
└── AddressLine [0..7]
Purpose: Structured address fields preferred, but up to 7 unstructured lines allowed as fallback.
Common Pitfall: Some implementations use AddressLine exclusively instead of structured fields, losing data quality.
5.5 Pattern 5: Status and Reason
StatusReport [1..1]
├── OriginalMessageId [1..1]
├── TransactionStatus [1..1]
├── StatusReason [0..1]
│ ├── Code [0..1]
│ └── AdditionalInformation [0..1]
└── AdditionalStatusInfo [0..n]
Purpose: Status is mandatory; reason is optional (not all statuses have reasons); additional info can repeat.
6 Cardinality in Data Modeling
6.1 Mapping to Object-Oriented Classes
- Note: The Java code snippet below is a proposed example demonstrating how ISO 20022 cardinality rules can be mapped to object-oriented class fields. The specific annotations and class structure may vary based on the framework and design choices.
// Cardinality: 1..1 → Required field, non-nullable
@XmlElement(required = true)
private String msgId;
// Cardinality: 0..1 → Optional field, nullable
@XmlElement(required = false)
private String numberOfTransactions;
// Cardinality: 1..n → Required collection, must have at least one
@XmlElement(required = true, minOccurs = 1)
private List<CreditTransferTransactionInformation> creditTransferTransactionInformation;
// Cardinality: 0..n → Optional collection, can be empty
@XmlElement(required = false)
private List<String> addressLine;
6.2 Mapping to Database Schema
- Note: The SQL script below is a proposed example of how cardinality rules can be mapped to database schema design using
NOT NULLconstraints and table relationships. The specific table names, column types, and constraints may vary based on the database system and ORM used.
-- Cardinality: 1..1 → NOT NULL constraint
CREATE TABLE payment_message (
id BIGSERIAL PRIMARY KEY,
msg_id VARCHAR(35) NOT NULL, -- 1..1
creation_time TIMESTAMP NOT NULL, -- 1..1
nb_of_txs VARCHAR(15), -- 0..1 (nullable)
ctrl_sum NUMERIC -- 0..1 (nullable)
);
-- Cardinality: 1..n → Parent table with child table (FK NOT NULL)
CREATE TABLE payment_transaction (
id BIGSERIAL PRIMARY KEY,
message_id BIGINT NOT NULL REFERENCES payment_message(id),
tx_id VARCHAR(35) NOT NULL, -- 1..1
amount NUMERIC NOT NULL, -- 1..1
currency CHAR(3) NOT NULL, -- Attribute, 1..1
settlement_date DATE NOT NULL -- 1..1
-- No ultimate_debtor_id here (0..1, so nullable FK)
);
-- Cardinality: 0..1 → Nullable foreign key
ALTER TABLE payment_transaction
ADD COLUMN ultimate_debtor_id BIGINT REFERENCES party(id); -- 0..1
-- Cardinality: 0..n → Separate child table
CREATE TABLE address_line (
address_id BIGINT NOT NULL REFERENCES postal_address(id),
line_number INTEGER NOT NULL, -- Track order
line_text VARCHAR(70) NOT NULL, -- 0..7 repeating
PRIMARY KEY (address_id, line_number)
);
6.3 Mapping to JSON APIs
- Note: The TypeScript interface below is a proposed example illustrating how ISO 20022 cardinality rules can be reflected in JSON API definitions. The specific types and optional indicators may vary based on the API design principles.
// TypeScript interface reflecting cardinality
interface FIToFICustomerCreditTransfer {
grpHdr: GroupHeader; // 1..1 → required property
cdTrfTxInf: CreditTransfer[]; // 1..n → non-empty array
}
interface GroupHeader {
msgId: string; // 1..1 → required
creDtTm: string; // 1..1 → required, ISODateTime
nbOfTxs?: string; // 0..1 → optional property
ctrlSum?: number; // 0..1 → optional property
sttlmInf: SettlementInfo; // 1..1 → required
}
interface CreditTransfer {
pmtId: PaymentId; // 1..1 → required
pmtTpInf?: PaymentType; // 0..1 → optional
intrBkSttlmAmt: Amount; // 1..1 → required
intrBkSttlmDt: string; // 1..1 → required, ISODate
dbtrAgt: FinancialInstitution; // 1..1 → required
cdtrAgt: FinancialInstitution; // 1..1 → required
ultmtDbtr?: Party; // 0..1 → optional
ultmtCdtr?: Party; // 0..1 → optional
rmtInf?: RemittanceInfo; // 0..1 → optional
}
7 Validation and Error Handling
7.1 Schema Validation Errors
Common validation error messages for cardinality violations:
| Error Message | Cause | Cardinality Rule |
|---|---|---|
Element 'MsgId' is missing |
Mandatory element not present | 1…1 violated |
Element 'CdtTrfTxInf' must appear at least once |
No transactions in batch | 1…n violated |
Element 'MsgId' appears more than once |
Duplicate element | 1…1 violated |
Maximum number of address lines exceeded |
More than 7 lines | 0…7 violated |
7.2 Schematron Rules for Cardinality
- Note: The XML snippet below is a proposed example of Schematron rules for enforcing business-level cardinality. The specific rules and XPath expressions will depend on the detailed validation requirements.
<sch:rule context="FIToFICstmrCdtTrf">
<sch:assert test="count(CdtTrfTxInf) >= 1">
At least one credit transfer transaction is required.
</sch:assert>
<sch:rule context="GrpHdr">
<sch:assert test="not(NbOfTxs) or NbOfTxs = count(../CdtTrfTxInf)">
NbOfTxs should match the actual number of transactions.
</sch:assert>
</sch:rule>
</sch:rule>
7.3 Application-Level Validation
- Note: The Java code snippet below is a proposed example demonstrating application-level validation for cardinality. The specific implementation will vary based on the programming language and validation framework used.
public void processPayment(FIToFICstmrCdtTrf payment) {
// XSD ensures these exist, but validate business rules
if (payment.getCdtTrfTxInf().isEmpty()) {
throw new ValidationException("No transactions in payment");
}
for (CreditTransferTransaction tx : payment.getCdtTrfTxInf()) {
// Optional element - must check for null
if (tx.getUltmtDbtr() != null) {
validateParty(tx.getUltmtDbtr());
}
// Optional remittance info
RemittanceInfo remittance = tx.getRmtInf();
if (remittance != null && remittance.getUstrd() != null) {
// Process unstructured remittance
}
}
}
8 Common Pitfalls and How to Avoid Them
8.1 Pitfall 1: Assuming Optional Means “Always Present”
- Note: The Java code snippets below are proposed examples illustrating a common pitfall and best practices for handling optional elements.
// ❌ WRONG: Assumes optional element exists
String debtorName = transaction.getUltmtDbtr().getNm(); // NullPointerException!
// ✅ CORRECT: Check for null first
Party debtor = transaction.getUltmtDbtr();
String debtorName = (debtor != null) ? debtor.getNm() : null;
// ✅ BETTER: Use Optional (Java 8+)
String debtorName = Optional.ofNullable(transaction.getUltmtDbtr())
.map(Party::getNm)
.orElse("Unknown");
8.2 Pitfall 2: Not Validating Collection Size
- Note: The Java code snippets below are proposed examples illustrating a common pitfall and best practices for validating collection sizes.
// ❌ WRONG: Assumes any size is valid
List<String> addrLines = address.getAddrLine();
process(addrLines.get(0)); // IndexOutOfBoundsException if empty!
// ✅ CORRECT: Validate collection
List<String> addrLines = address.getAddrLine();
if (addrLines != null && !addrLines.isEmpty()) {
process(addrLines.get(0));
}
// ✅ BETTER: Validate bounds (0..7)
if (addrLines != null && addrLines.size() <= 7) {
// Process address lines
}
8.3 Pitfall 3: Confusing 0…1 with 1…1 in Database Design
- Note: The SQL snippets below are proposed examples illustrating a common pitfall in database design related to cardinality and best practices for handling optional foreign keys.
-- ❌ WRONG: Making optional field NOT NULL
CREATE TABLE payment (
ultimate_debtor_id BIGINT NOT NULL -- ERROR: UltmtDbtr is 0..1!
);
-- ✅ CORRECT: Optional field is nullable
CREATE TABLE payment (
ultimate_debtor_id BIGINT -- NULL allowed for 0..1
);
8.4 Pitfall 4: Ignoring Bounded Cardinality
- Note: The Java code snippets below are proposed examples illustrating a common pitfall related to bounded cardinality and best practices for enforcing element limits.
// ❌ WRONG: No validation of upper bound
for (String line : address.getAddrLine()) {
save(line); // What if there are 20 lines? (max is 7!)
}
// ✅ CORRECT: Enforce bounds
List<String> addrLines = address.getAddrLine();
if (addrLines != null && addrLines.size() <= 7) {
for (String line : addrLines) {
save(line);
}
} else if (addrLines != null && addrLines.size() > 7) {
throw new ValidationException("Maximum 7 address lines allowed");
}
8.5 Pitfall 5: Misreading Cardinality in Documentation
Documentation shows:
CdtTrfTxInf [1..n]
❌ MISREAD: "It's optional because some messages might not have it"
✅ CORRECT: "At least one transaction is REQUIRED"
9 Cardinality Quick Reference Card
┌─────────────────────────────────────────────────────────────────┐
│ ISO 20022 CARDINALITY QUICK REFERENCE │
├─────────────────────────────────────────────────────────────────┤
│ Notation │ Name │ XML Schema │
│ 1..1 │ Mandatory Single │ minOccurs="1" maxOccurs="1" │
│ 0..1 │ Optional Single │ minOccurs="0" maxOccurs="1" │
│ 1..n │ Mandatory Repeat │ minOccurs="1" maxOccurs="unbounded" │
│ 0..n │ Optional Repeat │ minOccurs="0" maxOccurs="unbounded" │
│ 1..5 │ Bounded (1-5) │ minOccurs="1" maxOccurs="5" │
│ 0..5 │ Optional Bounded │ minOccurs="0" maxOccurs="5" │
├─────────────────────────────────────────────────────────────────┤
│ Java Mapping: │
│ 1..1 → private Type field; (required, non-null) │
│ 0..1 → private Type field; (optional, check for null) │
│ 1..n → private List<Type> field; (required, non-empty) │
│ 0..n → private List<Type> field; (optional, may be empty) │
├─────────────────────────────────────────────────────────────────┤
│ Database Mapping: │
│ 1..1 → column VARCHAR(35) NOT NULL │
│ 0..1 → column VARCHAR(35) (nullable) │
│ 1..n → child table with FK NOT NULL │
│ 0..n → child table with FK (nullable or empty set) │
├─────────────────────────────────────────────────────────────────┤
│ Common pacs.008 Cardinalities: │
│ MsgId [1..1] ← Must provide exactly one │
│ CreDtTm [1..1] ← Must provide exactly one │
│ NbOfTxs [0..1] ← Optional count │
│ CdtTrfTxInf [1..n] ← At least one transaction required │
│ TxId [1..1] ← Must provide exactly one per tx │
│ IntrBkSttlmAmt [1..1] ← Must provide exactly one per tx │
│ DbtrAgt [1..1] ← Must provide exactly one per tx │
│ CdtrAgt [1..1] ← Must provide exactly one per tx │
│ UltmtDbtr [0..1] ← Optional ultimate debtor │
│ UltmtCdtr [0..1] ← Optional ultimate creditor │
│ RmtInf [0..1] ← Optional remittance │
│ AddrLine [0..7] ← Up to 7 unstructured lines │
└─────────────────────────────────────────────────────────────────┘
10 Summary
📋 Key Takeaways
Essential cardinality knowledge:
✅ Notation Understanding
1..1= Mandatory, exactly one0..1= Optional, at most one1..n= Mandatory, one or more (repeating)0..n= Optional, zero or more (repeating)
✅ XML Schema Mapping
minOccurs="1"(default) = mandatoryminOccurs="0"= optionalmaxOccurs="1"(default) = singlemaxOccurs="unbounded"= repeating
✅ Implementation Rules
- Mandatory (1..1, 1..n) → Validate presence, never null
- Optional (0..1, 0..n) → Check for null/empty before access
- Bounded (0..5, 1..7) → Validate both min and max
✅ Common Pitfalls
- Assuming optional elements exist (NullPointerException)
- Not validating collection bounds
- Making optional database fields NOT NULL
- Misreading documentation notation
Footnotes for this article: