Error codes and localized messages
When a value is invalid, validate() says why, in four fields of error:
code: what kind of error it is, one of seven codes, for your code to branch on;rule: the ID of the SPEC.md rule that failed, such asDNI-2;message: a sentence for the user, in the locale you pass;expected: for a wrong control character only, the right one, for a “did you mean…?” hint.
import { validate } from "nif-dni-nie-cif-validation";
validate("12345678A").error;// { code: "INVALID_CONTROL_CHARACTER", rule: "DNI-2", expected: "Z",// message: 'The control character is not correct: for this DNI it should be "Z".' }validate() checks the value step by step and stops at the first failure, so an invalid value has exactly one error. Its type and normalized are set as soon as the format is recognisable, even when the control character is wrong.
Every error code
Section titled “Every error code”error.code |
When |
|---|---|
NOT_A_STRING |
The value is not a string (null, a number, an object). It is never converted to one |
EMPTY |
The value is empty, or only spaces and separators |
INVALID_LENGTH |
The value has the wrong number of characters for its type |
INVALID_FORMAT |
A character that the format doesn’t allow: a wrong first character, a letter where a digit goes (or a digit where the check letter goes), or an ES prefix without allowVatPrefix |
INVALID_CONTROL_CHARACTER |
The format is right, but the check letter or digit is wrong; expected has the right one |
UNSUPPORTED_TYPE |
A valid document of a type that types leaves out |
PLACEHOLDER |
A placeholder number (00000000T, 00000001R, 99999999R, X0000000T) with rejectPlaceholders: true |
The last two only happen with an option: with the default options, a valid document is always valid.
Every rule, and its message
Section titled “Every rule, and its message”The table below lists, for each code, every SPEC rule it can cite and the message that comes with it, in the language of this page. The messages come from the library itself, so they are exactly what your users see.
| Rule | Example | Message |
|---|---|---|
Code: NOT_A_STRING | ||
| INPUT-1 | null | The value must be text. |
Code: EMPTY | ||
| INPUT-2 | " - " | Enter a NIF, NIE or CIF. |
Code: INVALID_LENGTH | ||
| DNI-1 | "1234567890" | A DNI has 9 characters: 8 digits and a letter. |
| KLM-1 | "K123456" | A K/L/M NIF has 9 characters: K, L or M, 7 digits and a letter. |
| NIE-1 | "X123456" | An NIE has 9 characters: X, Y or Z, 7 digits and a letter. |
| NIE-3 | "X11234567L" | Only old NIEs have 10 characters: X, a 0, 7 digits and a letter. |
| CIF-1 | "B123456" | A NIF of a legal person or entity (CIF) has 9 characters: a letter, 7 digits and a control character. |
| VAT-1 | "ES", { allowVatPrefix: true } | A Spanish VAT number is ES followed by a 9-character NIF. |
Code: INVALID_FORMAT | ||
| NIF-1 | "T1234567A" | This is not a NIF, NIE or CIF: it must start with a digit or a valid letter. |
| VAT-1 | "ES12345678Z" | Enter the NIF without the ES prefix. |
| DNI-1 | "123456789" | A DNI is 8 digits followed by a letter. |
| KLM-1 | "K12345678" | A K/L/M NIF starts with K, L or M, followed by 7 digits and a letter. |
| KLM-3 | "K12345A7L" | The 7 characters after K, L or M must be digits. |
| NIE-1 | "X12345678" | An NIE starts with X, Y or Z, followed by 7 digits and a letter. |
| CIF-1 | "B12A45674" | A NIF of a legal person or entity (CIF) consists of a letter, 7 digits and a control character (a digit or a letter). |
Code: INVALID_CONTROL_CHARACTER | ||
| DNI-2 | "12345678A" | The control character is not correct: for this DNI it should be "Z". |
| DNI-3 | "12345678I" | The control character is not correct: for this DNI it should be "Z". |
| KLM-2 | "K1234567A" | The control character is not correct: for this K/L/M NIF it should be "L". |
| NIE-2 | "Y1234567L" | The control character is not correct: for this NIE it should be "X". |
| CIF-3 | "G1234567D" | The control character is not correct: for this NIF of a legal person or entity (CIF) it should be "4". |
| CIF-4 | "B12345675" | The control character is not correct: for this NIF of a legal person or entity (CIF) it should be "4". |
Code: UNSUPPORTED_TYPE | ||
| POLICY-2 | "B12345674", { types: ["DNI", "NIE"] } | This NIF of a legal person or entity (CIF) is not accepted here. |
Code: PLACEHOLDER | ||
| POLICY-1 | "00000000T", { rejectPlaceholders: true } | This number is on the list of known placeholder values, which are not accepted here. |
The rule tells apart cases that share a code. For example, INVALID_CONTROL_CHARACTER cites DNI-3 when the letter is one that never appears (I, Ñ, O, U), CIF-3 when a CIF control is a letter where the key takes a digit (or the other way round), and CIF-4 when it is the right kind but the wrong value:
import { validate } from "nif-dni-nie-cif-validation";
validate("12345678I").error?.rule; // "DNI-3"validate("B1234567A").error?.rule; // "CIF-3": B takes a digitvalidate("B12345675").error?.rule; // "CIF-4": a digit, but the wrong onevalidate("T1234567A").error?.rule; // "NIF-1": no NIF starts with Tvalidate("X11234567L").error?.rule; // "NIE-3": 10 characters, but not the old NIE formChoosing a locale
Section titled “Choosing a locale”English is built in. Every other language is a separate entry point, so a bundle only contains the languages it imports. Import the locale object and pass it as locale:
| Language | Import |
|---|---|
| English (the default) | import { en } from "nif-dni-nie-cif-validation/locales/en" |
| Spanish (español) | import { es } from "nif-dni-nie-cif-validation/locales/es" |
| Catalan (català), also for Valencian | import { ca } from "nif-dni-nie-cif-validation/locales/ca" |
| Basque (euskara) | import { eu } from "nif-dni-nie-cif-validation/locales/eu" |
| Galician (galego) | import { gl } from "nif-dni-nie-cif-validation/locales/gl" |
import { describeCifOrganisation, validate } from "nif-dni-nie-cif-validation";import { eu } from "nif-dni-nie-cif-validation/locales/eu";
validate("12345678A", { locale: eu }).error?.message;// "Kontrol-karakterea ez da zuzena: NAN honetan «Z» izan beharko luke."describeCifOrganisation("B", eu); // "Erantzukizun mugatuko sozietatea"The same locale object works everywhere: as the locale option of validate() and of every schema (zNif({ locale }), vNif({ locale }), yNif({ locale })), and as the second argument of describeCifOrganisation(). It also translates meta.orgDescription, the kind of organisation of a CIF.
To follow the user’s language, map the language tag to a locale, with a fallback:
import { type NifLocale, validate } from "nif-dni-nie-cif-validation";import { ca } from "nif-dni-nie-cif-validation/locales/ca";import { en } from "nif-dni-nie-cif-validation/locales/en";import { es } from "nif-dni-nie-cif-validation/locales/es";
const LOCALES: Record<string, NifLocale> = { ca, en, es };const localeFor = (language: string) => LOCALES[language.slice(0, 2)] ?? en;const locale = localeFor("ca-ES"); // for example navigator.language
validate("12345678A", { locale }).error?.message;// "El caràcter de control no és correcte: per a aquest DNI hauria de ser «Z»."validate("B12345674", { locale }).meta?.orgDescription; // "Societat de responsabilitat limitada"Catalan also serves Valencian: the AEAT gives the same organisation names in both, and the messages use no form that differs between them.
Your own messages
Section titled “Your own messages”code and rule are stable, so you can map them to your own texts and keep the library’s message as the fallback:
import { type NifErrorCode, validate } from "nif-dni-nie-cif-validation";
const MESSAGES: Partial<Record<NifErrorCode, string>> = { EMPTY: "Please enter your tax ID.", UNSUPPORTED_TYPE: "Please enter a personal tax ID, not a company's.",};
const error = validate("B12345674", { types: ["DNI", "NIE"] }).error;error && (MESSAGES[error.code] ?? error.message); // "Please enter a personal tax ID, not a company's."A locale is a plain object (NifLocale), so you can also write one for another language. A locale that lacks a text gives English for that text. The terminology and the sources of every translated organisation name are in docs/translations.md.
In the schemas
Section titled “In the schemas”The Zod, Valibot and Yup schemas give the same message, and put the code and the rule where each library allows:
| Library | Message | Code and rule |
|---|---|---|
| Zod | the issue’s message |
the issue’s params: { code, rule, expected? } |
| Valibot | the issue’s message |
the issue itself: issue.code, issue.rule, issue.expected |
| Yup | the ValidationError’s message |
the error’s params: { code, rule, expected? } |
See Validating forms for examples.