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
/**
* 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.
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.
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
// 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
// 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
// Basic boolean
z.boolean()
// With default value
z.boolean().default(false)
Date/Time Fields
Date/Time Fields
// 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
// Enum values from Strapi
z.enum(['draft', 'published', 'archived'])
Media Fields
Media Fields
// 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
// 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
// Single component
z.record(z.unknown()).nullable()
// Repeatable component
z.array(z.record(z.unknown()))
Dynamic Zones
Dynamic Zones
// 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
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
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
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: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:// 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(),
}),
])
// 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
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
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
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.Next Steps
Services
Use validated data with generated services
Relations
Validate related content references
Media
Upload files and validate media IDs
Types
Understand TypeScript type generation