Skip to content

Validation

Granit.Validation wraps FluentValidation with structured error codes and international validators (IBAN, BIC, E.164, ISO 3166). Granit.Validation.Europe adds France and Belgium-specific regulatory validators (NISS, NIR, SIREN, VAT, INAMI).

  • DirectoryGranit.Validation/ GranitValidator, international validators, endpoint filter
    • Granit.Validation.Europe France/Belgium regulatory validators
PackageRoleDepends on
Granit.ValidationGranitValidator<T>, international validators, endpoint filterGranit.ExceptionHandling, Granit.Localization
Granit.Validation.EuropeFrance/Belgium-specific validators (NISS, NIR, SIREN, VAT)Granit.Validation, Granit.Localization
[DependsOn(typeof(GranitValidationModule))]
public class AppModule : GranitModule { }

The module auto-discovers all IValidator<T> implementations from loaded module assemblies. Manual registration is only needed for modules without the Wolverine handler attribute:

services.AddGranitValidatorsFromAssemblyContaining<CreatePatientRequestValidator>();
public record CreatePatientRequest(
string FirstName,
string LastName,
string Email,
string Phone,
string CountryCode,
string Iban);
public class CreatePatientRequestValidator : GranitValidator<CreatePatientRequest>
{
public CreatePatientRequestValidator()
{
RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).Email();
RuleFor(x => x.Phone).E164Phone();
RuleFor(x => x.CountryCode).Iso3166Alpha2CountryCode();
RuleFor(x => x.Iban).Iban();
}
}

Apply validation to Minimal API endpoints with .ValidateBody<T>():

app.MapPost("/api/v1/patients", CreatePatient)
.ValidateBody<CreatePatientRequest>();

When validation fails, the filter returns 422 Unprocessable Entity with a HttpValidationProblemDetails body:

{
"status": 422,
"errors": {
"Email": ["Granit:Validation:InvalidEmail"],
"Phone": ["Granit:Validation:InvalidE164Phone"],
"Iban": ["Granit:Validation:InvalidIban"]
}
}

Extension methods on IRuleBuilder<T, string?> for common international formats:

CategoryValidatorFormatError code
Contact.Email()RFC 5321 practical subsetInvalidEmail
Contact.E164Phone()+ followed by 7-15 digitsInvalidE164Phone
Payment.Iban()ISO 13616 (MOD-97 check)InvalidIban
Payment.BicSwift()ISO 9362 (8 or 11 chars)InvalidBicSwift
Payment.SepaCreditorIdentifier()EPC262-08 (MOD 97-10)InvalidSepaCreditorIdentifier
Locale.Iso3166Alpha2CountryCode()2 uppercase lettersInvalidIso3166Alpha2
Locale.Bcp47LanguageTag()fr, fr-BE, zh-Hans-CNInvalidBcp47LanguageTag

All error codes are prefixed with Granit:Validation: (omitted in the table for brevity).

Use .WithErrorCodeAndMessage() to set both error code and message to the same value, preventing them from silently diverging:

RuleFor(x => x.AppointmentDate)
.GreaterThan(DateTimeOffset.UtcNow)
.WithErrorCodeAndMessage("Appointments:DateMustBeFuture");

EU-specific regulatory validators for France and Belgium. Separate package to avoid pulling regulatory dependencies into applications that do not need them.

[DependsOn(typeof(GranitValidationEuropeModule))]
public class AppModule : GranitModule { }
public class RegisterDoctorRequestValidator : GranitValidator<RegisterDoctorRequest>
{
public RegisterDoctorRequestValidator()
{
RuleFor(x => x.Niss).BelgianNiss();
RuleFor(x => x.InamiNumber).BelgianInami();
RuleFor(x => x.Vat).EuropeanVat();
RuleFor(x => x.PostalCode).BelgianPostalCode();
}
}
public class RegisterClinicRequestValidator : GranitValidator<RegisterClinicRequest>
{
public RegisterClinicRequestValidator()
{
RuleFor(x => x.Siret).FrenchSiret();
RuleFor(x => x.Vat).FrenchVat();
RuleFor(x => x.Finess).FrenchFiness();
RuleFor(x => x.PostalCode).FrenchPostalCode();
}
}

Personal identifiers:

ValidatorCountryFormatCheck algorithm
.BelgianNiss()BE11 digits (NISS/INSZ/SSIN)MOD 97 (pre-2000 and post-2000)
.FrenchNir()FR15 chars (NIR / Securite Sociale)MOD 97, Corse support (2A/2B)
.BelgianEid()BE12 digits (eID card number)MOD 97 check pair

Company identifiers:

ValidatorCountryFormatCheck algorithm
.FrenchSiren()FR9 digitsLuhn
.FrenchSiret()FR14 digits (SIREN + NIC)Luhn over all 14 digits
.BelgianBce()BE10 digits (BCE/KBO)MOD 97 check pair
.FrenchNafCode()FR4 digits + 1 letter (NAF/APE)Format only

Tax identifiers:

ValidatorCountryFormatCheck algorithm
.BelgianVat()BEBE + 10 digitsBCE algorithm
.FrenchVat()FRFR + 2-digit key + 9-digit SIRENKey = (12 + 3 * (SIREN % 97)) % 97
.EuropeanVat()EUCountry prefix + national formatDispatches per country (algorithmic for FR/BE, format for others)

Payment identifiers:

ValidatorCountryFormatCheck algorithm
.FrenchRib()FR23 chars (bank + branch + account + key)97 - (89*bank + 15*branch + 3*account) % 97
.BelgianAccountNumber()BENNN-NNNNNNN-NN (legacy pre-IBAN)first 10 digits % 97

Professional registries (healthcare):

ValidatorCountryFormatCheck algorithm
.FrenchRpps()FR11 digits (RPPS)Luhn
.FrenchAdeli()FR9 digits (ADELI)Format + length
.FrenchFiness()FR9 digits (FINESS)Luhn
.BelgianInami()BE11 digits (INAMI/RIZIV)MOD 97 check pair

Postal addresses:

ValidatorCountryFormatNotes
.FrenchPostalCode()FR5 digits (01000-99999)Includes Corse (20xxx) and DOM-TOM (97xxx, 98xxx)
.BelgianPostalCode()BE4 digits (1000-9999)
.FrenchInseeCode()FR5 chars (dept + commune)Supports 2A/2B (Corse), DOM 971-976
CategoryKey typesPackage
ModulesGranitValidationModule, GranitValidationEuropeModule
ValidationGranitValidator<T>, FluentValidationEndpointFilter<T>, .ValidateBody<T>(), .WithErrorCodeAndMessage()Granit.Validation
International validators.Email(), .E164Phone(), .Iban(), .BicSwift(), .SepaCreditorIdentifier(), .Iso3166Alpha2CountryCode(), .Bcp47LanguageTag()Granit.Validation
European validators.BelgianNiss(), .FrenchNir(), .BelgianEid(), .FrenchSiren(), .FrenchSiret(), .BelgianBce(), .FrenchNafCode(), .BelgianVat(), .FrenchVat(), .EuropeanVat(), .FrenchRib(), .BelgianAccountNumber(), .FrenchRpps(), .FrenchAdeli(), .FrenchFiness(), .BelgianInami(), .FrenchPostalCode(), .BelgianPostalCode(), .FrenchInseeCode()Granit.Validation.Europe
ExtensionsAddGranitValidation(), AddGranitValidatorsFromAssemblyContaining<T>()