Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. z.coerce tuzog'i
- 2.1 Qulay, lekin noaniq
- 3. Ro'yxat query'si — to'liq sxema
- 3.1 Express 5 query'sining shakli
- 3.2 Sxema
- 3.3 Qism-qism
- 4. discriminatedUnion — variantli tana
- 4.1 Eksport formati
- 5. Xato matnlari: lokal va prettifyError
- 5.1 O'zbekcha lokal
- 5.2 prettifyError — qayerda
- 6. z.toJSONSchema — sxemadan hujjat
- 6.1 Bitta sxema — uch foyda
- 6.2 Kanondagi skript
- 7. zod/mini — serverda kerakmi?
- 7.1 Funksional API
- 7.2 Kanon o'lchovi
- 8. API qadami: Zod sxemalari (holat 34, 2-qism)
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): fields parametri
- 3-mashq (qiyin): to'lov usuli
- 4-mashq: API qadami
- 11. Real ishda
- Xulosa
- Manbalar
Zod 4 serverda: query sxemasi, discriminatedUnion, prettifyError, toJSONSchema va zod/mini
Qisqacha: Serverda Zod'ning vazifasi — chegaradagi har qiymatni tiplangan, tozalangan qiymatga aylantirish. Query parametrlar satr (yoki satrlar massivi) bo'lib keladi, shuning uchun
z.coerceo'rniga aniq shakl tekshiruvi +transform+pipeishlatiladi (z.coerce.boolean()"false"nitrueqiladi). Variantli tana —z.discriminatedUnion. Zod 4 da o'zbekcha lokal bor:z.config(z.locales.uz()). Odam o'qiydigan xato matni —z.prettifyError, hujjat uchun JSON Schema —z.toJSONSchema(transform'lar ifodalanmaydi).zod/minibrauzer bundle'i uchun kichik, lekin serverda o'lchovda klassik Zod'dan ~2.5 marta sekinroq chiqdi.
Bu darsda
z.coercenega query uchun xavfli ekanini ko'rasiz va aniq sxema yozasiz.- Express 5 query'si uchun
union→transform→pipe,ctx.issuesvaz.NEVER,.default(), obyekt darajasidagirefineni ishlatasiz. discriminatedUnionbilan variantli tanani tekshirasiz va TypeScript'da toraytirasiz.- Lokal,
prettifyErrorvatoJSONSchemani serverda qayerda ishlatishni bilasiz. - Klassik Zod va
zod/minini o'lchov bilan solishtirib, serverda qaysi birini tanlashni asoslaysiz.
Oldin bilishingiz kerak: Validatsiya: nima uchun, qayerda va qanday, Zod va turni sxemadan chiqarish, Zod chuqurroq: coerce, refine va transform, Filtrlash, saralash va maydon tanlash.
1. Nega bu kerak?
Oldingi darsda tekshirib(...) middleware'ini yozdik va oddiy sxemalar qo'shdik: yangi vazifa, ID. Lekin vazifalar-api ning eng murakkab kirishi hali qo'lda tekshiriladi — ro'yxat query'si: bajarildi, teg (takrorlanadi), q, sort=-yaratildi,matn, fields, limit, offset yoki cursor, jami. src/vazifalar/sorov.ts da bu 120 qatorli if lar to'plami.
Zod'ni TypeScript qismida va formalarda ishlatgansiz. Formada qiymatlar ko'pincha allaqachon to'g'ri turda keladi. Serverda vaziyat boshqacha: query'da hammasi satr, bir kalit ikki marta kelishi mumkin, noma'lum parametr xato bo'lishi kerak va sukut qiymatlar bor.
O'xshatish: bojxona deklaratsiyasi. Yo'lovchi blankni qo'lda to'ldiradi — raqamlar ham matn ko'rinishida yozilgan. Bojxonachi har katakni o'qiydi, "1 000" ni songa aylantiradi, bo'sh katakka "yo'q" deb yozadi, noma'lum katakni (blankda yo'q qator) qabul qilmaydi. Sxema — shu bojxonachi.
2. z.coerce tuzog'i
2.1 Qulay, lekin noaniq
z.coerce.number() qiymatni Number(x) bilan, z.coerce.boolean() esa Boolean(x) bilan aylantiradi. Oldingi darsda ko'rgan JS tuzoqlari bu yerda ham ishlaydi. Blok Node'da Zod 4 bilan tekshiriladi:
import { z } from "zod";
const Coerced = z.object({
limit: z.coerce.number(),
bajarildi: z.coerce.boolean(),
});
console.log(Coerced.parse({ limit: "", bajarildi: "false" }));
console.log(Coerced.parse({ limit: "1e3", bajarildi: "0" }));
const Strict = z.object({
limit: z.string().regex(/^\d{1,3}$/).transform(Number),
bajarildi: z.enum(["true", "false"])
.transform((x) => x === "true"),
});
console.log(Strict.parse({ limit: "20", bajarildi: "false" }));
const bad = Strict.safeParse({ limit: "", bajarildi: "0" });
console.log(bad.error?.issues.length);Konsolda:
{ limit: 0, bajarildi: true }
{ limit: 1000, bajarildi: true }
{ limit: 20, bajarildi: false }
2?bajarildi=false — true! ?limit= — 0. Ikkalasi ham xatosiz o'tdi. Aniq sxema esa "false" ni to'g'ri false qiladi, bo'sh va noto'g'ri qiymatlarni esa ikki xato bilan rad etadi.
z.coerce formada (<input type="number"> qiymati) yoki env o'zgaruvchilarida (PORT=4000) qulay. Query uchun qoida: avval shakl (regex yoki enum), keyin transform.
3. Ro'yxat query'si — to'liq sxema
3.1 Express 5 query'sining shakli
Express 5 sukut bo'yicha "simple" parser ishlatadi (req obyekti): har qiymat — string, takrorlangan kalit — string[]. Obyekt (?filtr[a]=1) yasalmaydi. Demak har maydon sxemasi shu ikki shaklni hisobga olishi kerak:
| Maydon | Massiv kelsa |
|---|---|
teg |
to'g'ri — bir nechta teg (hammasi bo'lishi kerak) |
limit, q, sort … |
xato — "bir marta berilishi kerak" |
3.2 Sxema
Quyidagi blok kanondagi RoyxatQuerySxemasi ning qisqartirilgan nusxasi (inglizcha nomlar bilan). Har qismni pastda alohida tushuntiramiz:
import { z } from "zod";
const SORTABLE = ["id", "matn", "yaratildi"] as const;
type Field = (typeof SORTABLE)[number];
type Sort = { field: Field; desc: boolean };
const One = (name: string) =>
z.string({ error: `${name} bir marta berilishi kerak` });
const normalizeTags = (t: string | string[]) => {
const list = Array.isArray(t) ? t : [t];
const clean = list.map((x) => x.replace(/^#/, "").toLowerCase());
return [...new Set(clean)];
};
const Tag = z.string().regex(/^[\p{L}\p{N}_]{1,50}$/u);
const parseSort = (x: string, ctx: z.RefinementCtx): Sort[] => {
const out: Sort[] = [];
for (const part of x.split(",")) {
const desc = part.startsWith("-");
const field = desc ? part.slice(1) : part;
if (!(SORTABLE as readonly string[]).includes(field)) {
ctx.issues.push({
code: "custom", input: x,
message: `sort: noma'lum maydon "${field}"`,
});
return z.NEVER;
}
out.push({ field: field as Field, desc });
}
return out;
};
const ListQuery = z
.strictObject(
{
teg: z.union([z.string(), z.array(z.string())])
.transform(normalizeTags)
.pipe(z.array(Tag).max(10))
.optional(),
sort: One("sort").transform(parseSort).optional(),
limit: One("limit")
.regex(/^\d{1,6}$/, { error: "limit — 1..100" })
.transform(Number)
.refine((n) => n >= 1 && n <= 100, {
error: "limit — 1..100",
})
.default(20),
offset: One("offset").regex(/^\d{1,6}$/).transform(Number)
.optional(),
cursor: One("cursor").max(1000).optional(),
},
{
error: (i) => i.code === "unrecognized_keys"
? `noma'lum parametr: ${i.keys.join(", ")}`
: undefined,
},
)
.refine((q) => q.offset === undefined || q.cursor === undefined, {
error: "offset va cursor birga berilmaydi", path: ["cursor"],
});
const show = (r: z.ZodSafeParseResult<unknown>) => {
if (r.success) return console.log(r.data);
for (const i of r.error.issues) {
console.log(` ${i.path.join(".") || "(ildiz)"} | ${i.message}`);
}
};
show(ListQuery.safeParse({
teg: ["#UY", "uy", "Ish"], sort: "-yaratildi,matn",
}));
show(ListQuery.safeParse({
sort: "parol", limit: "0", sahifa: "2",
}));
show(ListQuery.safeParse({ offset: "0", cursor: "abc" }));
show(ListQuery.safeParse({ limit: ["10", "20"] }));Konsolda:
{
teg: [ 'uy', 'ish' ],
sort: [
{ field: 'yaratildi', desc: true },
{ field: 'matn', desc: false }
],
limit: 20
}
sort | sort: noma'lum maydon "parol"
limit | limit — 1..100
(ildiz) | noma'lum parametr: sahifa
cursor | offset va cursor birga berilmaydi
limit | limit bir marta berilishi kerak3.3 Qism-qism
teg: union → transform → pipe. Kirish — satr yoki massiv (union). transform ikkalasini massivga keltiradi. U # ni olib tashlaydi, kichik harf qiladi va takrorni yo'qotadi: ["#UY","uy","Ish"] → ["uy","ish"]. pipe esa natijani yana bitta sxemadan o'tkazadi: har teg — harf/raqam, 10 tadan ko'p emas. transform dan keyin tekshirish kerak bo'lsa — pipe.
sort: transform ichida xato. "-yaratildi,matn" → saralash massivi. Noma'lum maydonda transform xato qo'shadi: ctx.issues.push({...}) va z.NEVER qaytaradi. z.NEVER — "bu yerda qiymat yo'q, natija baribir xato" degan belgi; TypeScript uchun ham to'g'ri (never tur). Bu superRefine ning transform ichidagi ko'rinishi: tekshirish va aylantirish bitta joyda, satr ikki marta bo'linmaydi.
sort ning oq ro'yxati — xavfsizlik: sort=parol ruxsat etilmaydi, 26-qismda bu ro'yxat SQL ORDER BY ustunlari bo'ladi (injeksiya).
limit: regex → transform → refine → default. Tartib muhim: avval shakl, keyin son, keyin oraliq. .default(20) — parametr berilmasa, natijada 20. z.output turida limit: number (ixtiyoriy emas!) — handler undefined ni tekshirmaydi.
strictObject + error funksiyasi. Noma'lum kalit (sahifa) — sukut xabari o'rniga o'zimizniki. Funksiya undefined qaytarsa — Zod o'z (yoki lokal) xabarini ishlatadi.
Obyekt darajasidagi refine — bir nechta maydonga bog'liq qoida: offset va cursor birga emas. path: ["cursor"] — xato qaysi joyga yozilishini aytadi (aks holda (ildiz)).
E'tibor bering: refine faqat obyektning qolgan maydonlari yaroqli bo'lsa ishlaydi. Ikkinchi misolda sort va limit xato edi — shuning uchun offset/cursor tekshiruvi umuman ishlamadi (uchinchi misolda alohida ko'rinadi). Kanon bu xatti-harakatni config'da ham qayd etgan: avval bir xato tuzatiladi, keyin ikkinchisi chiqadi.
Tekshirib ko'ring:
?teg=uy&teg=UY&teg=#uyqanday natija beradi??limit=020chi?
Javob
teg: ["uy"] — uchalasi ham uy ga normallashadi va Set takrorni olib tashlaydi. limit: 20 — "020" regex'dan (^\d{1,6}$) o'tadi, Number("020") — 20, oraliqda. Agar 020 ni rad etish kerak bo'lsa, regex ^[1-9]\d*$ bo'lardi — bu loyiha qarori.
4. discriminatedUnion — variantli tana
4.1 Eksport formati
Eksport so'rovi ikki xil: CSV (ajratgich bilan) yoki JSON (ajratgichsiz). Excel o'zbek va rus lokalida ; ni kutadi — shuning uchun tanlov kerak. z.discriminatedUnion — bitta "kalit" maydon (format) qaysi variant ekanini aytadi:
import { z } from "zod";
const Csv = z.strictObject({
format: z.literal("csv"),
ajratgich: z.enum([",", ";"]).default(","),
});
const Json = z.strictObject({ format: z.literal("json") });
const ExportReq = z
.discriminatedUnion("format", [Csv, Json], {
error: 'format — "csv" yoki "json"',
})
.default({ format: "csv", ajratgich: "," });
type ExportReq = z.output<typeof ExportReq>;
function describe(r: ExportReq): string {
switch (r.format) {
case "csv":
return `CSV, ajratgich "${r.ajratgich}"`;
case "json":
return "JSON";
}
}
const good = [
undefined, { format: "csv" },
{ format: "csv", ajratgich: ";" }, { format: "json" },
];
for (const input of good) {
console.log(describe(ExportReq.parse(input)));
}
for (const input of [
{ format: "xlsx" }, { format: "json", ajratgich: ";" },
]) {
const r = ExportReq.safeParse(input);
const text = r.error?.issues
.map((i) => `${i.path.join(".") || "(ildiz)"}: ${i.message}`);
console.log(text?.join("; "));
}Konsolda:
CSV, ajratgich ","
CSV, ajratgich ","
CSV, ajratgich ";"
JSON
format: format — "csv" yoki "json"
(ildiz): Unrecognized key: "ajratgich"Uch afzallik:
- Aniq xato. Oddiy
z.unionhar variantni sinab, ikkala variantning xatolarini aralashtirib beradi.discriminatedUnionavvalformatga qaraydi va faqat mos variantni tekshiradi:json+ajratgich→ "noma'lum kalit",xlsx→ "format — csv yoki json". - Tezroq — kalit bo'yicha darhol tanlaydi.
- TypeScript toraytirish —
switch (r.format)ichidacase "csv"dar.ajratgichmavjud,case "json"da esa yo'q (discriminated union).describening hamma yo'lireturnqiladi — yangi format qo'shilsa, TS esdan chiqqancaseni ko'rsatadi.
.default(...) — tanasiz POST /v1/eksportlar ham ishlaydi (oldingi darsdagi eksport kodi buzilmaydi).
5. Xato matnlari: lokal va prettifyError
5.1 O'zbekcha lokal
Kanon topilmasi: Zod 4 da 50 dan ortiq lokal ichida o'zbekchasi bor — z.locales.uz. U bir marta, ilova boshida yoqiladi va sxemada o'z xabari yo'q qoidalarga ta'sir qiladi:
import { z } from "zod";
const NewTask = z.strictObject({
matn: z.string().trim().min(1).max(200),
bajarildi: z.boolean().optional(),
});
const input = { matn: "", bajarildi: "ha", ortiqcha: 1 };
console.log(z.prettifyError(NewTask.safeParse(input).error!));
z.config(z.locales.uz());
console.log(z.prettifyError(NewTask.safeParse(input).error!));Konsolda:
✖ Unrecognized key: "ortiqcha"
✖ Too small: expected string to have >=1 characters
→ at matn
✖ Invalid input: expected boolean, received string
→ at bajarildi
✖ Noma’lum kalit: "ortiqcha"
✖ Juda kichik: kutilgan string >=1 belgi bo‘lishi kerak
→ at matn
✖ Noto‘g‘ri kirish: kutilgan boolean, qabul qilingan string
→ at bajarildiUch halol eslatma:
- Lokal
‘va’(U+2018/U+2019) belgilarini ishlatadi, bizning matnlar esa oddiy'. Bitta javobda ikkala belgi aralashadi. - Ba'zi iboralar texnik: "kutilgan string >=1 belgi". Foydalanuvchiga ko'rinadigan maydonlarda o'z xabaringizni yozing (
{ error: "matn 1–200 belgi bo'lishi kerak" }) — lokal faqat zaxira. z.config— global sozlama: butun jarayonga ta'sir qiladi. Kanonda usrc/umumiy/tekshir.tsning boshida, bir marta.
Saytdagi «Ishga tushir» ishlatadigan Zod to'plamida faqat ingliz lokali bor — o'zbekcha xabarlarni o'z kompyuteringizda ko'rasiz.
5.2 prettifyError — qayerda
z.prettifyError(xato) — odam o'qiydigan ko'p qatorli matn. API javobi uchun emas (mijozga xatolar: [{ joy, xabar }] massivi kerak), balki:
- konfiguratsiya xatosi — server ishga tushganda env noto'g'ri bo'lsa, terminalga aniq ro'yxat (env validatsiyasi — kanon u yerda ishlatadi);
- skriptlar va CLI'lar;
- log.
✖ va → at — Zod'ning o'z belgilari, terminalda yaxshi ko'rinadi.
6. z.toJSONSchema — sxemadan hujjat
6.1 Bitta sxema — uch foyda
TS qismida "bitta sxema — ham tekshiruv, ham tur" degan edik. Uchinchisi — JSON Schema: tilga bog'liq bo'lmagan sxema formati. OpenAPI hujjatlari, boshqa tildagi mijozlar, formalar generatori uni o'qiydi.
import { z } from "zod";
const NewTask = z.strictObject({
matn: z.string().trim().min(1).max(200),
bajarildi: z.boolean().optional(),
});
console.log(JSON.stringify(z.toJSONSchema(NewTask)));
const Limit = z.object({
limit: z.string().regex(/^\d+$/).transform(Number).default(20),
});
const opts = { io: "input", unrepresentable: "any" } as const;
console.log(JSON.stringify(z.toJSONSchema(Limit, opts)));
try {
z.toJSONSchema(Limit);
} catch (error) {
console.log((error as Error).message);
}Konsolda:
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"matn":{"type":"string","minLength":1,"maxLength":200},"bajarildi":{"type":"boolean"}},"required":["matn"],"additionalProperties":false}
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"limit":{"type":"string","pattern":"^\\d+$"}}}
Transforms cannot be represented in JSON SchemaBirinchi natija to'liq: minLength, maxLength, required, strictObject → additionalProperties: false.
Ikkinchisida — chegara. transform — JS funksiya, uni JSON Schema ifodalay olmaydi. Sukut bo'yicha toJSONSchema xato tashlaydi (uchinchi qator). io: "input" — mijoz yuboradigan shakl (transform'dan oldingi): limit satr va pattern. unrepresentable: "any" — ifodalab bo'lmaydigan qism xato o'rniga "har qanday" bo'ladi. Ya'ni JSON Schema'da "1..100" oralig'i va "son" ekani yo'qoldi — bu halol cheklov.
6.2 Kanondagi skript
Kanon scripts/sxema.ts qo'shdi: npm run sxema → docs/sxemalar.json (6 ta sxema, io: "input", unrepresentable: "any"). Natijada discriminatedUnion → oneOf, strictObject → additionalProperties: false, transform'li query maydonlari esa faqat {"type":"string"} (+ pattern). Bu fayl keyinroq OpenAPI hujjatining asosi bo'ladi (OpenAPI).
7. zod/mini — serverda kerakmi?
7.1 Funksional API
zod/mini — o'sha paketning ikkinchi kirishi. Metod zanjiri o'rniga funksiyalar: tree-shaking yaxshi ishlaydi va brauzer bundle'i kichrayadi (bundle tahlili — 22-qism frontend'i shuni ishlatadi):
import * as zm from "zod/mini";
const NewTask = zm.strictObject({
matn: zm.string()
.check(zm.trim(), zm.minLength(1), zm.maxLength(200)),
bajarildi: zm.optional(zm.boolean()),
});
console.log(NewTask.parse({ matn: " Non " }));
const r = NewTask.safeParse({ matn: "" });
console.log(r.error?.issues[0]?.message);Konsolda:
{ matn: 'Non' }
Invalid inputIkkinchi qatorga qarang: "Invalid input" — klassik Zod'dagi "Too small: expected string to have >=1 characters" o'rniga. zod/mini hajmni tejash uchun hech qanday lokalni o'zi yuklamaydi — umumiy xabar beradi. Aniq xabar kerak bo'lsa, zm.config(zm.locales.en()) (yoki uz) qo'shiladi.
7.2 Kanon o'lchovi
Kanon (t30, Node 24.21.0, i5-12500H, band mashina, bitta o'lchov) bir xil sxemani 200 000 yaroqli va 20 000 yaroqsiz qiymat bilan tekshirdi:
- zod 4.6.76,63 mln/s
- zod/mini2,69 mln/s
Manba: kanon t30-validatorlar, Node 24.21.0, i5-12500H, 2026-10-24
| zod | zod/mini | |
|---|---|---|
| import (alohida jarayon, mediana) | 99.9 ms | 100.8 ms |
| yaroqli, mln/s | 6.63 | 2.69 |
| yaroqsiz, mln/s | 0.425 | 0.466 |
diskdagi kod (v4/classic / v4/mini) |
364 KB | 185 KB |
Kutilmagan natija: mini serverda sekinroq. Sababi tekshirilmadi — klassik Zod'ning tez yo'li (JIT bilan kompilyatsiya qilingan tekshiruv) bo'lishi mumkin, lekin bu taxmin. Import vaqti bir xil — ikkalasi ham v4/core ni yuklaydi (1 190 KB). Yaroqsiz qiymatlar ikkalasida ham ~15 marta sekinroq: xato obyektlarini yasash qimmat.
Kanon qarori: serverda klassik Zod. Bundle hajmi serverda ahamiyatsiz, tezlik va to'liq xabarlar muhimroq. Brauzerda (22-qism) — zod/mini, u yerda har kilobayt foydalanuvchi telefoniga yuklanadi.
8. API qadami: Zod sxemalari (holat 34, 2-qism)
Oldingi darsdagi tekshir.ts ustiga. Hozircha commit qilmang — holat 34 commit'i jarayon xatolari darsida.
src/umumiy/tekshir.tsboshiga —z.config(z.locales.uz()).src/vazifalar/sxema.tsga:RoyxatQuerySxemasi(yuqoridagi sxemaning to'liq varianti:bajarildi,teg,q,sort,fields→z.enum+iddoim,limit,offset,cursor,jami),ToplamSxemasi(ToliqVazifaSxemasi.extend({ id: z.int().positive().optional() })),BulkSxemasi,EksportSxemasi(discriminatedUnion).src/vazifalar/sorov.tsdagi qo'lda parse — o'chiriladi;GET /v1/vazifalar—tekshirib({ query: RoyxatQuerySxemasi }, ...).- Turlar —
z.output<typeof …>:RoyxatQuery,EksportSorovi,YangiVazifa,ToliqVazifa,VazifaOzgarishi. scripts/sxema.ts+package.jsonda"sxema": "node scripts/sxema.ts"→docs/sxemalar.json.- Yangi test fayli
test/sxema.test.ts: query transform, sukut, noma'lum parametr, offset+cursor, o'zbekcha lokal, eksportdiscriminatedUnion(csv;bilan"Non; sut"to'g'ri qochiriladi,json,xlsx→ 422,json+ ajratgich → 422).
To'liq src/vazifalar/sxema.ts (kanondan; uzun qatorlar telefon uchun bo'lingan — faqat format, ma'no aynan):
// sxema.ts — vazifalar API'ning Zod sxemalari: bitta sxemadan
// ham ish vaqti tekshiruvi, ham TS turi (z.infer),
// ham JSON Schema (npm run sxema)
import { z } from "zod";
import { SARALASH_MAYDONLARI } from "./ombor.ts";
import type { Saralash, SaralashMaydoni } from "./ombor.ts";
export const MAKS_MATN = 200;
export const MAKS_TOPLAM = 1000;
export const SUKUT_LIMIT = 20;
export const MAKS_LIMIT = 100;
export const MAKS_OFFSET = 10_000;
const matnXabari =
`matn 1–${String(MAKS_MATN)} belgi bo'lishi kerak`;
const Matn = z
.string({ error: "matn — satr bo'lishi kerak" })
.trim()
.min(1, { error: matnXabari })
.max(MAKS_MATN, { error: matnXabari });
const Bajarildi = z.boolean({
error: "bajarildi — true yoki false",
});
/** POST: matn majburiy; noma'lum maydon — xato
* (strictObject = oq ro'yxat) */
export const YangiVazifaSxemasi = z.strictObject({
matn: Matn,
bajarildi: Bajarildi.optional(),
});
/** PUT /:id: hamma yoziladigan maydon majburiy */
export const ToliqVazifaSxemasi = z.strictObject({
matn: Matn,
bajarildi: Bajarildi,
});
/** PATCH (JSON Merge Patch): kamida bitta maydon; null — xato */
export const OzgarishSxemasi = z
.strictObject({
matn: Matn.optional(),
bajarildi: Bajarildi.optional(),
})
.refine((o) => o.matn !== undefined || o.bajarildi !== undefined, {
error: "o'zgarish yo'q: matn yoki bajarildi",
});
/** PUT /v1/vazifalar: butun to'plam */
export const ToplamSxemasi = z.strictObject({
vazifalar: z
.array(ToliqVazifaSxemasi.extend({
id: z.int().positive().optional(),
}))
.max(MAKS_TOPLAM),
});
/** POST /toplam: 1..100 element; elementlar alohida tekshiriladi */
export const BulkSxemasi = z.strictObject({
vazifalar: z.array(z.unknown()).min(1).max(100),
});
export const IdSxemasi = z.strictObject({
id: z
.string()
.regex(/^[1-9]\d{0,9}$/, { error: "id — musbat butun son" })
.transform(Number),
});
// ─── Ro'yxat query'si ───
// Express 5 "simple" parser: qiymat — string yoki string[]
const Bitta = (nom: string) =>
z.string({ error: `${nom} bir marta berilishi kerak` });
const Mantiqiy = (nom: string) =>
Bitta(nom)
.refine((x) => x === "true" || x === "false", {
error: `${nom} — true yoki false`,
})
.transform((x) => x === "true");
const Butun = (nom: string, min: number, max: number) => {
const xabar =
`${nom} — ${String(min)}..${String(max)} ` +
"oralig'idagi butun son";
return Bitta(nom)
.regex(/^\d{1,6}$/, { error: xabar })
.transform(Number)
.refine((n) => n >= min && n <= max, { error: xabar });
};
const TEG = /^[\p{L}\p{N}_'‘’]{1,50}$/u;
const MAYDONLAR = [
"id", "matn", "bajarildi", "teglar", "yaratildi", "yangilandi",
"versiya",
] as const;
export type Maydon = (typeof MAYDONLAR)[number];
function saralashniOqi(x: string, ctx: z.RefinementCtx): Saralash[] {
const natija: Saralash[] = [];
for (const qism of x.split(",")) {
const kamayish = qism.startsWith("-");
const maydon = kamayish ? qism.slice(1) : qism;
const mumkin = SARALASH_MAYDONLARI as readonly string[];
if (!mumkin.includes(maydon)) {
ctx.issues.push({
code: "custom", input: x,
message: `sort: noma'lum maydon "${maydon}" ` +
`(mumkin: ${SARALASH_MAYDONLARI.join(", ")})`,
});
return z.NEVER;
}
if (natija.some((s) => s.maydon === maydon)) {
ctx.issues.push({
code: "custom", input: x,
message: `sort: "${maydon}" takrorlangan`,
});
return z.NEVER;
}
natija.push({ maydon: maydon as SaralashMaydoni, kamayish });
}
return natija;
}
export const RoyxatQuerySxemasi = z
.strictObject(
{
bajarildi: Mantiqiy("bajarildi").optional(),
teg: z
.union([z.string(), z.array(z.string())])
.transform((t) => [
...new Set((Array.isArray(t) ? t : [t])
.map((x) => x.replace(/^#/, "").toLowerCase())),
])
.pipe(z.array(z.string().regex(TEG, {
error: "teg — harf, raqam, _ (50 belgigacha)",
})).max(10))
.optional(),
q: Bitta("q").trim()
.min(1, { error: "q — 1..100 belgi" })
.max(100, { error: "q — 1..100 belgi" })
.optional(),
sort: Bitta("sort").transform(saralashniOqi).optional(),
fields: Bitta("fields")
.transform((x) => x.split(","))
.pipe(z.array(z.enum(MAYDONLAR, {
error: (i) =>
`fields: noma'lum maydon "${String(i.input)}"`,
})))
.transform((m): Maydon[] =>
[...new Set<Maydon>(["id", ...m])])
.optional(),
limit: Butun("limit", 1, MAKS_LIMIT).default(SUKUT_LIMIT),
offset: Butun("offset", 0, MAKS_OFFSET).optional(),
cursor: Bitta("cursor").max(1000).optional(),
jami: Mantiqiy("jami").optional(),
},
{
error: (i) => (i.code === "unrecognized_keys"
? `noma'lum parametr: ${i.keys.join(", ")}`
: undefined),
},
)
.refine((q) => q.offset === undefined || q.cursor === undefined, {
error: "offset va cursor birga berilmaydi", path: ["cursor"],
});
export type RoyxatQuery = z.output<typeof RoyxatQuerySxemasi>;
// ─── Eksport: discriminatedUnion ───
// "format" maydoni qaysi variant ekanini aytadi
export const EksportSxemasi = z
.discriminatedUnion(
"format",
[
// Excel o'zbek/rus lokalida ";" ni kutadi — tanlash mumkin
z.strictObject({
format: z.literal("csv"),
ajratgich: z.enum([",", ";"]).default(","),
}),
z.strictObject({ format: z.literal("json") }),
],
{ error: "format — \"csv\" yoki \"json\"" },
)
.default({ format: "csv", ajratgich: "," });
export type EksportSorovi = z.output<typeof EksportSxemasi>;Kanonda sort transform'i sxema ichida yozilgan. Bu yerda o'qish oson bo'lsin deb u saralashniOqi funksiyasiga chiqarildi. Ma'nosi aynan bir xil. MAKS_MATN = 200 — holat 34 qiymati; backend'da sana va vaqt darsida (holat 40) frontend bilan moslab 100 ga tushiriladi.
scripts/sxema.ts (to'liq, kanondan):
// sxema.ts — Zod sxemalaridan JSON Schema (z.toJSONSchema): hujjat,
// OpenAPI va frontend uchun yagona manba.
// npm run sxema → docs/sxemalar.json.
// io: "input" — mijoz YUBORADIGAN shakl (transform'dan oldin)
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { z } from "zod";
import {
EksportSxemasi, OzgarishSxemasi, RoyxatQuerySxemasi,
ToliqVazifaSxemasi, ToplamSxemasi, YangiVazifaSxemasi,
} from "../src/vazifalar/sxema.ts";
const sxemalar = {
YangiVazifa: YangiVazifaSxemasi,
ToliqVazifa: ToliqVazifaSxemasi,
VazifaOzgarishi: OzgarishSxemasi,
Toplam: ToplamSxemasi,
Eksport: EksportSxemasi,
RoyxatQuery: RoyxatQuerySxemasi,
};
// transform/refine JSON Schema'da ifodalanmaydi — xato bermasin
const natija = Object.fromEntries(
Object.entries(sxemalar).map(([nom, s]) => [
nom,
z.toJSONSchema(s, { io: "input", unrepresentable: "any" }),
]),
);
const papka = join(import.meta.dirname, "..", "docs");
mkdirSync(papka, { recursive: true });
writeFileSync(join(papka, "sxemalar.json"),
JSON.stringify(natija, null, 2) + "\n");
console.log(`docs/sxemalar.json: ${Object.keys(natija).join(", ")}`);Kanon jonli natijasi (holat 34, sorovId siz):
curl "$A/v1/vazifalar?limit=0&sort=parol&sahifa=2"
curl -X POST $A/v1/eksportlar -H 'Content-Type: application/json' \
-d '{"format":"xlsx"}'{"type":"https://vazifalar-api.example/xatolar/parametr","title":"Parametr noto'g'ri","status":400,"detail":"sort: noma'lum maydon \"parol\" (mumkin: id, matn, bajarildi, yaratildi, yangilandi)","instance":"/v1/vazifalar?limit=0&sort=parol&sahifa=2","kod":"parametr","xatolar":[{"joy":"query.sort","xabar":"sort: noma'lum maydon \"parol\" (mumkin: id, matn, bajarildi, yaratildi, yangilandi)"},{"joy":"query.limit","xabar":"limit — 1..100 oralig'idagi butun son"},{"joy":"query","xabar":"noma'lum parametr: sahifa"}]}
{"type":"https://vazifalar-api.example/xatolar/validatsiya","title":"So'rov ma'lumoti noto'g'ri","status":422,"detail":"format — \"csv\" yoki \"json\"","instance":"/v1/eksportlar","kod":"validatsiya","xatolar":[{"joy":"body.format","xabar":"format — \"csv\" yoki \"json\""}]}Bitta query'da uch xato — uchalasi bitta javobda. Query'da 400, tanada 422.
Kesim tekshiruvi: tip 0, lint 0, testlar 35/35 — biz ishga tushirdik (Node 24.21.0). Kesim holat 34 nusxasidan yasaldi: keyingi darslarning fayllari (xatolar.ts, xato-ishlovchi.ts, jarayon.ts va uning testi) olib tashlandi, xatolar HttpXato ga qaytarildi. 35 = holat 27 dagi 31 ta + test/sxema.test.ts ning 4 tasi.
9. Ko'p uchraydigan xatolar
1. Query uchun z.coerce.boolean(). "false" → true. z.enum(["true","false"]).transform(...).
2. transform dan keyin .min() yozish. Transform natijasi boshqa tur — tekshiruv pipe orqali: .transform(f).pipe(z.array(...).max(10)).
3. Transform ichida throw. Zod uni xato sifatida yig'maydi — butun so'rov 500 bilan yiqiladi. To'g'risi: ctx.issues.push(...) va return z.NEVER.
4. z.union variantli tanada. Xatolar aralashadi; discriminatedUnion ishlating.
5. z.config ni har so'rovda chaqirish. Global sozlama — ilova boshida bir marta.
6. prettifyError matnini API javobiga qo'yish. Mijoz uni tahlil qila olmaydi — xatolar massivi kerak.
7. toJSONSchema natijasini to'liq shartnoma deb hisoblash. Transform va refine qoidalari unda yo'q.
10. Mashqlar
Yechimlar kurs/mashqlar/25/30-zod-server/ da.
1-mashq (oson): to'ldiring
- Transform natijasini yana sxemadan o'tkazish metodi —
.() - Transform ichida "natija yo'q, xato" belgisi —
z. - Kalit maydon bo'yicha variant tanlaydigan sxema —
z. - O'zbekcha lokalni yoqish —
z.config(z.locales.()) - Mijoz yuboradigan shakl uchun
toJSONSchemaopsiyasi —io: ""
Yechim
- pipe —
transformdan keyingi tekshiruv. - NEVER —
ctx.issues.pushbilan birga. - discriminatedUnion —
formatkabi kalit bo'yicha. - uz — 4.6.x da bor.
- input — transform'dan oldingi shakl.
2-mashq (o'rta): fields parametri
?fields=matn,teglar uchun sxema yozing: satr → vergul bo'yicha bo'linadi → har biri z.enum(["id","matn","bajarildi","teglar"]) → id doim qo'shiladi, takror yo'q. Noma'lum maydon xabari: fields: noma'lum maydon "<nom>". Blok Node'da Zod bilan.
Yechim
import { z } from "zod";
const FIELDS = ["id", "matn", "bajarildi", "teglar"] as const;
type Field = (typeof FIELDS)[number];
const Fields = z.string()
.transform((x) => x.split(","))
.pipe(z.array(z.enum(FIELDS, {
error: (i) => `fields: noma'lum maydon "${String(i.input)}"`,
})))
.transform((m): Field[] => [...new Set<Field>(["id", ...m])]);
console.log(Fields.parse("matn,teglar"));
console.log(Fields.parse("matn,id,matn"));
const bad = Fields.safeParse("matn,parol");
console.log(bad.error?.issues.map((i) => i.message));Konsolda:
[ 'id', 'matn', 'teglar' ]
[ 'id', 'matn' ]
[ `fields: noma'lum maydon "parol"` ]Set tartibni birinchi uchrash bo'yicha saqlaydi — id doim birinchi. Oxirgi qatordagi teskari qo'shtirnoq — Node satr ichida ' bo'lgani uchun shunday chiqaradi.
3-mashq (qiyin): to'lov usuli
POST /v1/tolovlar tanasi uchun discriminatedUnion("usul", …) yozing: karta — { usul, kartaOxiri: 4 raqamli satr }; click — { usul, telefon: "+998" + 9 raqam }; naqd — { usul }. Hammasida summa — musbat butun son (tiyin). Natija turini switch bilan toraytirib, tavsif chiqaring. Noto'g'ri usul va click uchun noto'g'ri telefonni sinang.
Yechim
import { z } from "zod";
const Summa = z.int().positive();
const Payment = z.discriminatedUnion("usul", [
z.strictObject({
usul: z.literal("karta"), summa: Summa,
kartaOxiri: z.string().regex(/^\d{4}$/),
}),
z.strictObject({
usul: z.literal("click"), summa: Summa,
telefon: z.string().regex(/^\+998\d{9}$/, {
error: "telefon +998XXXXXXXXX",
}),
}),
z.strictObject({ usul: z.literal("naqd"), summa: Summa }),
], { error: "usul — karta, click yoki naqd" });
function describe(p: z.output<typeof Payment>): string {
switch (p.usul) {
case "karta":
return `karta *${p.kartaOxiri}, ${p.summa / 100} so'm`;
case "click":
return `Click ${p.telefon}, ${p.summa / 100} so'm`;
case "naqd":
return `naqd, ${p.summa / 100} so'm`;
}
}
console.log(describe(Payment.parse({
usul: "karta", summa: 3500000, kartaOxiri: "4242",
})));
console.log(describe(Payment.parse({ usul: "naqd", summa: 500000 })));
for (const bad of [
{ usul: "kripto", summa: 100 },
{ usul: "click", summa: 100, telefon: "901234567" },
]) {
const r = Payment.safeParse(bad);
console.log(r.error?.issues.map((i) => i.message).join("; "));
}Konsolda:
karta *4242, 35000 so'm
naqd, 5000 so'm
usul — karta, click yoki naqd
telefon +998XXXXXXXXXKarta raqamining faqat oxirgi 4 raqami — to'liq raqam API'ga umuman kelmasligi kerak (to'lov sahifasi provayderda, to'lov UI).
4-mashq: API qadami
«API qadami» bo'limidagi 1–6 qadamlarni bajaring. npm run sxema ni ishga tushiring va docs/sxemalar.json da Eksport sxemasi oneOf bilan, RoyxatQuery dagi limit esa faqat {"type":"string","pattern":…} ekanini tekshiring. curl bilan ?limit=0&sort=parol&sahifa=2 yuboring — uchta xato. Commit qilmang.
11. Real ishda
- tRPC, Hono, Fastify (type provider) va Next.js Server Actions — hammasi Zod sxemasini kirish tekshiruvi sifatida qabul qiladi (tRPC).
- NestJS da Zod —
ZodValidationPipeyoki Standard Schema orqali (NestJS'da Zod). zod-openapi,@asteasolutions/zod-to-openapikabi paketlar Zod sxemalaridan to'liq OpenAPI hujjati yasaydi.- Katta loyihalarda sxemalar
packages/sharedda — frontend formasi va backend chegarasi bitta qoidadan foydalanadi.
Xulosa
- Query — satr yoki satrlar massivi;
z.coerceo'rniga shakl →transform→ (pipe). - Transform ichida xato —
ctx.issues.push+z.NEVER; ko'p maydonli qoida — obyektrefinevapath. - Variantli tana —
discriminatedUnion: aniq xato va TS toraytirish. z.locales.uz— zaxira xabarlar;prettifyError— config va log uchun;toJSONSchema— transform'siz shakl.- Serverda klassik Zod: kanon o'lchovida
zod/mini2.5 marta sekinroq va lokalsiz.
Keyingi dars: Backend validatorlari: Joi, class-validator va express-validator — bir xil sxemani boshqa uchta kutubxonada yozib, tezlik, hajm va xato matnlarini solishtiramiz.
Manbalar
- Zod 4 hujjati: "Defining schemas", "Customizing errors", "Formatting errors", "JSON Schema", "Zod Mini" — zod.dev
- Zod lokallari ro'yxati — zod.dev/error-customization
- JSON Schema 2020-12 — json-schema.org/draft/2020-12
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!