AI Tools Compared

AI tools generate correct Zod schemas about 80% of the time for common validation patterns but frequently miss edge cases, custom validators, and complex nested structures. This guide evaluates each tool’s accuracy and shows which schema types are reliable versus requiring manual review.

Testing AI Tools with Zod Schema Generation

I tested three leading AI coding tools—Claude Code, Cursor, and GitHub Copilot—by giving each the same TypeScript interfaces and asking them to generate corresponding Zod schemas. The test cases ranged from simple user objects to nested data structures with validation rules. Here is what I found.

Test Case 1: Basic User Object

The first test used a straightforward TypeScript interface representing an user:

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

Claude Code generated a clean, accurate Zod schema:

import { z } from "zod";

export const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  age: z.number(),
  isActive: z.boolean(),
});

export type User = z.infer<typeof UserSchema>;

The tool correctly inferred .email() for the email field without prompting. It also added proper TypeScript inference with z.infer.

Cursor produced an identical schema with one minor difference—it used z.string().uuid() for the id field, assuming UUID format. This shows the tool makes assumptions based on field naming conventions. The assumption is reasonable for many applications but could cause issues if your IDs use different formats.

GitHub Copilot generated a schema that matched Claude Code’s output but omitted the .email() validation, treating the email field as a plain string. This is a common limitation with Copilot’s inline suggestions—it tends toward safer, less restrictive schemas unless explicitly guided.

Test Case 2: Nested Object with Arrays

The second test added complexity with nested data:

interface Order {
  orderId: string;
  customer: {
    name: string;
    email: string;
  };
  items: Array<{
    productId: string;
    quantity: number;
    price: number;
  }>;
  createdAt: string;
}

Claude Code handled the nested structure correctly:

const CustomerSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

const OrderItemSchema = z.object({
  productId: z.string(),
  quantity: z.number().int().positive(),
  price: z.number().positive(),
});

export const OrderSchema = z.object({
  orderId: z.string(),
  customer: CustomerSchema,
  items: z.array(OrderItemSchema),
  createdAt: z.string().datetime(),
});

The tool correctly identified that createdAt should be a datetime string and added reasonable validations like .positive() for prices and quantities. It also extracted nested objects into their own schemas for better reusability.

Cursor matched this quality but added optional chaining in unexpected places, creating minor syntax errors that required manual correction. GitHub Copilot struggled with the nested structure, generating flat schemas that ignored the object hierarchy.

Test Case 3: Optional Fields and Defaults

Real-world schemas often include optional fields and default values:

interface Profile {
  username: string;
  bio?: string;
  settings: {
    theme?: "light" | "dark";
    notifications: boolean;
  };
}

Claude Code correctly handled optional fields with the optional() method and properly typed the theme union:

export const ProfileSchema = z.object({
  username: z.string().min(3).max(20),
  bio: z.string().optional(),
  settings: z.object({
    theme: z.enum(["light", "dark"]).optional(),
    notifications: z.boolean(),
  }),
});

This schema correctly marks bio and theme as optional while enforcing the union type for theme. The username validation with length constraints is an useful addition.

Cursor showed inconsistency here—it sometimes used the ?: syntax from TypeScript instead of Zod’s .optional(), which does not work in runtime validation. GitHub Copilot consistently missed optional field handling, generating schemas where optional fields were required.

Test Case 4: Discriminated Unions

The most challenging test involved a discriminated union type, which is common in event-driven systems and API responses:

type ApiResponse =
  | { status: "success"; data: { id: string; value: number } }
  | { status: "error"; code: number; message: string };

Claude Code generated a correct discriminated union schema:

const ApiResponseSchema = z.discriminatedUnion("status", [
  z.object({
    status: z.literal("success"),
    data: z.object({
      id: z.string(),
      value: z.number(),
    }),
  }),
  z.object({
    status: z.literal("error"),
    code: z.number().int(),
    message: z.string(),
  }),
]);

Cursor fell back to z.union() without the discriminated variant, which works but is slower at runtime because Zod must try each schema in sequence instead of keying directly on the discriminant. GitHub Copilot failed this test entirely, generating a flat object schema that combined all fields without union handling.

Accuracy Comparison

The testing revealed clear patterns in each tool’s strengths and weaknesses:

Feature Claude Code Cursor GitHub Copilot
Basic types Excellent Excellent Good
Email/URL validators Auto-inferred Sometimes Rarely
Nested objects Excellent Good Poor
Optional fields Excellent Inconsistent Poor
Discriminated unions Excellent Partial Fails
.refine() custom validators Good with prompt Good with prompt Requires manual
z.infer type export Always included Usually included Rarely included

Claude Code demonstrated the highest accuracy across all test cases. It correctly applied Zod methods like .email(), .datetime(), .positive(), and union types. It handled nested schemas well and made sensible assumptions about validation rules based on field names and types.

Cursor produced good results but occasionally introduced syntax errors with its auto-completions. The tool excels at understanding project context but requires careful review of generated schemas.

GitHub Copilot lagged in Zod-specific accuracy. Its suggestions often treated strings as generic strings without validation methods, and it struggled with nested object structures. It works better as a starting point that requires manual refinement.

Best Practices for AI-Generated Zod Schemas

Based on these tests, here are recommendations for getting better results from AI tools when generating Zod schemas:

Provide context explicitly. Tell the AI tool what validations you need, such as “generate a Zod schema with email validation and positive number constraints.” The more specific your instructions, the better the output.

Review generated schemas carefully. AI tools make assumptions about data formats that may not match your requirements. Check field validations, optional modifiers, and type constraints.

Use refinement for complex rules. When your validation logic goes beyond basic type checking, add Zod refinements after the AI generates the base schema:

const PasswordSchema = z.string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), {
    message: "Password must contain at least one uppercase letter",
  })
  .refine((val) => /[0-9]/.test(val), {
    message: "Password must contain at least one number",
  });

Validate the output. Always test your generated schemas with sample data to ensure they handle edge cases correctly:

const testUser = {
  id: "123",
  name: "John",
  email: "invalid-email",
  age: -5,
  isActive: true,
};

const result = UserSchema.safeParse(testUser);
if (!result.success) {
  console.log(result.error.format());
}

Prompt for error messages. By default, AI tools generate schemas with Zod’s built-in error messages. For production applications, add a follow-up prompt: “Now add custom error messages to each validation rule so users see clear feedback.” This produces schemas like:

export const UserSchema = z.object({
  name: z.string().min(2, { message: "Name must be at least 2 characters" }),
  email: z.string().email({ message: "Please enter a valid email address" }),
  age: z.number().int().min(0, { message: "Age cannot be negative" }).max(120, { message: "Age must be realistic" }),
});

When to Trust AI-Generated Schemas vs. Write Manually

AI-generated schemas are reliable for standard CRUD entity validation (users, products, orders), form input validation with common field types, and converting existing TypeScript interfaces to runtime validators. These represent the 80% accuracy case.

Manual schema writing is still necessary for custom business rules that require domain knowledge (checking that a discount percentage does not exceed a product’s margin), multi-field cross-validation using .superRefine(), schemas that interact with external API quirks or legacy formats, and async validation that calls a database or external service.

A practical workflow: use AI to generate the base schema structure, then manually add refinements for business logic, then write a test suite covering valid and invalid inputs before shipping to production.

Built by theluckystrike — More at zovo.one