Links: GitHub · npm · API documentation
Short answer: install lanka-validations from npm and call isValidNic('921234567V') from lanka-validations/nic. It returns true for valid old-format (9 digits plus V or X) and new-format (12 digits) Sri Lankan NIC numbers. The same package validates phone numbers, passports, driving licences, vehicle plates, LKR amounts, postal codes, CBSL bank codes, and TIN/VAT numbers. It has no runtime dependencies.
npm install lanka-validations
import { isValidNic, parseNic } from 'lanka-validations/nic';
isValidNic('921234567V'); // true
parseNic('921234567V')?.dobString; // '1992-05-02'
The rest of this guide explains how Sri Lankan identifiers are structured, how to validate each one, how to plug the checks into Zod, React Hook Form, and Express, and what format validation can and cannot prove.
What is lanka-validations?
lanka-validations is an open-source (MIT) TypeScript and JavaScript library for validating, parsing, and formatting Sri Lankan data. It covers National Identity Card (NIC) numbers, phone numbers, passports, driving licences, vehicle number plates, LKR currency, dates of birth, gender, postal codes, CBSL bank codes, TIN/VAT numbers, provinces, and districts. It runs in Node.js 20+, browsers, Deno, and Bun.
Most Sri Lankan web forms ask for some of these values: sign-up pages, KYC onboarding, checkout, payroll, and government-service portals. Without a library, every team writes its own NIC regex and phone-prefix list, and those copies drift out of date. lanka-validations keeps the rules and reference tables in one tested place, and records the official source for each table.
Key facts:
| Property | Value |
|---|---|
| Runtime dependencies | None |
| Runtimes | Node.js 20+, browsers, Deno, Bun |
| Module formats | ESM and CommonJS, with TypeScript types |
Size of isValidNic
|
About 1 kB gzipped (enforced in CI) |
| Typical validation time | About 0.4 to 0.9 µs for NIC and phone checks (local benchmark) |
| Languages | English, Sinhala, and Tamil for currency words, gender, regions, errors |
| Licence | MIT |
How a Sri Lankan NIC number is structured
A Sri Lankan NIC number encodes the holder's birth year, birthday, and gender. There are two formats, and both are still in circulation:
| Format | Example | Layout |
|---|---|---|
| Old (before 2016) | 921234567V |
2-digit year, 3-digit day, 3-digit serial, check digit, V or X |
| New (12 digits) | 199212304567 |
4-digit year, 3-digit day, 4-digit serial, check digit |
The day number counts from 1 January using a 366-day year, so day 123 is 2 May. For women, 500 is added to the day, so a woman born on the same day has 623 instead of 123. Converting old to new adds 19 before the year and a 0 before the serial.
That is why a plain regex is not enough. /^\d{9}[VX]$/ accepts 929994567V, which has day 999 and cannot be a real birthday. A real validator has to decode the day number, check it against the calendar, and reject birth dates in the future.
Validate and parse an NIC number
isValidNic returns a boolean. parseNic returns the decoded details, or null if the number is invalid.
import {
isValidNic,
parseNic,
convertOldToNewNic,
convertNewToOldNic,
formatNic,
} from 'lanka-validations/nic';
isValidNic('921234567V'); // true
isValidNic('199212304567'); // true
isValidNic('9212345678'); // false
const info = parseNic('921234567V');
info?.year; // 1992
info?.gender; // 'male'
info?.dobString; // '1992-05-02'
info?.isVoter; // true
info?.newNic; // '199212304567'
convertOldToNewNic('921234567V'); // '199212304567'
convertNewToOldNic('199212304567'); // '921234567V'
formatNic('199212304567', 'spaced-new'); // '1992 123 0456 7'
When you need an error message, use validateNic. It returns either { isValid: true, data } or { isValid: false, error, errors, code }, so TypeScript narrows the type for you:
import { validateNic } from 'lanka-validations/nic';
const result = validateNic('92123456V');
if (result.isValid) {
result.data.dobString;
} else {
result.code; // 'NIC_INVALID_FORMAT'
result.error; // 'Invalid NIC format. Must be 9 digits followed by V/X or 12 digits.'
}
The code is stable across versions, so you can branch on it or translate it without matching on English text.
Validate Sri Lankan phone numbers
isValidMobile accepts Sri Lankan mobile numbers in local or +94 form, with spaces, dashes, brackets, dots, or slashes. parsePhone tells you the line type and the operator that the prefix was originally allocated to. formatPhone converts to E.164, national, or international format.
import { isValidMobile, isValidLandline, parsePhone, formatPhone } from 'lanka-validations/phone';
isValidMobile('077 123 4567'); // true
isValidLandline('0112345678'); // true (Colombo)
const phone = parsePhone('+94 77 123 4567');
phone?.operator; // 'Dialog'
phone?.normalized; // '0771234567'
phone?.type; // 'mobile'
formatPhone('0771234567', 'e164'); // '+94771234567'
formatPhone('0771234567', 'international'); // '+94 77 123 4567'
Store phone numbers in E.164 (+94771234567). It is the format SMS gateways and most APIs expect, and it removes duplicates caused by spacing differences.
Two details matter in production. First, isValidPhone also accepts short codes such as 119 (police emergency). For a "contact number" field, use isValidMobile or isValidContactPhone, which reject them. Second, the operator comes from the prefix's original allocation. Because of number portability, a 077 number may now be on a different network.
Parse and format LKR currency, with lakh grouping and words
Sri Lankan amounts are written in several ways: Rs. 1,500.50, LKR 1,50,000, or with the Sinhala රු. and Tamil ரூ. symbols. parseLKR reads all of them. formatLKR writes either Western grouping or South Asian lakh grouping, and lkrToWords writes the amount in words for cheques and receipts.
import { parseLKR, formatLKR, lkrToWords, parseLKRMinorUnits } from 'lanka-validations/currency';
parseLKR('Rs. 1,500.50'); // 1500.5
parseLKR('LKR 1,50,000'); // 150000
formatLKR(1500000); // 'Rs. 1,500,000.00'
formatLKR(1500000, { lakhScale: true }); // 'Rs. 15,00,000.00'
lkrToWords(1250); // 'One Thousand Two Hundred Fifty Rupees Only'
lkrToWords(1500.5, { language: 'si' }); // 'රුපියල් එක්දහස් පන්සියයයි ශත පනහයි'
lkrToWords(1500.5, { language: 'ta' }); // 'ஆயிரத்து ஐந்நூறு ரூபாய் ஐம்பது சதம்'
parseLKRMinorUnits('Rs. 1,500.50'); // 150050n (cents, as a bigint)
parseLKR returns a JavaScript number, which is fine for display and range checks. For balances, invoices, and anything you add up, use parseLKRMinorUnits, which returns exact cents as a bigint and avoids floating-point rounding errors.
Other Sri Lankan identifiers and reference data
The library uses the same pattern for every identifier: isValidX for a boolean, validateX for a result with an error code, and parseX or a lookup function for details.
| Data | Example call | Result |
|---|---|---|
| Passport | isValidPassport('N1234567') |
true |
| Driving licence | isValidDrivingLicense('B1234567') |
true |
| Vehicle plate | parseVehiclePlate('wp cab 1234')?.formatted |
'WP CAB-1234' |
| Postal code to district | lookupPostalCode('20000')?.district |
'Kandy' |
| CBSL bank code | getBankByCode('7010')?.name |
'Bank of Ceylon' |
| Taxpayer Identification No. | isValidTIN('123456789') |
true |
| District in Sinhala or Tamil | isValidDistrict('කොළඹ') |
true |
| Gender in any language | normalizeGender('ஆண்') |
'male' |
An NIC is not a TIN. Individuals register with the Inland Revenue Department using their NIC and receive a separate TIN, so isValidTIN('921234567V') returns false. Validate NIC fields with isValidNic.
Check that a form's date of birth and gender match the NIC
KYC forms often ask for the NIC, date of birth, and gender separately. Because the NIC encodes the other two, you can catch typos and mismatched entries before they reach a manual review queue:
import { matchesNicDob } from 'lanka-validations/dob';
import { matchesNicGender } from 'lanka-validations/gender';
const nic = '921234567V'; // encodes a man born on 1992-05-02
matchesNicDob('1992-05-02', nic); // true
matchesNicDob('1995-01-01', nic); // false
matchesNicGender('male', nic); // true
matchesNicGender('female', nic); // false
Validate a whole form with the schema validator
For forms with several Sri Lankan fields, createSchemaValidator validates the whole object in one call, runs cross-field rules, and infers the parsed output type in TypeScript:
import { createSchemaValidator, rules } from 'lanka-validations/schema';
const signUp = createSchemaValidator({
nic: rules.nic(),
phone: rules.mobile(),
dob: [rules.dob({ minAge: 18 }), rules.matchesNicDob('nic')],
gender: [rules.gender(), rules.matchesNicGender('nic')],
postalCode: rules.postalCode(),
});
const result = signUp.validate({
nic: '921234567V',
phone: '123',
dob: '1992-05-02',
gender: 'male',
postalCode: '00100',
});
if (!result.isValid) {
result.errorCodes; // { phone: 'PHONE_UNSUPPORTED' }
result.errors.phone; // 'Phone number prefix or short code is not in the bundled reference data.'
}
Show errors in Sinhala or Tamil
Every failure carries a stable code, so you can show messages in the user's language. localizeValidationResult translates a single result, and localizeSchemaResult translates a whole form:
import { validateNic } from 'lanka-validations/nic';
import { localizeValidationResult } from 'lanka-validations/i18n';
localizeValidationResult(validateNic('abc'), 'si').error;
// 'ජාතික හැඳුනුම්පත් ආකෘතිය වලංගු නොවේ.'
Use it with Zod, React Hook Form, and Express
The isValidX functions return plain booleans, so they work as custom checks in any validation library.
Zod
import { z } from 'zod';
import { isValidNic } from 'lanka-validations/nic';
import { isValidMobile } from 'lanka-validations/phone';
const SignUp = z.object({
nic: z.string().refine(isValidNic, { message: 'Enter a valid Sri Lankan NIC number' }),
phone: z.string().refine(isValidMobile, { message: 'Enter a Sri Lankan mobile number' }),
});
React Hook Form
import { useForm } from 'react-hook-form';
import { isValidNic } from 'lanka-validations/nic';
const { register } = useForm<{ nic: string }>();
<input {...register('nic', { validate: (value) => isValidNic(value) || 'Invalid NIC number' })} />;
Express
import express from 'express';
import { validateNic } from 'lanka-validations/nic';
const app = express();
app.use(express.json());
app.post('/customers', (req, res) => {
const nic = validateNic(req.body.nic);
if (!nic.isValid) {
res.status(400).json({ code: nic.code, error: nic.error });
return;
}
res.json({ dateOfBirth: nic.data.dobString, gender: nic.data.gender });
});
Run the same checks on the server even when the browser already ran them. Client-side validation helps the user; server-side validation protects your data.
Safe on untrusted input
Form fields and API bodies can contain anything. Every validator in lanka-validations accepts unknown and never throws:
isValidNic(null); // false
isValidNic({}); // false
isValidNic('x'.repeat(2000)); // false
Objects and arrays are rejected without calling their toString. Strings longer than 1,024 characters are rejected rather than truncated, which keeps validation time bounded. Property-based tests check these guarantees across thousands of generated inputs. The one deliberate exception is lkrToWords, which throws a RangeError for amounts too large to write in words.
When you log identifiers, mask them:
import { maskNic, maskPhone } from 'lanka-validations/privacy';
maskNic('921234567V'); // '******567V'
maskPhone('0771234567'); // '******4567'
What format validation does not prove
A valid format is not proof of identity. lanka-validations checks that a value is well-formed and consistent with published rules. It does not check that:
- an NIC, passport, or licence number was issued, or belongs to the person entering it;
- a phone number is assigned and reachable;
- a bank account exists or is open;
- a TIN is registered with the IRD.
NIC check digits are also not verified, because the Department for Registration of Persons has not published the algorithm. For KYC and payments, treat these checks as input hygiene, then confirm identity and accounts with the issuing authority or a verification provider.
Where the reference data comes from
Bank codes, postal codes, phone prefixes, and district names change over time, so each bundled table records its official source and the date it was checked:
| Table | Official source |
|---|---|
| Provinces and districts | Department of Census and Statistics (Census 2024) |
| Bank codes | Central Bank of Sri Lanka (CBSL) bank reference manual |
| Postal codes | Sri Lanka Post code book |
| Phone prefixes, short codes | TRCSL National Numbering Plan |
| Driving licence classes | Department of Motor Traffic (DMT) |
| TIN and VAT | Inland Revenue Department (IRD) registration guidelines |
getReferenceDataMetadata() returns the source, as-of date, coverage, and known gaps for each table at runtime, so you can show or log which data version made a decision. The full list, with links to each source, is in the README on GitHub.
When to use something else
lanka-validations is for Sri Lanka-specific data. For phone numbers from many countries, use libphonenumber-js. For generic checks such as email addresses and URLs, use Zod or a general-purpose validator. The libraries combine well: Zod for the form shape, lanka-validations for the Sri Lankan fields.
| Approach | Old and new NIC | Decodes DOB and gender | LKR lakh format and words | Sinhala and Tamil | Sources recorded |
|---|---|---|---|---|---|
| Hand-written regex | Partly | No | No | No | No |
libphonenumber-js |
No (phones only) | No | No | No | Not applicable |
lanka-validations |
Yes | Yes | Yes | Yes | Yes |
Frequently asked questions
How do I validate a Sri Lankan NIC number in JavaScript?
Install lanka-validations and call isValidNic(value) from lanka-validations/nic. It returns true for valid old-format (9 digits plus V or X) and new-format (12 digits) numbers. Use validateNic(value) to also get the date of birth, age, gender, and an error code when the number is invalid.
How do I get the date of birth from a Sri Lankan NIC?
Call parseNic(nic). It returns dobString (for example '1992-05-02'), dob as a UTC Date, age, and gender. The birth year and the day of the year are encoded in the number. For women, 500 is added to the day, so 927234567V decodes to a woman born on 10 August 1992.
How do I convert an old NIC to the new 12-digit format?
Use convertOldToNewNic('921234567V'), which returns '199212304567'. It prefixes 19 to the year and adds a 0 before the serial number. convertNewToOldNic converts back for people born between 1900 and 1999.
Does it work in React, Next.js, Deno, and Bun?
Yes. It has no runtime dependencies and uses no Node.js-only APIs, so it runs in browsers, React and Next.js (client and server components), Node.js 20+, Deno, and Bun. Import from subpaths such as lanka-validations/nic to keep the browser bundle small.
Does validating an NIC confirm someone's identity?
No. It checks the number's format and whether the encoded birth date is a real date in the past. It cannot tell whether the number was issued or who it belongs to. For KYC, verify identity with the issuing authority or a verification provider.
Is the bank and postal code data up to date?
Each table records its official source and the date it was last checked. Call getReferenceDataMetadata() to read them at runtime. If you find an outdated entry, open an issue on GitHub with a link to the current official source.
Get started
npm install lanka-validations
- GitHub (source code, issues, contributions): github.com/mmsalmanfaris/lanka-validations
- npm package: npmjs.com/package/lanka-validations
- API documentation: mmsalmanfaris.github.io/lanka-validations
- For AI coding assistants: llms.txt
lanka-validations is maintained by M.M. Salman Faris. If it saves you time, star the repository on GitHub. Bug reports and pull requests are welcome.
Top comments (0)