> ## Documentation Index
> Fetch the complete documentation index at: https://docs.strapi2front.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Generated Zod Schemas

> Type-safe validation schemas for forms, API inputs, and data validation

strapi2front generates Zod validation schemas from your Strapi content types, providing runtime type safety and validation for forms, API inputs, and more.

## What Gets Generated

For each content type, strapi2front creates Zod schemas that:

<CardGroup cols={2}>
  <Card title="Validate Structure" icon="check-circle">
    Ensure data matches your Strapi schema structure
  </Card>

  <Card title="Enforce Constraints" icon="shield">
    Min/max length, number ranges, email format, etc.
  </Card>

  <Card title="Type Inference" icon="wand-magic-sparkles">
    Generate TypeScript types from schemas automatically
  </Card>

  <Card title="Custom Messages" icon="message">
    Add custom validation error messages
  </Card>
</CardGroup>

## Generated Schema Example

For an **Article** collection type:

```typescript article/schemas.ts theme={null}
/**
 * Article Zod Schemas
 * Blog article with author and categories
 * Generated by strapi2front
 */

import { z } from 'zod';

/**
 * Schema for creating a new Article
 */
export const articleCreateSchema = z.object({
  title: z.string().min(1).max(255),
  slug: z.string(),
  content: z.array(
    z.discriminatedUnion('type', [
      z.object({
        type: z.literal('paragraph'),
        children: z.array(z.object({
          type: z.literal('text'),
          text: z.string(),
        })),
      }),
      z.object({
        type: z.literal('heading'),
        level: z.number().int().min(1).max(6),
        children: z.array(z.object({
          type: z.literal('text'),
          text: z.string(),
        })),
      }),
      z.object({
        type: z.literal('list'),
        format: z.enum(['ordered', 'unordered']),
        children: z.array(z.object({
          type: z.literal('list-item'),
          children: z.array(z.object({
            type: z.literal('text'),
            text: z.string(),
          })),
        })),
      }),
    ])
  ),
  excerpt: z.string().optional(),
  coverImage: z.number().int().positive().nullable(),
  status: z.enum(['draft', 'published', 'archived']),
  views: z.number().int(),
  readingTime: z.number().int().optional(),
  featured: z.boolean().default(false),
  author: z.string().nullable(),  // documentId in v5
  categories: z.array(z.string()).default([]),  // array of documentIds
});

/**
 * Schema for updating an Article
 * All fields are optional for partial updates
 */
export const articleUpdateSchema = z.object({
  title: z.string().min(1).max(255).optional(),
  slug: z.string().optional(),
  content: z.array(z.any()).optional(),
  excerpt: z.string().optional(),
  coverImage: z.number().int().positive().nullable().optional(),
  status: z.enum(['draft', 'published', 'archived']).optional(),
  views: z.number().int().optional(),
  readingTime: z.number().int().optional(),
  featured: z.boolean().optional(),
  author: z.string().nullable().optional(),
  categories: z.array(z.string()).optional(),
});

// Type inference helpers
export type ArticleCreateInput = z.infer<typeof articleCreateSchema>;
export type ArticleUpdateInput = z.infer<typeof articleUpdateSchema>;
```

## Schema Types

<Tabs>
  <Tab title="Create Schemas">
    Validates data for creating new entries. Required fields are enforced.

    ```typescript theme={null}
    const result = articleCreateSchema.safeParse({
      title: 'New Article',
      slug: 'new-article',
      content: [],
      status: 'draft',
      views: 0,
      coverImage: null,
      author: null,
      categories: [],
    });

    if (result.success) {
      await articleService.create(result.data);
    } else {
      console.error(result.error.errors);
    }
    ```
  </Tab>

  <Tab title="Update Schemas">
    Validates data for updating entries. All fields are optional for partial updates.

    ```typescript theme={null}
    const result = articleUpdateSchema.safeParse({
      status: 'published',
      publishedAt: new Date().toISOString(),
    });

    if (result.success) {
      await articleService.update('abc123', result.data);
    }
    ```
  </Tab>
</Tabs>

## Field Type Mappings

strapi2front maps Strapi field types to Zod schemas:

