IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API30/51-dars21 daqiqa
Mundarija (29)

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.coerce o'rniga aniq shakl tekshiruvi + transform + pipe ishlatiladi (z.coerce.boolean() "false" ni true qiladi). 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/mini brauzer bundle'i uchun kichik, lekin serverda o'lchovda klassik Zod'dan ~2.5 marta sekinroq chiqdi.

Bu darsda

  • z.coerce nega query uchun xavfli ekanini ko'rasiz va aniq sxema yozasiz.
  • Express 5 query'si uchun union → transform → pipe, ctx.issues va z.NEVER, .default(), obyekt darajasidagi refine ni ishlatasiz.
  • discriminatedUnion bilan variantli tanani tekshirasiz va TypeScript'da toraytirasiz.
  • Lokal, prettifyError va toJSONSchema ni serverda qayerda ishlatishni bilasiz.
  • Klassik Zod va zod/mini ni 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:

ts
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:

text
{ 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:

ts
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:

text
{
  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 kerak

3.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=#uy qanday natija beradi? ?limit=020 chi?

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:

ts
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:

text
CSV, ajratgich ","
CSV, ajratgich ","
CSV, ajratgich ";"
JSON
format: format — "csv" yoki "json"
(ildiz): Unrecognized key: "ajratgich"

Uch afzallik:

  1. Aniq xato. Oddiy z.union har variantni sinab, ikkala variantning xatolarini aralashtirib beradi. discriminatedUnion avval format ga qaraydi va faqat mos variantni tekshiradi: json + ajratgich → "noma'lum kalit", xlsx → "format — csv yoki json".
  2. Tezroq — kalit bo'yicha darhol tanlaydi.
  3. TypeScript toraytirish — switch (r.format) ichida case "csv" da r.ajratgich mavjud, case "json" da esa yo'q (discriminated union). describe ning hamma yo'li return qiladi — yangi format qo'shilsa, TS esdan chiqqan case ni 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:

ts
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:

text
✖ 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 bajarildi

Uch 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 u src/umumiy/tekshir.ts ning 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.

ts
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:

text
{"$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 Schema

Birinchi 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):

ts
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:

text
{ matn: 'Non' }
Invalid input

Ikkinchi 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:

Yaroqli qiymat tekshiruvi tezligi (safeParse)
  • 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.

  1. src/umumiy/tekshir.ts boshiga — z.config(z.locales.uz()).
  2. src/vazifalar/sxema.ts ga: RoyxatQuerySxemasi (yuqoridagi sxemaning to'liq varianti: bajarildi, teg, q, sort, fields → z.enum + id doim, limit, offset, cursor, jami), ToplamSxemasi (ToliqVazifaSxemasi.extend({ id: z.int().positive().optional() })), BulkSxemasi, EksportSxemasi (discriminatedUnion).
  3. src/vazifalar/sorov.ts dagi qo'lda parse — o'chiriladi; GET /v1/vazifalar — tekshirib({ query: RoyxatQuerySxemasi }, ...).
  4. Turlar — z.output<typeof …>: RoyxatQuery, EksportSorovi, YangiVazifa, ToliqVazifa, VazifaOzgarishi.
  5. scripts/sxema.ts + package.json da "sxema": "node scripts/sxema.ts" → docs/sxemalar.json.
  6. Yangi test fayli test/sxema.test.ts: query transform, sukut, noma'lum parametr, offset+cursor, o'zbekcha lokal, eksport discriminatedUnion (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):

ts
// 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):

ts
// 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):

bash
curl "$A/v1/vazifalar?limit=0&sort=parol&sahifa=2"
curl -X POST $A/v1/eksportlar -H 'Content-Type: application/json' \
  -d '{"format":"xlsx"}'
text
{"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 toJSONSchema opsiyasi — io: ""
Yechim
  • pipe — transform dan keyingi tekshiruv.
  • NEVER — ctx.issues.push bilan birga.
  • discriminatedUnion — format kabi 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
ts
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:

text
[ '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
ts
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:

text
karta *4242, 35000 so'm
naqd, 5000 so'm
usul — karta, click yoki naqd
telefon +998XXXXXXXXX

Karta 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 — ZodValidationPipe yoki Standard Schema orqali (NestJS'da Zod).
  • zod-openapi, @asteasolutions/zod-to-openapi kabi paketlar Zod sxemalaridan to'liq OpenAPI hujjati yasaydi.
  • Katta loyihalarda sxemalar packages/shared da — frontend formasi va backend chegarasi bitta qoidadan foydalanadi.

Xulosa

  • Query — satr yoki satrlar massivi; z.coerce o'rniga shakl → transform → (pipe).
  • Transform ichida xato — ctx.issues.push + z.NEVER; ko'p maydonli qoida — obyekt refine va path.
  • 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/mini 2.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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zod 4 serverda: query sxemasi, discriminatedUnion, prettifyError, toJSONSchema va zod/mini — IlmHamroh