What Gets Generated
For each content type, strapi2front creates Zod schemas that:Validate Structure
Ensure data matches your Strapi schema structure
Enforce Constraints
Min/max length, number ranges, email format, etc.
Type Inference
Generate TypeScript types from schemas automatically
Custom Messages
Add custom validation error messages
Generated Schema Example
For an Article collection type:article/schemas.ts
Copy
Ask AI
/**
* 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
- Create Schemas
- Update Schemas
Validates data for creating new entries. Required fields are enforced.
Copy
Ask AI
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);
}
Validates data for updating entries. All fields are optional for partial updates.
Copy
Ask AI
const result = articleUpdateSchema.safeParse({
status: 'published',
publishedAt: new Date().toISOString(),
});
if (result.success) {
await articleService.update('abc123', result.data);
}
Field Type Mappings
strapi2front maps Strapi field types to Zod schemas:String Fields
String Fields
Copy
Ask AI
// 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-]+$/)
Number Fields
Number Fields
Copy
Ask AI
// 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()
Boolean Fields
Boolean Fields
Copy
Ask AI
// Basic boolean
z.boolean()
// With default value
z.boolean().default(false)
Date/Time Fields
Date/Time Fields
Copy
Ask AI
// Date (YYYY-MM-DD)
z.string().date()
// Time (HH:MM:SS)
z.string().time()
// DateTime with timezone
z.string().datetime({ offset: true })
Enumeration Fields
Enumeration Fields
Copy
Ask AI
// Enum values from Strapi
z.enum(['draft', 'published', 'archived'])
Media Fields
Media Fields
Copy
Ask AI
// Single media (accepts file ID from upload API)
z.number().int().positive().nullable()
// Multiple media
z.array(z.number().int().positive()).default([])
Media fields accept file IDs (numbers), not file objects. Upload files first via the upload API, then use the returned IDs.
Relation Fields
Relation Fields
Copy
Ask AI
// 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
Component Fields
Component Fields
Copy
Ask AI
// Single component
z.record(z.unknown()).nullable()
// Repeatable component
z.array(z.record(z.unknown()))
Dynamic Zones
Dynamic Zones
Copy
Ask AI
// 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') }),
])
)
Using Schemas in Forms
React Hook Form Example
components/ArticleForm.tsx
Copy
Ask AI
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
actions/article.ts
Copy
Ask AI
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
Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
// 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(),
}),
])
Copy
Ask AI
// 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
Copy
Ask AI
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
Copy
Ask AI
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
components/CreateArticleWizard.tsx
Copy
Ask AI
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>
);
}
Zod schemas are automatically regenerated when you run
npx strapi2front sync. Keep them in sync with your Strapi schema.