Why this is hard to get right
The Problem With Guessing at API Shapes
Marcus is a mid-level TypeScript engineer at a B2B SaaS company. His team just integrated a third-party CRM webhook that fires user activity events in real time. The payload documentation is sparse — a single example object, no field-level descriptions, and a note that says "some fields may be absent."
He pastes the sample JSON into ChatGPT and asks it to "generate TypeScript types and a Zod schema." The output looks plausible. It runs. He ships it.
Three days later, production starts throwing unhandled errors. The webhook sometimes sends null for lastActivity and omits metadata entirely. His generated schema treated both as required strings. The validator crashes silently in one middleware and loudly in another, depending on Zod version inconsistencies across two microservices.
This is the core problem with vague generation prompts: they produce output that matches the sample you gave, not the data you'll actually receive. AI fills in the gaps with assumptions — and those assumptions break at the edges.
Marcus goes back and tries again, this time being more deliberate. He specifies:
- The exact JSON payload, including a
nullfield and a missing field - That his project uses TypeScript strict mode
- That he needs Zod v3, not v3.22's newer
.catch()API - That missing fields should be optional, not stripped
- That explicit
nullvalues should become nullable, not undefined - That he wants two separate output files — one for types, one for schemas
- That error messages should be human-readable, not Zod's default internal codes
The difference in output is dramatic. The regenerated schema correctly uses z.string().nullable() for fields that can be null, wraps optional fields with .optional(), and generates error messages like "User email is required" instead of "Expected string, received undefined."
A well-crafted prompt eliminates the gap between the sample and the reality. It doesn't just describe what you want — it encodes the decisions you'd otherwise make by hand: nullability rules, strictness settings, version constraints, and output structure.
For engineers, this matters because type generation isn't a creative task. It's a decision-encoding task. Every ambiguity in your prompt becomes a silent assumption in the output. And silent assumptions become production bugs.
The more context you embed upfront — edge cases, tooling versions, file structure — the less time you spend patching generated code after the fact.
Common mistakes to avoid
Pasting Only the Happy-Path Payload
If you paste a single, clean JSON sample, the AI treats every field as present, non-null, and required. Production payloads rarely match the happy path. Paste 2–3 variants — one with a null field, one with a missing optional field — so the schema reflects what you'll actually receive, not what the docs show.
Skipping Zod Version Specification
Zod's API changed significantly between v3.19 and v3.22, and again in Zod v4. If you don't specify the version, the AI may use
.catch(),.pipe(), or.brand()methods that don't exist in your installed version. Always state the exact Zod version your package.json pins. This prevents silent import errors.Not Distinguishing Optional from Nullable
These are different TypeScript concepts that produce different Zod code.
optionalmeans the key may be absent.nullablemeans the key is present but its value can benull. Without a clear rule in your prompt, the AI defaults to one or the other — and gets it wrong half the time. State the rule explicitly.Omitting Strict Mode Context
TypeScript strict mode changes how the AI handles implicit
any, index signatures, and inferred return types in your generated code. Without knowing yourtsconfig.jsonsettings, the AI produces types that compile under loose settings but fail under strict mode. Include your strictness level — it changes the output meaningfully.Requesting One Combined Output File
Asking for types and schemas in a single file creates a circular-dependency risk and makes the code harder to maintain. Specify that you want separate
types.tsandschema.tsfiles. This signals architectural intent to the AI and produces output that fits real project structures instead of a self-contained script.Forgetting to Specify Error Message Format
Zod's default error messages are technical and opaque — useful for debugging, not for logging pipelines or user-facing error surfaces. If you don't specify the message format, you get raw Zod codes. Include an example error message style (e.g., 'User email is required') so the AI generates
.message()calls throughout the schema.
The transformation
Create TypeScript types and Zod schemas for this API response JSON.
You are a **senior TypeScript engineer**. Generate TypeScript types and **Zod** validators from this JSON sample.
1. **Input JSON:** `{ "id": "u_123", "email": "a@b.com", "age": null, "roles": ["admin"], "profile": { "displayName": "Sam", "timezone": "UTC" }, "createdAt": "2026-03-01T10:12:00Z" }`
2. Assume **TypeScript strict mode** and Zod v3.
3. Treat missing fields as **optional**, explicit `null` as **nullable**.
4. Output: `types.ts` and `schema.ts` code blocks.
5. Add 3 example validations and **human-readable error messages** for failures.Why this works
Anchored Role Context
The prompt opens with 'You are a senior TypeScript engineer', which shifts the AI's output register. It produces idiomatic, production-grade code rather than tutorial-level examples. Generic prompts get generic output. Role framing signals the expected depth, and the AI calibrates its type decisions accordingly.
Concrete Payload as Ground Truth
The Input JSON field in step 1 contains a real sample — including an explicit
nullforageand a nestedprofileobject. This gives the AI a precise data shape to work from, not an abstract description. Every field in the output maps directly to a field in the sample, leaving no room for invented structure.Explicit Nullability Rules
Step 3 states: 'Treat missing fields as optional, explicit null as nullable.' This single rule eliminates the most common source of schema bugs. Without it, the AI guesses at the distinction. With it,
age: nullbecomesz.number().nullable()and an absent field becomes.optional()— consistently, throughout the output.Versioned Tooling Constraints
Step 2 specifies 'TypeScript strict mode and Zod v3', which narrows the solution space. The AI won't reach for APIs introduced in later versions or produce types that only compile under loose settings. Tooling constraints act as guardrails that keep the output compatible with your actual development environment.
Defined Deliverables and Validation Examples
Steps 4 and 5 request two named code blocks plus 3 example validations with human-readable error messages. This forces the AI to produce testable, runnable output rather than abstract type definitions. You can paste
schema.tsinto your repo and immediately verify it handles the edge cases shown in the examples.
The framework behind the prompt
The Theory Behind Type-Safe API Validation
TypeScript gives you compile-time safety, but it can't protect you at runtime. When your app receives external data — from a REST API, a webhook, a CSV import — TypeScript has no way to verify that the data matches the types you defined. You need runtime validation, and that's where schema libraries like Zod come in.
Zod follows the parse, don't validate principle, coined by Alexis King. The idea is that instead of checking data against a type and hoping it conforms, you transform unknown input into a known type through parsing. If parsing succeeds, you get a fully typed value. If it fails, you get a structured error — not a runtime crash three calls down the stack.
This connects directly to defensive programming and the fail-fast principle: catch invalid data at the boundary of your system, before it propagates. Zod's .parse() and .safeParse() methods implement this pattern by design.
The challenge with AI-generated schemas is that AI models treat your prompt as the contract. Every ambiguity in your prompt becomes an assumption in the output. This mirrors the garbage in, garbage out principle that applies to data pipelines — it applies equally to prompt engineering.
Effective schema generation prompts use a technique similar to example-driven specification: you give the AI a concrete sample (like a real JSON payload) rather than an abstract description. This grounds the output in reality and reduces hallucinated field types.
The separation of concerns principle explains why requesting separate types.ts and schema.ts files produces better maintainable code. Types describe the shape; schemas encode the validation rules. Keeping them separate (or deriving types from schemas via z.infer) prevents the two from diverging over time.
Finally, specifying tooling versions (Zod v3, TypeScript strict mode) reflects the reproducibility standard from software engineering: a prompt that produces correct output today should produce the same correct output tomorrow, regardless of what version the AI assumes by default.
Prompt variations
You are a TypeScript developer with strong Zod knowledge.
Generate TypeScript types and a Zod validation schema from this JSON payload:
{ "userId": "abc123", "score": 84, "passed": true, "completedAt": "2025-06-01T09:00:00Z", "notes": null }
Rules:
- Use Zod v3.
- Treat
nullvalues as nullable fields (z.nullable()). - Treat any field that could be omitted as optional.
- Output a single TypeScript file with the inferred type and Zod schema together.
- Include one parse example that shows a validation failure with a clear, plain-English error message like: "Score must be a number."
You are a senior TypeScript architect working on a monorepo with shared type packages.
Generate TypeScript interfaces and Zod v3.22 schemas for this webhook payload from our payment processor:
{ "eventId": "evt_9x2", "type": "payment.succeeded", "amount": 4999, "currency": "USD", "customerId": "cus_abc", "metadata": { "orderId": "ord_77", "source": "checkout" }, "failureCode": null, "processedAt": "2025-05-15T14:22:00Z" }
Requirements:
- TypeScript strict mode, ESM imports.
nullfields are nullable. Fields absent in some payloads are optional.- Export from three files:
types.ts(interfaces),schema.ts(Zod schemas),index.ts(barrel export). - Use
z.inferto derive the TypeScript type from the Zod schema — do not duplicate type definitions. - Add JSDoc comments on each field explaining the business meaning.
- Include 4 parse tests covering: valid payload, missing required field, wrong type, and null in a non-nullable field.
You are a TypeScript data engineer.
I'm ingesting rows from a CSV export into a TypeScript pipeline. Each row, when parsed to JSON, looks like this:
{ "respondentId": "R_001", "ageGroup": "25-34", "score": "78", "completedSurvey": "true", "region": "", "submittedAt": "2025-04-10" }
Rules:
- Use Zod v3 with TypeScript strict mode.
- All CSV values arrive as strings — coerce
scoretonumberandcompletedSurveytobooleanusing Zod's.coerceAPI. - Treat empty strings as invalid — fail validation with the message: "[FieldName] cannot be empty."
regionis optional but cannot be an empty string if present.- Parse
submittedAtas a valid ISO date string. - Output
types.tsandschema.tsas separate code blocks. - Include 3 parse examples: one passing, one with a coercion, one with an empty string failure.
You are a senior TypeScript engineer specializing in API contract design.
Generate Zod v3 schemas and TypeScript types for this polymorphic event payload:
{ "eventType": "user.action", "payload": { "userId": "u_99", "action": "login", "ip": "203.0.113.4" } }
The eventType field can be one of three values: "user.action", "system.alert", or "billing.event". Each maps to a different payload shape.
Requirements:
- TypeScript strict mode, Zod v3.
- Model this as a Zod discriminated union using
z.discriminatedUnion('eventType', [...])— do not usez.union(). - Define a separate schema and interface for each payload variant.
- Export:
types.tswith all interfaces,schema.tswith the discriminated union and sub-schemas. - Show 3 parse examples: one for each event type, each with realistic mock data.
- Add human-readable Zod error messages on every required field.
When to use this prompt
Product Managers Writing API Contracts
Turn sample payloads from engineering into typed models and validators for shared specs and faster handoffs.
Engineers Integrating Third-Party APIs
Create safe parsing for unpredictable payloads, so your app fails early with clear errors.
Customer Success Building Internal Tools
Validate incoming data from exports and webhooks before it hits your dashboard or CRM workflows.
Researchers Cleaning Collected Data
Generate schemas that catch missing, null, and malformed fields during ingestion and analysis.
Pro tips
- 1
Paste 2–3 real payload variants so the schema handles optional fields you see in production.
- 2
Specify your date handling so you avoid silent string parsing bugs.
- 3
Define required fields by environment so staging can stay strict while dev stays flexible.
- 4
Add your preferred error format so messages match your logging and UI patterns.
If your API ships an OpenAPI or Swagger specification, you can skip manual JSON sampling entirely. Paste the relevant schema object from the spec directly into your prompt.
Why this works better than a payload sample:
- OpenAPI schemas explicitly mark fields as
required,nullable, orreadOnly - You get enum values, min/max constraints, and format hints (
format: email,format: uri) already defined - You don't have to guess which fields are optional — the spec tells you
How to structure the prompt: Paste the OpenAPI schema object (the JSON under components/schemas/YourModel), then add: 'Convert this OpenAPI schema to a Zod v3 schema. Respect the required array — fields not listed are optional. Convert nullable: true to z.nullable(). Map OpenAPI format values to Zod validators: email to z.string().email(), uri to z.string().url(), date-time to z.string().datetime().
This approach scales to large APIs and produces schemas that stay synchronized with the spec when it updates.
The core prompt structure stays consistent, but domain context changes what the AI prioritizes:
Fintech and payments: Emphasize decimal precision. Ask for z.string() on monetary amounts instead of z.number() to avoid floating-point errors. Specify that currency codes must match ISO 4217 using .regex(/^[A-Z]{3}$/). Request that amount and currency always be treated as a required pair — never one without the other.
Healthcare and FHIR data: FHIR resources have deeply nested, highly optional structures. Ask the AI to generate schemas with .passthrough() on unknown extension fields rather than .strict(). FHIR date fields use a non-ISO format — specify this explicitly or the AI will use z.string().datetime(), which rejects valid FHIR dates.
Analytics event pipelines: Events often have a shared envelope (eventId, timestamp, source) plus a polymorphic properties object. Ask the AI to use a discriminated union keyed on an eventName field. This produces narrow types per event rather than a wide Record<string, unknown> that loses all type safety downstream.
Internal tooling and CRM exports: These often include legacy fields with inconsistent casing or legacy types. Ask the AI to add a .transform() step that normalizes field names before validation, so your downstream code sees consistent keys regardless of the export format.
Run through this list before you prompt the AI. Each item maps to a decision the AI will make for you if you don't make it first:
Data shape:
- Do you have 1 sample payload or multiple variants?
- Which fields are always present? Which are sometimes absent?
- Which fields can be explicitly
null? Which are just missing when empty?
Tooling:
- Exact Zod version (check
package.json, not docs) - TypeScript version and whether
strict: trueis set intsconfig.json - ESM or CommonJS imports?
Architecture:
- One file or separate
types.ts/schema.ts? - Should types be derived from schemas with
z.inferor defined independently? - Do nested objects need their own named exports?
Error handling:
- What format should validation error messages use?
- Should the schema use
.safeParse()or.parse()in the examples? - Do you need custom error maps for localization?
Edge cases:
- Date fields: strings, Date objects, or transformed?
- Number fields from string sources: coercion needed?
- Enum fields: exhaustive union or
z.enum()?
Answering these before you open the prompt window cuts revision time by more than half.
When not to use this prompt
This prompt pattern is not the right tool in every situation. Avoid it when:
-
Your schema already exists in an OpenAPI or JSON Schema spec. Use a code generation tool like
openapi-typescriptorzod-from-openapiinstead. AI generation introduces drift risk when a machine-readable spec is available. -
Your payload is extremely simple — three or four flat string fields with no nullability complexity. Writing the schema by hand takes two minutes and is easier to review than an AI-generated file.
-
You need guaranteed determinism across your team. AI output varies between runs. For shared type packages used by multiple teams, commit the schema to source control and treat it as a reviewed artifact, not an auto-generated one.
-
Your team uses a different validation library such as Yup, Valibot, or io-ts. This prompt is Zod-specific. Adapting it to another library requires significant restructuring — the nullability rules, coercion API, and error message patterns differ substantially.
In these cases, point the AI toward a different task: reviewing an existing schema for gaps, generating test fixtures for a known schema, or documenting field meanings in JSDoc format.
Troubleshooting
The generated schema uses .optional() everywhere, even on fields that should be required
Your prompt likely didn't specify which fields are required. Add an explicit rule: 'The following fields are always required and must not be marked optional: id, email, createdAt. All other fields are optional unless explicitly null in the sample.' List required fields by name — the AI will not infer required status from a single payload sample.
The AI generates a single types.ts file that mixes interfaces and Zod schemas in the same export
Restate the output structure with file names as headers. Add to your prompt: 'Respond with exactly two code blocks. Label the first one // types.ts and the second // schema.ts. Do not mix type definitions and Zod schemas in the same file.' Explicit labeling overrides the AI's default tendency to produce self-contained examples.
Error messages are Zod's internal codes like 'invalid_type' instead of plain English
The AI didn't receive a message format instruction. Add a concrete example: 'All Zod fields must include a .message() call with a human-readable string. Example: z.string({ message: "User email is required" }). Do not rely on Zod default error messages anywhere in the schema.'
The AI produces Zod v4 syntax (.check(), z.interface()) that fails to compile on Zod v3
Version specification alone isn't always enough. Add a negative constraint: 'Do not use any of the following APIs: .check(), .overwrite(), z.interface(), z.globalRegistry. These were introduced in Zod v4 and are not available in v3.' Listing forbidden APIs is more reliable than stating a version number.
Nested objects are inlined instead of exported as named schemas
Ask explicitly for modular structure. Add: 'Export each nested object as a named Zod schema before the root schema. For example, define profileSchema before userSchema and compose them. Do not inline nested schemas inside the root schema definition.' This produces maintainable code and enables reuse of sub-schemas elsewhere.
How to measure success
How to Evaluate the AI Output
Don't just paste the output into your repo. Run these checks before committing:
Correctness checks:
- Every field from your JSON sample appears in the schema — no additions, no omissions
nullfields usez.nullable(), not.optional()- Absent-but-valid fields use
.optional(), not.nullable() - Nested objects have their own named schema exports, not inline definitions
Tooling compatibility:
- The schema compiles under your exact TypeScript version — run
tsc --noEmitimmediately - No Zod APIs appear that are outside your installed version's changelog
- ESM vs CommonJS imports match your project's module system
Validation behavior:
- Run the 3 example validations included in the output
- Confirm that the failure cases actually fail with the expected error messages
- Confirm that the success case actually succeeds and infers the correct type
Maintainability signals:
- Types are derived via
z.inferrather than defined twice - Error messages are human-readable and follow your specified format
- File structure matches your repo conventions
Now try it on something of your own
Reading about the framework is one thing. Watching it sharpen your own prompt is another — takes 90 seconds, no signup.
Turn your JSON payload into strict TypeScript types and Zod v3 schemas in one prompt — no guessing at nullability or version compatibility.
Try one of these
Frequently asked questions
Paste multiple payload samples in your prompt — one per endpoint variant. Label each one (e.g., 'Payload A: create event', 'Payload B: update event'). Ask the AI to generate a union type or shared base interface with variant-specific extensions. This prevents you from over-widening types just to cover all cases with a single schema.
Yes, with one adjustment. GraphQL responses always have a top-level data and optional errors wrapper. Specify this in your prompt explicitly — ask the AI to wrap the inner schema in z.object({ data: yourSchema, errors: z.array(errorSchema).optional() }). Otherwise the AI treats the raw payload as a flat object and misses the envelope.
Include an example error message format in your prompt. For instance: 'Error messages should follow the pattern: [FieldName]: [human-readable reason]. Example: email: Must be a valid email address.' The AI will apply this format consistently using Zod's .message() parameter throughout the schema.
Structure your prompt to request modular sub-schemas for nested objects. Ask the AI to export each nested object as its own named schema (e.g., profileSchema, rolesSchema), then compose them in the root schema. When a field changes, you update the relevant sub-schema without touching the rest of the file.
Add a hard constraint to your prompt: 'Do not use any Zod API introduced after v3.20. Do not use .check(), .overwrite(), or the new z.interface() syntax.' Listing specific forbidden APIs is more reliable than just stating the version number, because the AI's training cutoff may not perfectly match Zod's changelog.
Use z.infer whenever possible. Defining types separately creates a maintenance burden — you update the schema but forget the interface, or vice versa. In your prompt, specify: 'Derive the TypeScript type from the Zod schema using z.infer<typeof yourSchema>. Do not write a separate interface.' This keeps a single source of truth.
Decide before you prompt and state it explicitly. Options are:
- String validation:
z.string().datetime()— validates format, keeps it as a string - Coercion to Date:
z.coerce.date()— converts ISO strings to JS Date objects - Custom transform:
z.string().transform(s => new Date(s))
Without this instruction, the AI picks one inconsistently across fields.
Yes — it's one of the most valuable applications. Swap the JSON payload for your .env key list and add: 'All values arrive as strings. Coerce PORT to number, NODE_ENV to an enum, and DATABASE_URL to a validated URL string.' The pattern is identical; only the domain changes. Tools like t3-env use this exact approach under the hood.