Brand new validation library

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:

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():

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].