<AccordionGroup>
  <Accordion title="String Fields">
    ```typescript theme={null}
    // Basic string
    z.string()

    // With min/max length from Strapi
    z.string().min(3).max(100)

    // Email
    z.string().email()

    // With regex pattern
    z.string().regex(/^[a-z0-9-]+$/)
    ```
  </Accordion>

  <Accordion title="Number Fields">
    ```typescript theme={null}
    // Integer
    z.number().int()

    // Float/Decimal
    z.number()

    // With min/max from Strapi
    z.number().int().min(0).max(100)

    // Positive numbers
    z.number().int().positive()
    ```
  </Accordion>

  <Accordion title="Boolean Fields">
    ```typescript theme={null}
    // Basic boolean
    z.boolean()

    // With default value
    z.boolean().default(false)
    ```
  </Accordion>

  <Accordion title="Date/Time Fields">
    ```typescript theme={null}
    // Date (YYYY-MM-DD)
    z.string().date()

    // Time (HH:MM:SS)
    z.string().time()

    // DateTime with timezone
    z.string().datetime({ offset: true })
    ```
  </Accordion>

  <Accordion title="Enumeration Fields">
    ```typescript theme={null}
    // Enum values from Strapi
    z.enum(['draft', 'published', 'archived'])
    ```
  </Accordion>

  <Accordion title="Media Fields">
    ```typescript theme={null}
    // Single media (accepts file ID from upload API)
    z.number().int().positive().nullable()

    // Multiple media
    z.array(z.number().int().positive()).default([])
    ```

    <Info>
      Media fields accept **file IDs** (numbers), not file objects. Upload files first via the upload API, then use the returned IDs.
    </Info>
  </Accordion>

  <Accordion title="Relation Fields">
    ```typescript theme={null}
    // To-one relation (Strapi v5: documentId string, v4: id number)
    z.string().nullable()  // v5
    z.number().int().positive().nullable()  // v4

    // To-many relation
    z.array(z.string()).default([])  // v5
    z.array(z.number().int().positive()).default([])  // v4
    ```
  </Accordion>

  <Accordion title="Component Fields">
    ```typescript theme={null}
    // Single component
    z.record(z.unknown()).nullable()

    // Repeatable component
    z.array(z.record(z.unknown()))
    ```
  </Accordion>

  <Accordion title="Dynamic Zones">
    ```typescript theme={null}
    // Discriminated union of component types
    z.array(
      z.discriminatedUnion('__component', [
        textBlockSchema.extend({ __component: z.literal('content.text-block') }),
        imageBlockSchema.extend({ __component: z.literal('content.image-block') }),
      ])
    )
    ```
  </Accordion>
</AccordionGroup>

## Using Schemas in Forms

### React Hook Form Example

```typescript components/ArticleForm.tsx theme={null}
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { articleCreateSchema, type ArticleCreateInput } from '@/strapi/collections/article/schemas';
import { articleService } from '@/strapi/collections/article/service';

export function ArticleForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<ArticleCreateInput>({
    resolver: zodResolver(articleCreateSchema),
  });
  
  const onSubmit = async (data: ArticleCreateInput) => {
    const article = await articleService.create(data);
    console.log('Created:', article);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Title</label>
        <input {...register('title')} />
        {errors.title && <span>{errors.title.message}</span>}
      </div>
      
      <div>
        <label>Slug</label>
        <input {...register('slug')} />
        {errors.slug && <span>{errors.slug.message}</span>}
      </div>
      
      <div>
        <label>Status</label>
        <select {...register('status')}>
          <option value="draft">Draft</option>
          <option value="published">Published</option>
          <option value="archived">Archived</option>
        </select>
        {errors.status && <span>{errors.status.message}</span>}
      </div>
      
      <button type="submit">Create Article</button>
    </form>
  );
}
```

### Astro Actions Example

```typescript actions/article.ts theme={null}
import { defineAction } from 'astro:actions';
import { articleCreateSchema } from '@/strapi/collections/article/schemas';
import { articleService } from '@/strapi/collections/article/service';

export const createArticle = defineAction({
  accept: 'form',
  input: articleCreateSchema,
  handler: async (input) => {
    const article = await articleService.create(input);
    return { success: true, article };
  },
});
```

### Manual Validation

```typescript theme={null}
import { articleCreateSchema } from '@/strapi/collections/article/schemas';

const formData = {
  title: 'My Article',
  slug: 'my-article',
  status: 'draft',
  // ...
};

// Use safeParse for error handling
const result = articleCreateSchema.safeParse(formData);

if (result.success) {
  // Data is valid and typed
  const validData = result.data;
  await articleService.create(validData);
} else {
  // Validation failed
  console.error(result.error.errors);
  // [
  //   { path: ['title'], message: 'String must contain at least 1 character(s)' },
  //   { path: ['views'], message: 'Required' }
  // ]
}

// Use parse to throw on error
try {
  const validData = articleCreateSchema.parse(formData);
  await articleService.create(validData);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error(error.errors);
  }
}
```

## Custom Validation

Extend generated schemas with custom validation:

```typescript theme={null}
import { articleCreateSchema } from '@/strapi/collections/article/schemas';

// Add custom validation rules
const customArticleSchema = articleCreateSchema.extend({
  title: z.string()
    .min(1, 'Title is required')
    .max(255, 'Title too long')
    .refine(
      (title) => !title.toLowerCase().includes('spam'),
      'Title contains forbidden words'
    ),
  
  slug: z.string().regex(
    /^[a-z0-9-]+$/,
    'Slug must contain only lowercase letters, numbers, and hyphens'
  ),
}).refine(
  (data) => data.excerpt || data.content.length > 0,
  'Either excerpt or content must be provided'
);

// Use the custom schema
const result = customArticleSchema.safeParse(formData);
```

## Advanced Relation Schemas (v5)

