Skip to content

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 as DNI-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.

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.

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.

Every error code of validate(), each SPEC rule it can cite, an input that gives it, and its message in English
RuleExampleMessage
Code: NOT_A_STRING
INPUT-1nullThe 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 digit
validate("B12345675").error?.rule; // "CIF-4": a digit, but the wrong one
validate("T1234567A").error?.rule; // "NIF-1": no NIF starts with T
validate("X11234567L").error?.rule; // "NIE-3": 10 characters, but not the old NIE form

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.

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.

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.