Mrz.Net
0.2.0
dotnet add package Mrz.Net --version 0.2.0
NuGet\Install-Package Mrz.Net -Version 0.2.0
<PackageReference Include="Mrz.Net" Version="0.2.0" />
<PackageVersion Include="Mrz.Net" Version="0.2.0" />
<PackageReference Include="Mrz.Net" />
paket add Mrz.Net --version 0.2.0
#r "nuget: Mrz.Net, 0.2.0"
#:package Mrz.Net@0.2.0
#addin nuget:?package=Mrz.Net&version=0.2.0
#tool nuget:?package=Mrz.Net&version=0.2.0
Mrz.NET
ICAO 9303 machine-readable-zone parser and validator for passports, ID cards, and visas. Parses TD1, TD2, and TD3 layouts plus MRV-A and MRV-B machine-readable visas, and verifies every check digit against the official 7-3-1 algorithm. Zero external dependencies.
Every passport, national ID card, and visa printed to ICAO Doc 9303 carries two or three lines of OCR-B text at the bottom: the machine-readable zone. It packs the document type, issuing state, holder name, document number, nationality, date of birth, sex, expiry date, and a set of check digits into a handful of fixed-width lines. Reading it correctly is the first step in any KYC or border-control pipeline, and getting the check-digit math wrong is the easiest way to silently accept a forged or mistyped document. On NuGet there was no small, dependency-free, ICAO-verified library for this. Mrz.NET is that library: it embeds the ICAO worked examples as test fixtures and asserts exact field parsing and exact check-digit results against them, including deliberately corrupted input.
Install
dotnet add package Mrz.Net
Quickstart
using Mrz;
string passportMrz = """
P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<
L898902C36UTO7408122F1204159ZE184226B<<<<<10
""";
MrzDocument document = MrzParser.Parse(passportMrz);
Console.WriteLine($"{document.Surname}, {document.GivenNames}"); // ERIKSSON, ANNA MARIA
Console.WriteLine(document.DocumentNumber); // L898902C3
Console.WriteLine(document.Validation.IsValid); // True
Checking whether a scan is trustworthy
A scanner, a phone camera, or a human at a counter can all introduce a single wrong character. MrzDocument.Validation tells you exactly which field's check digit failed instead of just handing you a broken parse:
using Mrz;
if (!MrzParser.TryParse(scannedLines, out MrzDocument? document))
{
// Structurally not an MRZ: wrong line count, wrong line length, or a
// character outside the permitted MRZ character set.
return Reject("Unreadable machine-readable zone.");
}
if (!document!.Validation.IsValid)
{
if (!document.Validation.DocumentNumberCheckDigitValid)
{
return Reject("Document number check digit failed; re-scan the document.");
}
if (!document.Validation.CompositeCheckDigitValid)
{
return Reject("Composite check digit failed; the document may be tampered with.");
}
}
Accept(document);
Feeding a KYC screening pipeline
MrzDocument gives you the exact surname and given names ICAO 9303 defines, ready to hand to a sanctions or watchlist screen, alongside the raw document identifiers:
using Mrz;
MrzDocument document = MrzParser.Parse(idCardLines);
var screeningRequest = new
{
Surname = document.Surname,
GivenNames = document.GivenNames,
Nationality = document.Nationality,
DateOfBirth = document.DateOfBirth, // "YYMMDD" as printed
DocumentNumber = document.DocumentNumber,
};
// Pass screeningRequest to a sanctions/PEP screening service, e.g. Sanctions.Net.
What it parses
| Format | Lines | Line length | Typical use |
|---|---|---|---|
| TD1 | 3 | 30 | National ID cards |
| TD2 | 2 | 36 | ID cards |
| TD3 | 2 | 44 | Passports |
| MRV-A | 2 | 44 | Machine-readable visas |
| MRV-B | 2 | 36 | Machine-readable visas |
MrzParser.Parse auto-detects the format from the number and length of lines supplied, so you do not need to tell it which one you have. MRV-A shares TD3's two-lines-of-44 geometry and MRV-B shares TD2's two-lines-of-36 geometry; a visa is told apart from a passport or ID card by a document code that begins with V (ICAO Doc 9303 Part 7). A machine-readable visa has no overall composite check digit — ICAO 9303 defines the trailing positions on line 2 as optional data, not a composite check — so for MrvA and MrvB documents Validation.CompositeCheckDigitValid is null (not applicable) rather than a misleading false. The individual document-number, date-of-birth, and date-of-expiry check digits are present on a visa and are still validated. The optional-data region is exposed raw through PersonalNumber.
Every MrzDocument exposes: DocumentType, DocumentCode, IssuingState, Surname, GivenNames, IsNameTruncated, DocumentNumber, Nationality, DateOfBirth, Sex, DateOfExpiry, PersonalNumber, SupplementalOptionalData (TD1's second optional data field, next to the document number), the raw Lines, and Validation.
DateOfBirth and DateOfExpiry are the six raw digits (YYMMDD) exactly as printed. ICAO 9303 does not define how to resolve the two-digit year to a century, so Mrz.NET does not guess; interpret it with whatever domain knowledge you have (a passport application date, an OCR timestamp, a plausible age range).
Check digits
MrzCheckDigitCalculator implements the ICAO 9303 algorithm directly if you need it standalone: every character is converted to a value (digits keep their value, A-Z map to 10-35, the filler < maps to 0), multiplied by a weight that cycles 7, 3, 1 from the first character, summed, and reduced modulo 10.
using Mrz;
char checkDigit = MrzCheckDigitCalculator.Compute("L898902C3"); // '6'
bool isValid = MrzCheckDigitCalculator.Verify("L898902C3", '6'); // true
MrzDocument.Validation runs this for every check-digited field in the document (document number, date of birth, date of expiry, the TD3 personal number, and the composite check digit) and exposes both the per-field results and a single IsValid flag. TD1 and TD2 have no independently check-digited personal number field, so PersonalNumberCheckDigitValid is null for those two formats rather than a misleading true. The MRV-A and MRV-B visas have neither a personal number check digit nor a composite check digit, so both PersonalNumberCheckDigitValid and CompositeCheckDigitValid are null for a visa; IsValid reflects only the document-number, date-of-birth, and date-of-expiry check digits it actually carries.
Known limitations
- Extended document number (TD1 and TD2). ICAO 9303 lets a document number longer than 9 characters overflow into the adjacent optional data field, signaled by a filler character in place of the document number check digit. Mrz.NET does not implement this mechanism for either TD1 or TD2 (the two formats that carry an optional data field next to the document number). Such a document parses, but
DocumentNumberis truncated to the first 9 characters andValidation.DocumentNumberCheckDigitValidisfalserather than the document being recognized as using extended numbering. TD3 (passports) has no adjacent optional data field, so this mechanism does not apply there. - Date of birth accepts filler, date of expiry does not. ICAO 9303 permits the filler character in date-of-birth positions the issuing authority does not know (for example an approximate birth year for an undocumented minor), so
DateOfBirthaccepts digits or filler and computes/verifies its check digit the same way either way. Date of expiry has no such allowance in the standard and is still validated as strict digits. - Sex marker 'X'. ICAO 9303 itself only defines
M,F, and the filler character for the sex position. Mrz.NET additionally accepts the nonconformantXmarker some real-world issuers print for an unspecified or non-binary sex, mapping it toMrzSex.Unspecifiedthe same as filler. This is a deliberate leniency, not a spec requirement. - Composite check digit is always a digit. Unlike the other check-digit positions, the filler character is never valid in the composite check-digit position; a filler (or any non-digit) there throws
MrzFormatExceptionas a structural error rather than being accepted and reported as an invalid check digit.
Why this exists
Machine-readable-zone parsing is a solved problem in principle: it is a fixed-width text format from a public standard. In practice, the .NET ecosystem's NuGet offerings in this space are either unmaintained, undocumented, or get the check-digit weighting or the character-to-value mapping subtly wrong, which is the one place a KYC pipeline cannot afford to be subtly wrong. Mrz.NET verifies its own check-digit math against ICAO's own published worked examples for all three formats, plus deliberately corrupted variants of each, so a build that passes its test suite is a build you can trust with a passport number.
Zero dependencies, AOT-friendly
Mrz.NET has no runtime dependencies beyond the .NET 8 base class library. It uses no reflection, no dynamic code generation, and no System.Text.Json or regular expressions; parsing is plain string and span slicing. It is fully compatible with Native AOT and trimming.
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.