Zod schemas
zUz — ready-made zod (v4) schemas for phone, PINFL, passport, card, money and region, with Uzbek error messages.
@akhatmirzo/core/zod provides ready-made zod (v4) schemas for forms. Each
one builds on the isValidX / parseX functions from @akhatmirzo/core (the validation logic
lives in one place), error messages are in Uzbek, and the output is a clean value ready for
your API: phone "998901234567", money 1250000.
pnpm add zodzod is an optional peer dependency: the main @akhatmirzo/core import never pulls it in.
import { zUz } from '@akhatmirzo/core/zod';
import { z } from 'zod';
const schema = z.object({
phone: zUz.phone(),
pinfl: zUz.pinfl(),
amount: zUz.money({ min: 1000 }),
});
schema.parse({ phone: '+998 90 123-45-67', pinfl: '31503 90123 4567', amount: '1 250 000' });
// { phone: '998901234567', pinfl: '31503901234567', amount: 1250000 }Natija: "998901234567"
'use client';
import { zUz } from '@akhatmirzo/core/zod';
import { Field, Input, Select } from '@akhatmirzo/ui';
import { useState } from 'react';
const schemas = {
phone: zUz.phone(),
pinfl: zUz.pinfl(),
passport: zUz.passport(),
carPlate: zUz.carPlate(),
card: zUz.card(),
money: zUz.money({ min: 1000 }),
password: zUz.password(),
};
type Kind = keyof typeof schemas;
export default function CoreZod() {
const [kind, setKind] = useState<Kind>('phone');
const [text, setText] = useState('+998 90 123 45 67');
const result = schemas[kind].safeParse(text);
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Field label="Sxema">
<Select value={kind} onValueChange={(v) => setKind(v as Kind)}>
{Object.keys(schemas).map((k) => (
<Select.Item key={k} value={k}>{`zUz.${k}()`}</Select.Item>
))}
</Select>
</Field>
<Field
label="Kiritilgan qiymat"
error={result.success ? undefined : result.error.issues[0]?.message}
>
<Input value={text} onChange={(e) => setText(e.target.value)} />
</Field>
<p className="text-sm text-text-muted">
Natija:{' '}
<code className="text-text">{result.success ? JSON.stringify(result.data) : '—'}</code>
</p>
</div>
);
}
With Form: useForm({ resolver: zodResolver(schema) }) —
FormField shows the error message and onSubmit receives the cleaned values.
Common rules
- Empty values (
'',null,undefined) produce therequirederror, bad values produceinvalid. Spaces and dashes are ignored. - Override messages with the
messagesoption (pass only the keys you need):
zUz.phone({ messages: { invalid: 'Mobile numbers only' } });
zUz.money({ min: 1000, messages: { min: (min) => `At least ${min} so'm` } });- Optional fields. An empty form field is
'', notundefined, so use.or(z.literal(''))instead of.optional():zUz.inn().or(z.literal('')). - Input and output types differ:
z.input<typeof schema>is the form value (number | string | nullformoney),z.output<typeof schema>is whatonSubmitgets (number).
Schemas
| Schema | Check | Output | Default message (required / invalid) |
|---|---|---|---|
zUz.phone() | parsePhone(v).valid | '998901234567' | Telefon raqamini kiriting / Telefon raqami noto'g'ri |
zUz.pinfl({ checksum }) | isValidPinfl(v, { checksum }) | '31503901234567' | JShShIRni kiriting / JShShIR noto'g'ri |
zUz.inn() | isValidInn | '123456789' | STIRni kiriting / STIR noto'g'ri |
zUz.passport() | isValidPassport | 'AA1234567' | Pasport seriya va raqamini kiriting / … noto'g'ri |
zUz.carPlate() | isValidCarPlate | '01A123BC' | Avtomobil raqamini kiriting / Avtomobil raqami noto'g'ri |
zUz.bankAccount() | isValidBankAccount (20 digits) | '20208000900012345001' | Hisob raqamini kiriting / … 20 ta raqamdan iborat bo'lishi kerak |
zUz.mfo() | isValidMfo (5 digits) | '00014' | MFOni kiriting / MFO 5 ta raqamdan iborat bo'lishi kerak |
zUz.card() | isValidCardNumber (16 digits + Luhn) | '4111111111111111' | Karta raqamini kiriting / Karta raqami noto'g'ri |
zUz.email() | isValidEmail | trimmed | Email manzilini kiriting / Email manzili noto'g'ri |
zUz.password({ minScore }) | passwordStrength(v).score >= minScore | unchanged | Parolni kiriting / Parol juda oddiy — … |
zUz.money({ min, max }) | number or "1 250 000" text | number | Summani kiriting / Summa noto'g'ri |
zUz.region() | an id from UZ_REGIONS | UzRegionId | Hududni tanlang / Hudud noto'g'ri |
zUz.phone
function phone(options?: { messages?: Partial<{ required: string; invalid: string }> }): ZodType;An Uzbek mobile number: 9 digits with a known operator code. Accepts +998, spaces, brackets
and dashes. Works with PhoneInput.
zUz.phone().parse('+998 90 123-45-67'); // '998901234567'
zUz.phone().parse('901234567'); // '998901234567'
zUz.phone().safeParse('+998 12 123 45 67').success; // false — unknown operatorzUz.pinfl
function pinfl(options?: { checksum?: boolean; messages?: Partial<ZUzMessages> }): ZodType;PINFL: 14 digits, the first is 3–6, digits 2–7 are a real date. checksum: true also checks
the control digit (the algorithm is unverified, see identifiers).
zUz.pinfl().parse('31503 90123 4567'); // '31503901234567'
zUz.pinfl().safeParse('33102901234567').success; // false — there is no Feb 31zUz.inn · zUz.passport · zUz.carPlate · zUz.bankAccount · zUz.mfo
zUz.inn().parse('123 456 789'); // '123456789'
zUz.passport().parse('aa 123-45-67'); // 'AA1234567'
zUz.carPlate().parse('01 a 123 bc'); // '01A123BC'
zUz.carPlate().parse('30 123 abc'); // '30123ABC'
zUz.bankAccount().parse('2020 8000 9000 1234 5001'); // '20208000900012345001'
zUz.mfo().parse('00014'); // '00014' — leading zeros are keptThe output is upper-cased with spaces and dashes removed — the same shape the document inputs
emit from onValueChange.
zUz.card
A card number: exactly 16 digits and a valid Luhn checksum. The output is digits only.
zUz.card().parse('4111 1111 1111 1111'); // '4111111111111111'
zUz.card().safeParse('4111 1111 1111 1112').success; // false — LuhnzUz.email
zUz.email().parse(' sarvar@mail.uz '); // 'sarvar@mail.uz'zUz.password
function password(options?: {
minScore?: 0 | 1 | 2 | 3 | 4; // default 2 — "O'rtacha"
messages?: Partial<{ required: string; weak: string }>;
}): ZodType;Fails with weak when the passwordStrength score is below minScore. The
value is not changed.
zUz.password().safeParse('parol').success; // false
zUz.password({ minScore: 3 }).parse('Parol123!'); // 'Parol123!'zUz.money
function money(options?: {
min?: number;
max?: number;
messages?: Partial<{
required: string;
invalid: string;
min: string | ((min: number) => string);
max: string | ((max: number) => string);
}>;
}): ZodType<number, number | string | null | undefined>;Accepts a number or text: spaces (including non-breaking ones) and a trailing so'm / UZS are
stripped. The decimal separator is a comma or a dot; when both appear, the last one is the decimal
separator and the others group thousands ('1,250,000.50', '1.250.000,50'). An empty value
fails with the required message. Works directly with the number | null value of
MoneyInput.
zUz.money().parse('1 250 000'); // 1250000
zUz.money().parse("1 250 000 so'm"); // 1250000
zUz.money().parse('1250,5'); // 1250.5
zUz.money().parse('1,250,000.50'); // 1250000.5
zUz.money({ min: 1000 }).safeParse(500).error?.issues[0]?.message; // "Eng kam summa — 1 000 so'm"zUz.region
A region id from UZ_REGIONS. Input is a string (an empty form value '' too), output is
UzRegionId. Works with RegionSelect.
zUz.region().parse('samarkand'); // 'samarkand'
zUz.region().safeParse('').error?.issues[0]?.message; // 'Hududni tanlang'