For Strapi v5, schemas support advanced relation operations:

```typescript theme={null}
// Simple format (array of documentIds)
z.array(z.string())

// Advanced format with connect/disconnect/set
z.union([
  // Shorthand: array of documentIds
  z.array(z.string()),
  
  // Longhand: object with operations
  z.object({
    /** Add relations while preserving existing ones */
    connect: z.array(
      z.union([
        z.string(),  // Just documentId
        z.object({
          documentId: z.string(),
          locale: z.string().optional(),
          status: z.enum(['draft', 'published']).optional(),
          position: z.object({
            before: z.string().optional(),
            after: z.string().optional(),
            start: z.boolean().optional(),
            end: z.boolean().optional(),
          }).optional(),
        }),
      ])
    ).optional(),
    
    /** Remove specific relations */
    disconnect: z.array(
      z.union([
        z.string(),
        z.object({
          documentId: z.string(),
          locale: z.string().optional(),
          status: z.enum(['draft', 'published']).optional(),
        }),
      ])
    ).optional(),
    
    /** Replace ALL relations */
    set: z.array(z.string()).optional(),
  }),
])
```

Usage example:

```typescript theme={null}
// Simple: just set the relations
const simpleData = {
  categories: ['cat-1', 'cat-2'],
};

// Advanced: add and remove specific items
const advancedData = {
  categories: {
    connect: [{ documentId: 'new-cat', position: { start: true } }],
    disconnect: [{ documentId: 'old-cat' }],
  },
};

// Both validate successfully
articleUpdateSchema.parse(simpleData);
articleUpdateSchema.parse(advancedData);
```

## Error Handling

### Display Validation Errors

```typescript theme={null}
import { z } from 'zod';
import { articleCreateSchema } from '@/strapi/collections/article/schemas';

function displayErrors(error: z.ZodError) {
  return error.errors.map(err => ({
    field: err.path.join('.'),
    message: err.message,
  }));
}

const result = articleCreateSchema.safeParse(invalidData);

if (!result.success) {
  const errors = displayErrors(result.error);
  // [
  //   { field: 'title', message: 'String must contain at least 1 character(s)' },
  //   { field: 'status', message: "Invalid enum value. Expected 'draft' | 'published' | 'archived'" }
  // ]
}
```

### Custom Error Messages

```typescript theme={null}
const customSchema = articleCreateSchema.extend({
  title: z.string()
    .min(1, { message: 'Please enter a title' })
    .max(255, { message: 'Title cannot exceed 255 characters' }),
  
  status: z.enum(['draft', 'published', 'archived'], {
    errorMap: () => ({ message: 'Please select a valid status' }),
  }),
});
```

## Real-World Example: Multi-Step Form

```typescript components/CreateArticleWizard.tsx theme={null}
import { useState } from 'react';
import { articleCreateSchema } from '@/strapi/collections/article/schemas';
import type { ArticleCreateInput } from '@/strapi/collections/article/schemas';
import { articleService } from '@/strapi/collections/article/service';

// Split schema into steps
const step1Schema = articleCreateSchema.pick({
  title: true,
  slug: true,
  excerpt: true,
});

const step2Schema = articleCreateSchema.pick({
  content: true,
  coverImage: true,
});

const step3Schema = articleCreateSchema.pick({
  status: true,
  author: true,
  categories: true,
});

export function CreateArticleWizard() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState<Partial<ArticleCreateInput>>({});
  
  const validateStep = (stepNum: number, data: unknown) => {
    const schemas = [step1Schema, step2Schema, step3Schema];
    return schemas[stepNum - 1].safeParse(data);
  };
  
  const handleNext = (stepData: unknown) => {
    const result = validateStep(step, stepData);
    
    if (result.success) {
      setFormData({ ...formData, ...result.data });
      setStep(step + 1);
    } else {
      console.error(result.error.errors);
    }
  };
  
  const handleSubmit = async (finalStepData: unknown) => {
    const finalData = { ...formData, ...finalStepData };
    const result = articleCreateSchema.safeParse(finalData);
    
    if (result.success) {
      await articleService.create(result.data);
    } else {
      console.error(result.error.errors);
    }
  };
  
  return (
    <div>
      {step === 1 && <Step1Form onNext={handleNext} />}
      {step === 2 && <Step2Form onNext={handleNext} />}
      {step === 3 && <Step3Form onSubmit={handleSubmit} />}
    </div>
  );
}
```

<Tip>
  Zod schemas are automatically regenerated when you run `npx strapi2front sync`. Keep them in sync with your Strapi schema.
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="Services" icon="code" href="/guides/services">
    Use validated data with generated services
  </Card>

  <Card title="Relations" icon="link" href="/guides/relations">
    Validate related content references
  </Card>

  <Card title="Media" icon="image" href="/guides/media">
    Upload files and validate media IDs
  </Card>

  <Card title="Types" icon="shield" href="/guides/types">
    Understand TypeScript type generation
  </Card>
</CardGroup>
