We’re announcing a new package today: BeneficialStrategies.Iso20022.FluentValidation, the validation companion to the C# ISO 20022 library. It’s on NuGet now, and it’s up to date with the same June 26, 2026 specification snapshot as the library it validates.
Up front: this is a work in progress, not a finished product. It already covers real ground — enough to fully validate two entire business areas — but large parts of the specification aren’t built out yet. We’d rather ship what’s solid and be honest about what isn’t than wait until everything is done. Below is exactly where that line sits today.
Why a separate package
The C# library gives you strongly-typed, compiler-enforced ISO 20022 messages — a BICFI can’t
hold something that isn’t a valid BIC, an IBAN can’t hold something that isn’t a valid IBAN. But
some ISO 20022 rules aren’t about any one field; they’re about the relationship between fields, or
they depend on data the compiler simply can’t see. A classic example: a case identification is
allowed to appear at the message level or the transaction level, but not both — each location is
individually optional, so no property type on its own can catch it if both get filled in at once.
That’s what this package is for: FluentValidation validators
that check the cross-field business rules the type system can’t express.
Not just the easy checks
It’s worth showing what these validators actually catch, because it’s not just “field is required”
or “string is too long” — those are already handled at the type level. Take
FIToFIPaymentStatusReportV16 (pacs.002.001.16) — the message a bank sends back to say what
happened to a payment it was asked to process. It carries an overall group status for the whole
batch, and each individual transaction in that batch can optionally report its own status too — and
the spec requires those two to stay consistent with each other. The validator enforces four rules
that all follow the same “if the group status says X, then every transaction status must Y” shape:
| Rule | What it enforces |
|---|---|
GroupStatusAcceptedRule |
If the group status is any flavor of Accepted, no individual transaction is allowed to report Rejected — the two would contradict each other. |
GroupStatusPendingRule |
If the group status is Pending, same thing — no transaction can claim Rejected yet. |
GroupStatusRejectedRule |
If the group status is Rejected, every transaction that does report a status must also say Rejected — a rejected batch can’t contain a transaction claiming success. |
GroupStatusReceivedRule |
If the group status is Received (“we got it, haven’t processed it yet”), no individual transaction is allowed to report a status at all — it’s too early for that. |
None of those are things a property type could ever catch on its own — GroupStatus and
TransactionStatus are both perfectly valid values individually; it’s only the combination that’s
wrong. The same validator also enforces a second kind of conditional rule, keyed off how many times
a reference block appears rather than its value: whether the original-group-information block shows
up zero, one, or more than one time in the message determines what has to be present (or absent)
down at the transaction level.
It’s also honest about where it can’t fully verify a spec rule. The same message has an eighth
constraint — that message-level supplementary data must not be used to describe an individual
transaction — that the validator documents but doesn’t enforce, because nothing in the C# model
distinguishes message-scoped content from transaction-scoped content once it’s inside a
SupplementaryData instance. Every validator’s XML doc <remarks> spells out exactly this kind of
thing: what’s checked, what isn’t, and why — not just a coverage percentage.
Scope: what’s covered today, and what isn’t
The package ships 1,296 validators in total, split into two tiers:
- 391 validators carry full spec-compliance coverage — every field-level constraint and
cross-field rule the specification defines for that type. That includes 43 top-level messages
validated completely, top to bottom, with zero exceptions anywhere in their reachable graph.
Two business areas are entirely done:
pain(Payments Initiation) is 100% complete, andpacs(Payments Clearing and Settlement) is 100% complete. - 905 validators have abbreviated coverage. Right now these only enforce one specific thing — a “required-looking” collection that was generated with no enforced lower bound, so an empty collection compiles but actually violates the spec. They haven’t yet been reviewed for the rest of what a full validator would check.
Every validator, full or abbreviated, documents in its own XML doc comments exactly which constraints it currently enforces — so you’re never guessing what coverage you’re actually getting from a given type.
The known gaps: most business areas outside pain and pacs don’t yet have a single
message-level validator that checks a whole message end to end — the component-level validators
exist and are correct, you just compose them yourself with SetValidator() for now, the same
mechanism described below. And the abbreviated validators, by definition, aren’t a complete
compliance check yet.
Hierarchical validation
ISO 20022 messages are trees of nested components, and the validators mirror that shape.
Validators for a top-level message wire in the validators for their nested components via
FluentValidation’s SetValidator(), so validating one message automatically walks its entire
graph — you’re not manually re-checking each nested piece.
Polymorphic parts of the model (ISO 20022’s “choice” types, where a field can be one of several
alternative shapes) use FluentValidation’s SetInheritanceValidator for the dispatch, so the
correct validator runs for whichever variant is actually present at runtime, and that variant’s own
payload gets validated in turn.
Built for extending, not just consuming
Everything registers through standard .NET dependency injection, which means the parts you’re likely to want to adjust for your own environment are just DI registrations you can override — nothing requires forking the library.
Three ways to scope what gets registered with AddIso20022Validators():
- By
businessAreas— register validators for specific ISO 20022 business areas only. - By
rootTypes— register only what’s reachable from a specific set of top-level types (computed by walking each validator’s own constructor dependencies, so it’s the tightest of the three options). - By an arbitrary
filterpredicate over validator types, for anything more specific than the other two allow.
And for genuine implementation-specific rules — a check the ISO spec doesn’t define but your
integration needs — the registry types described below are declared virtual specifically so you
can subclass and layer your own logic on top at startup, without touching the package’s own code.
Configurable external code sets
Some ISO 20022 fields reference “external” code sets — lists the standard defers to an outside
authority for (currency codes, country codes, and similar), rather than enumerating in the spec
itself. The package validates these through an IExternalCodeRegistry<TCode> per code type, and
by default it’s an in-memory registry you populate yourself:
var countries = new InMemoryExternalCodeRegistry<CountryCode>();
countries.Add("US");
countries.Add("CA");
services.AddIso20022Validators();
services.AddSingleton<IExternalCodeRegistry<CountryCode>>(countries);
An unpopulated registry is permissive by default — nothing gets rejected on that basis until
you actually add values, so turning the package on doesn’t suddenly start failing messages against
an empty allowlist. Some external code types that ISO 20022’s own registry snapshot already has
known values for start pre-populated automatically; Add/Remove just adjust that seeded set.
If a fixed list isn’t enough — you want your own rule layered on top, or values sourced from a
database instead of an in-memory set — InMemoryExternalCodeRegistry<TCode> exposes a virtual IsAcceptable method built for exactly that: subclass it, override the method, and register your
subclass in place of the default.
What’s next
The immediate priority is deepening coverage within pain and pacs — turning more of the 905
abbreviated validators into full spec-compliance ones — and then extending that same full-coverage
treatment out to the rest of ISO 20022’s business areas: acmt, camt, and the others beyond the
two that are done today.
That expansion follows a policy we’ve formalized internally: for any business area, full coverage
means implementing the newest Registered version of every top-level message, plus any more
recently Provisionally Registered version beyond it — not just whatever version happened to be
convenient to build first. It’s the same principle already reflected in how pain and pacs were
built: superseded message IDs (the kind ISO 20022 quietly renamed and chained forward years ago)
are tracked under their current names, not their retired ones.
Try it
dotnet add package BeneficialStrategies.Iso20022 --version 0.6.2-alpha
dotnet add package BeneficialStrategies.Iso20022.FluentValidation --version 0.6.2-alpha
Get it from NuGet:
BeneficialStrategies.Iso20022.FluentValidation →
Like the rest of the library, it’s free and permissively licensed. Source, issue tracker, and full documentation live on GitHub. Questions or feedback on the implementation are welcome at [email protected].