{"id":"b8124668-3a5e-4448-864a-2c0abe0eff77","shortId":"4f6XQZ","kind":"skill","title":"zod-validation-expert","tagline":"Expert in Zod — TypeScript-first schema validation. Covers parsing, custom errors, refinements, type inference, and integration with React Hook Form, Next.js, and tRPC.","description":"# Zod Validation Expert\n\nYou are a production-grade Zod expert. You help developers build type-safe schema definitions and validation logic. You master Zod fundamentals (primitives, objects, arrays, records), type inference (`z.infer`), complex validations (`.refine`, `.superRefine`), transformations (`.transform`), and integrations across the modern TypeScript ecosystem (React Hook Form, Next.js API Routes / App Router Actions, tRPC, and environment variables).\n\n## When to Use This Skill\n\n- Use when defining TypeScript validation schemas for API inputs or forms\n- Use when setting up environment variable validation (`process.env`)\n- Use when integrating Zod with React Hook Form (`@hookform/resolvers/zod`)\n- Use when extracting or inferring TypeScript types from runtime validation schemas\n- Use when writing complex validation rules (e.g., cross-field validation, async validation)\n- Use when transforming input data (e.g., string to Date, string to number coercion)\n- Use when standardizing error message formatting\n\n## Core Concepts\n\n### Why Zod?\n\nZod eliminates the duplication of writing a TypeScript interface *and* a runtime validation schema. You define the schema once, and Zod infers the static TypeScript type. Note that Zod is for **parsing, not just validation**. `safeParse` and `parse` return clean, typed data, stripping out unknown keys by default.\n\n## Schema Definition & Inference\n\n### Primitives & Coercion\n\n```typescript\nimport { z } from \"zod\";\n\n// Basic primitives\nconst stringSchema = z.string().min(3).max(255);\nconst numberSchema = z.number().int().positive();\nconst dateSchema = z.date();\n\n// Coercion (automatically casting inputs before validation)\n// Highly useful for FormData in Next.js Server Actions or URL queries\nconst ageSchema = z.coerce.number().min(18); // \"18\" -> 18\nconst activeSchema = z.coerce.boolean(); // \"true\" -> true\nconst dobSchema = z.coerce.date(); // \"2020-01-01\" -> Date object\n```\n\n### Objects & Type Inference\n\n```typescript\nconst UserSchema = z.object({\n  id: z.string().uuid(),\n  username: z.string().min(3).max(20),\n  email: z.string().email(),\n  role: z.enum([\"ADMIN\", \"USER\", \"GUEST\"]).default(\"USER\"),\n  age: z.number().min(18).optional(), // Can be omitted\n  website: z.string().url().nullable(), // Can be null\n  tags: z.array(z.string()).min(1), // Array with at least 1 item\n});\n\n// Infer the TypeScript type directly from the schema\n// No need to write a separate `interface User { ... }`\nexport type User = z.infer<typeof UserSchema>;\n```\n\n### Advanced Types\n\n```typescript\n// Records (Objects with dynamic keys but specific value types)\nconst envSchema = z.record(z.string(), z.string()); // Record<string, string>\n\n// Unions (OR)\nconst idSchema = z.union([z.string(), z.number()]); // string | number\n// Or simpler:\nconst idSchema2 = z.string().or(z.number());\n\n// Discriminated Unions (Type-safe switch cases)\nconst ActionSchema = z.discriminatedUnion(\"type\", [\n  z.object({ type: z.literal(\"create\"), id: z.string() }),\n  z.object({ type: z.literal(\"update\"), id: z.string(), data: z.any() }),\n  z.object({ type: z.literal(\"delete\"), id: z.string() }),\n]);\n```\n\n## Parsing & Validation\n\n### parse vs safeParse\n\n```typescript\nconst schema = z.string().email();\n\n// ❌ parse: Throws a ZodError if validation fails\ntry {\n  const email = schema.parse(\"invalid-email\");\n} catch (err) {\n  if (err instanceof z.ZodError) {\n    console.error(err.issues);\n  }\n}\n\n// ✅ safeParse: Returns a result object (No try/catch needed)\nconst result = schema.safeParse(\"user@example.com\");\n\nif (!result.success) {\n  // TypeScript narrows result to SafeParseError\n  console.log(result.error.format()); \n  // Early return or throw domain error\n} else {\n  // TypeScript narrows result to SafeParseSuccess\n  const validEmail = result.data; // Type is `string`\n}\n```\n\n## Customizing Validation\n\n### Custom Error Messages\n\n```typescript\nconst passwordSchema = z.string()\n  .min(8, { message: \"Password must be at least 8 characters long\" })\n  .max(100, { message: \"Password is too long\" })\n  .regex(/[A-Z]/, { message: \"Password must contain at least one uppercase letter\" })\n  .regex(/[0-9]/, { message: \"Password must contain at least one number\" });\n\n// Global custom error map (useful for i18n)\nz.setErrorMap((issue, ctx) => {\n  if (issue.code === z.ZodIssueCode.invalid_type) {\n    if (issue.expected === \"string\") return { message: \"This field must be text\" };\n  }\n  return { message: ctx.defaultError };\n});\n```\n\n### Refinements (Custom Logic)\n\n```typescript\n// Basic refinement\nconst passwordCheck = z.string().refine((val) => val !== \"password123\", {\n  message: \"Password is too weak\",\n});\n\n// Cross-field validation (e.g., password matching)\nconst formSchema = z.object({\n  password: z.string().min(8),\n  confirmPassword: z.string()\n}).refine((data) => data.password === data.confirmPassword, {\n  message: \"Passwords don't match\",\n  path: [\"confirmPassword\"], // Sets the error on the specific field\n});\n```\n\n### Transformations\n\n```typescript\n// Change data during parsing\nconst stringToNumber = z.string()\n  .transform((val) => parseInt(val, 10))\n  .refine((val) => !isNaN(val), { message: \"Not a valid integer\" });\n\n// Now the inferred type is `number`, not `string`!\ntype TransformedResult = z.infer<typeof stringToNumber>; // number\n```\n\n## Integration Patterns\n\n### React Hook Form\n\n```typescript\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\n\nconst loginSchema = z.object({\n  email: z.string().email(\"Invalid email address\"),\n  password: z.string().min(6, \"Password must be 6+ characters\"),\n});\n\ntype LoginFormValues = z.infer<typeof loginSchema>;\n\nexport function LoginForm() {\n  const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValues>({\n    resolver: zodResolver(loginSchema)\n  });\n\n  const onSubmit = (data: LoginFormValues) => {\n    // data is fully typed and validated\n    console.log(data.email, data.password);\n  };\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <input {...register(\"email\")} />\n      {errors.email && <span>{errors.email.message}</span>}\n      {/* ... */}\n    </form>\n  );\n}\n```\n\n### Next.js Server Actions\n\n```typescript\n\"use server\";\nimport { z } from \"zod\";\n\n// Coercion is critical here because FormData values are always strings\nconst createPostSchema = z.object({\n  title: z.string().min(3),\n  content: z.string().optional(),\n  published: z.coerce.boolean().default(false), // checkbox -> \"on\" -> true\n});\n\nexport async function createPost(prevState: any, formData: FormData) {\n  // Convert FormData to standard object using Object.fromEntries\n  const rawData = Object.fromEntries(formData.entries());\n  \n  const validatedFields = createPostSchema.safeParse(rawData);\n  \n  if (!validatedFields.success) {\n    return {\n      errors: validatedFields.error.flatten().fieldErrors,\n    };\n  }\n  \n  // Proceed with validated database operation\n  const { title, content, published } = validatedFields.data;\n  // ...\n  return { success: true };\n}\n```\n\n### Environment Variables\n\n```typescript\n// Make environment variables strictly typed and fail-fast\nimport { z } from \"zod\";\n\nconst envSchema = z.object({\n  DATABASE_URL: z.string().url(),\n  NODE_ENV: z.enum([\"development\", \"test\", \"production\"]).default(\"development\"),\n  PORT: z.coerce.number().default(3000),\n  API_KEY: z.string().min(10),\n});\n\n// Fails the build immediately if env vars are missing or invalid\nconst env = envSchema.parse(process.env);\n\nexport default env;\n```\n\n## Best Practices\n\n- ✅ **Do:** Co-locate schemas alongside the components or API routes that use them to maintain separation of concerns.\n- ✅ **Do:** Use `z.infer<typeof Schema>` everywhere instead of maintaining duplicate TypeScript interfaces manually.\n- ✅ **Do:** Prefer `safeParse` over `parse` to avoid scattered `try/catch` blocks and leverage TypeScript's control flow narrowing for robust error handling.\n- ✅ **Do:** Use `z.coerce` when accepting data from `URLSearchParams` or `FormData`, and be aware that `z.coerce.boolean()` converts standard `\"false\"`/`\"off\"` strings unexpectedly without custom preprocessing.\n- ✅ **Do:** Use `.flatten()` or `.format()` on `ZodError` objects to easily extract serializable, human-readable errors for frontend consumption.\n- ❌ **Don't:** Rely exclusively on `.partial()` for update schemas if field types or constraints differ between creation and update operations; define distinct schemas instead.\n- ❌ **Don't:** Forget to pass the `path` option in `.refine()` or `.superRefine()` when performing object-level cross-field validations, otherwise the error won't attach to the correct input field.\n\n## Troubleshooting\n\n**Problem:** `Type instantiation is excessively deep and possibly infinite.`\n**Solution:** This occurs with extreme schema recursion (e.g. deeply nested self-referential schemas). Use `z.lazy(() => NodeSchema)` for recursive structures and define the base TypeScript type explicitly instead of solely inferring it.\n\n**Problem:** Empty strings pass validation when using `.optional()`.\n**Solution:** `.optional()` permits `undefined`, not empty strings. If an empty string means \"no value,\" use `.or(z.literal(\"\"))` or preprocess it: `z.string().transform(v => v === \"\" ? undefined : v).optional()`.\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["zod","validation","expert","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding"],"capabilities":["skill","source-sickn33","skill-zod-validation-expert","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/zod-validation-expert","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34404 github stars · SKILL.md body (9,631 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-22T00:52:02.143Z","embedding":null,"createdAt":"2026-04-18T21:48:01.006Z","updatedAt":"2026-04-22T00:52:02.143Z","lastSeenAt":"2026-04-22T00:52:02.143Z","tsv":"'-01':277,278 '-9':533 '0':532 '1':326,331 '10':634,851 '100':512 '18':265,266,267,310 '20':296 '2020':276 '255':235 '3':233,294,759 '3000':846 '6':689,693 '8':501,508,600 'a-z':519 'accept':927 'across':71 'action':84,257,735 'actionschema':397 'activeschema':269 'address':685 'admin':302 'advanc':353 'age':307 'ageschema':262 'alongsid':877 'alway':751 'api':80,101,847,881 'app':82 'array':58,327 'ask':1132 'async':144,771 'attach':1016 'automat':245 'avoid':908 'awar':935 'base':1055 'basic':227,573 'best':870 'block':911 'boundari':1140 'build':43,854 'case':395 'cast':246 'catch':444 'chang':623 'charact':509,694 'checkbox':767 'clarif':1134 'clean':208 'clear':1107 'co':874 'co-loc':873 'coercion':158,221,244,743 'complex':63,136 'compon':879 'concept':166 'concern':890 'confirmpassword':601,613 'console.error':450 'console.log':471,720 'const':229,236,241,261,268,273,285,365,375,384,396,426,438,460,485,497,575,594,627,677,701,710,753,785,789,804,828,863 'constraint':979 'consumpt':965 'contain':525,537 'content':760,806 'control':916 'convert':778,938 'core':165 'correct':1019 'cover':13 'creat':403 'createpost':773 'createpostschema':754 'createpostschema.safeparse':791 'creation':982 'criteria':1143 'critic':745 'cross':141,588,1008 'cross-field':140,587,1007 'ctx':551 'ctx.defaulterror':568 'custom':15,491,493,543,570,945 'data':150,210,412,604,624,712,714,928 'data.confirmpassword':606 'data.email':721 'data.password':605,722 'databas':802,831 'date':154,279 'dateschema':242 'deep':1028 'deepli':1040 'default':216,305,765,841,845,868 'defin':96,184,986,1053 'definit':48,218 'delet':417 'describ':1111 'develop':42,838,842 'differ':980 'direct':337 'discrimin':389 'distinct':987 'dobschema':274 'domain':477 'duplic':172,898 'dynam':359 'e.g':139,151,591,1039 'earli':473 'easili':956 'ecosystem':75 'elimin':170 'els':479 'email':297,299,429,439,443,680,682,684,730 'empti':1065,1077,1081 'env':836,857,864,869 'environ':87,109,812,816,1123 'environment-specif':1122 'envschema':366,829 'envschema.parse':865 'err':445,447 'err.issues':451 'error':16,162,478,494,544,616,705,796,921,962,1013 'errors.email':731 'errors.email.message':732 'everywher':894 'excess':1027 'exclus':969 'expert':4,5,31,39,1128 'explicit':1058 'export':349,698,770,867 'extract':124,957 'extrem':1036 'fail':436,822,852 'fail-fast':821 'fals':766,940 'fast':823 'field':142,562,589,620,976,1009,1021 'fielderror':798 'first':10 'flatten':949 'flow':917 'forget':992 'form':25,78,104,120,660,668,724 'format':164,951 'formdata':253,748,776,777,779,932 'formdata.entries':788 'formschema':595 'formstat':704 'frontend':964 'fulli':716 'function':699,772 'fundament':55 'global':542 'grade':37 'guest':304 'handl':922 'handlesubmit':703,726 'help':41 'high':250 'hook':24,77,119,659,667 'hookform/resolvers/zod':121,672 'human':960 'human-read':959 'i18n':548 'id':288,404,410,418 'idschema':376 'idschema2':385 'immedi':855 'import':223,662,669,673,739,824 'infer':19,61,126,190,219,283,333,646,1062 'infinit':1031 'input':102,149,247,728,1020,1137 'instanceof':448 'instanti':1025 'instead':895,989,1059 'int':239 'integ':643 'integr':21,70,115,656 'interfac':177,347,900 'invalid':442,683,862 'invalid-email':441 'isnan':637 'issu':550 'issue.code':553 'issue.expected':557 'item':332 'key':214,360,848 'least':330,507,527,539 'letter':530 'level':1006 'leverag':913 'limit':1099 'locat':875 'logic':51,571 'loginform':700 'loginformvalu':696,713 'loginschema':678,709 'long':510,517 'maintain':887,897 'make':815 'manual':901 'map':545 'master':53 'match':593,611,1108 'max':234,295,511 'mean':1083 'messag':163,495,502,513,522,534,560,567,582,607,639 'min':232,264,293,309,325,500,599,688,758,850 'miss':860,1145 'modern':73 'must':504,524,536,563,691 'narrow':467,481,918 'need':342,459 'nest':1041 'next.js':26,79,255,733 'node':835 'nodeschema':1048 'note':195 'null':321 'nullabl':318 'number':157,381,541,649,655 'numberschema':237 'object':57,280,281,357,456,782,954,1005 'object-level':1004 'object.fromentries':784,787 'occur':1034 'omit':314 'one':528,540 'onsubmit':711,725,727 'oper':803,985 'option':311,762,997,1071,1073,1098 'otherwis':1011 'output':1117 'pars':14,200,206,420,422,430,626,906 'parseint':632 'partial':971 'pass':994,1067 'password':503,514,523,535,583,592,597,608,686,690 'password123':581 'passwordcheck':576 'passwordschema':498 'path':612,996 'pattern':657 'perform':1003 'permiss':1138 'permit':1074 'port':843 'posit':240 'possibl':1030 'practic':871 'prefer':903 'preprocess':946,1090 'prevstat':774 'primit':56,220,228 'problem':1023,1064 'proceed':799 'process.env':112,866 'product':36,840 'production-grad':35 'publish':763,807 'queri':260 'rawdata':786,792 'react':23,76,118,658,666 'react-hook-form':665 'readabl':961 'record':59,356,370 'recurs':1038,1050 'referenti':1044 'refin':17,65,569,574,578,603,635,999 'regex':518,531 'regist':702,729 'reli':968 'requir':1136 'resolv':707 'result':455,461,468,482 'result.data':487 'result.error.format':472 'result.success':465 'return':207,453,474,559,566,723,795,809 'review':1129 'robust':920 'role':300 'rout':81,882 'router':83 'rule':138 'runtim':130,180 'safe':46,393 'safepars':204,424,452,904 'safeparseerror':470 'safeparsesuccess':484 'safeti':1139 'scatter':909 'schema':11,47,99,132,182,186,217,340,427,876,974,988,1037,1045 'schema.parse':440 'schema.safeparse':462 'scope':1110 'self':1043 'self-referenti':1042 'separ':346,888 'serializ':958 'server':256,734,738 'set':107,614 'simpler':383 'skill':93,1102 'skill-zod-validation-expert' 'sole':1061 'solut':1032,1072 'source-sickn33' 'specif':362,619,1124 'standard':161,781,939 'static':192 'stop':1130 'strict':818 'string':152,155,371,372,380,490,558,651,752,942,1066,1078,1082 'stringschema':230 'stringtonumb':628 'strip':211 'structur':1051 'substitut':1120 'success':810,1142 'superrefin':66,1001 'switch':394 'tag':322 'task':1106 'test':839,1126 'text':565 'throw':431,476 'titl':756,805 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'transform':67,68,148,621,630,1093 'transformedresult':653 'treat':1115 'tri':437 'troubleshoot':1022 'trpc':28,85 'true':271,272,769,811 'try/catch':458,910 'type':18,45,60,128,194,209,282,336,350,354,364,392,399,401,407,415,488,555,647,652,695,717,819,977,1024,1057 'type-saf':44,391 'typescript':9,74,97,127,176,193,222,284,335,355,425,466,480,496,572,622,661,736,814,899,914,1056 'typescript-first':8 'undefin':1075,1096 'unexpect':943 'union':373,390 'unknown':213 'updat':409,973,984 'uppercas':529 'url':259,317,832,834 'urlsearchparam':930 'use':91,94,105,113,122,133,146,159,251,546,737,783,884,892,924,948,1046,1070,1086,1100 'useform':663,706 'user':303,306,348,351 'user@example.com':463 'usernam':291 'userschema':286 'uuid':290 'v':1094,1095,1097 'val':579,580,631,633,636,638 'valid':3,12,30,50,64,98,111,131,137,143,145,181,203,249,421,435,492,590,642,719,801,1010,1068,1125 'validatedfield':790 'validatedfields.data':808 'validatedfields.error.flatten':797 'validatedfields.success':794 'validemail':486 'valu':363,749,1085 'var':858 'variabl':88,110,813,817 'vs':423 'weak':586 'websit':315 'without':944 'won':1014 'write':135,174,344 'z':224,521,674,740,825 'z.any':413 'z.array':323 'z.coerce':925 'z.coerce.boolean':270,764,937 'z.coerce.date':275 'z.coerce.number':263,844 'z.date':243 'z.discriminatedunion':398 'z.enum':301,837 'z.infer':62,352,654,697,893 'z.lazy':1047 'z.literal':402,408,416,1088 'z.number':238,308,379,388 'z.object':287,400,406,414,596,679,755,830 'z.record':367 'z.seterrormap':549 'z.string':231,289,292,298,316,324,368,369,378,386,405,411,419,428,499,577,598,602,629,681,687,757,761,833,849,1092 'z.union':377 'z.zoderror':449 'z.zodissuecode.invalid':554 'zod':2,7,29,38,54,116,168,169,189,197,226,676,742,827 'zod-validation-expert':1 'zoderror':433,953 'zodresolv':670,708","prices":[{"id":"f1eda261-f9a0-4955-ba34-25effe0ff74a","listingId":"b8124668-3a5e-4448-864a-2c0abe0eff77","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:48:01.006Z"}],"sources":[{"listingId":"b8124668-3a5e-4448-864a-2c0abe0eff77","source":"github","sourceId":"sickn33/antigravity-awesome-skills/zod-validation-expert","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/zod-validation-expert","isPrimary":false,"firstSeenAt":"2026-04-18T21:48:01.006Z","lastSeenAt":"2026-04-22T00:52:02.143Z"}],"details":{"listingId":"b8124668-3a5e-4448-864a-2c0abe0eff77","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"zod-validation-expert","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34404,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-21T16:43:40Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"341498720a24cde4349c3cc5736dd350d2368633","skill_md_path":"skills/zod-validation-expert/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/zod-validation-expert"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"zod-validation-expert","description":"Expert in Zod — TypeScript-first schema validation. Covers parsing, custom errors, refinements, type inference, and integration with React Hook Form, Next.js, and tRPC."},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/zod-validation-expert"},"updatedAt":"2026-04-22T00:52:02.143Z"}}