Skip to content

Astro Collection Schemas — Advanced Validation with Zod

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Astro Collection Schemas. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn advanced Astro content collection schemas: Zod validation, image references, nested objects, custom types, and schema reference patterns.

In this lesson, you'll create sophisticated Zod schemas for content collections, validate images and references, define reusable schema fragments, and handle complex frontmatter structures.

What You'll Learn

Zod validation for advanced frontmatter, image references with z.image(), nested object schemas, reusable base schemas, and cross-collection references.

Why It Matters

Real-world content has complex metadata. Advanced schemas catch more errors at build time and provide richer type information for your templates.

Real-World Use

DodaTech's tutorial content uses schemas with nested author objects, image references, and version fields to ensure every page has complete metadata.

flowchart TD
    A[Base Schema] --> B[Blog Schema]
    A --> C[Doc Schema]
    B --> D[Image Ref]
    C --> E[Nested Objects]
    D --> F[Build-Time Validation]
    style A fill:#ff5a03,color:#fff

Reusable Base Schema

Define common fields in a base schema:

import { defineCollection, z, referable } from "astro:content";

const baseSchema = z.object({
  title: z.string(),
  description: z.string().min(50).max(165),
  date: z.date(),
  lastmod: z.date().optional(),
  draft: z.boolean().default(false),
  tags: z.array(z.string()).default([]),
});

const blogCollection = defineCollection({
  schema: baseSchema.extend({
    author: z.string(),
    image: z.image().optional(),
    category: z.enum(["tutorial", "news", "review"]),
  }),
});

const docsCollection = defineCollection({
  schema: baseSchema.extend({
    order: z.number(),
    category: z.enum(["getting-started", "guides", "api"]),
    related: z.array(z.string()).optional(),
  }),
});

Image References

Use z.image() for optimized image handling:

const projectCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    screenshot: z.image(),
    thumbnail: z.image().optional(),
    gallery: z.array(z.image()).default([]),
  }),
});

In frontmatter, reference images relative to the content directory:

---
title: My Project
screenshot: "./images/project-screenshot.png"
gallery:
  - "./images/screenshot-1.png"
  - "./images/screenshot-2.png"
---

Astro validates that the referenced files exist and optimizes them during build.

Nested Object Schemas

Validate complex frontmatter with nested objects:

const courseCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    instructor: z.object({
      name: z.string(),
      bio: z.string().max(300),
      avatar: z.image().optional(),
    }),
    lessons: z.array(z.object({
      title: z.string(),
      duration: z.number(),
      videoUrl: z.string().url().optional(),
    })),
  }),
});

Cross-Collection References

Reference entries from other collections:

const authorCollection = defineCollection({
  schema: z.object({
    name: z.string(),
    slug: z.string(),
  }),
});

const postCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    author: z.string().refine(async (slug) => {
      const authors = await getCollection("authors");
      return authors.some(a => a.slug === slug);
    }, "Author slug must exist"),
  }),
});

Custom Zod Methods

Add custom validation with Zod methods:

const slugSchema = z.string().regex(
  /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
  "Slug must be kebab-case"
);

const pageCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    slug: slugSchema,
    seo: z.object({
      title: z.string().max(60).optional(),
      description: z.string().max(165).optional(),
    }).optional(),
  }),
});

Common Mistakes

  1. Forgetting .extend() when reusing schemas: Use baseSchema.extend({...}) to inherit fields. Direct assignment loses the base fields.
  2. Using z.image() without the image service: Image references require the image service integration. Install @astrojs/image or use the built-in experimental image support.
  3. Making required fields optional by accident: Check that required frontmatter fields exist on every entry. Optional fields should use .optional() explicitly.
  4. Not validating URL formats: Use z.string().url() for external links and z.string().startsWith('/') for internal paths to catch invalid URLs at build time.
  5. Over-engineering schemas: Start simple with a few fields and add complexity as your content grows. Overly strict schemas frustrate content authors.

Practice Questions

  1. How do you make one schema inherit fields from another? Answer: Use .extend(). const blogSchema = baseSchema.extend({ author: z.string() }) adds the author field to the base fields.

  2. What does z.image() validate? Answer: It validates that the referenced image file exists in the expected location and enables automatic image optimization.

  3. How do you create an optional nested object? Answer: Wrap the object schema in .optional(), e.g., seo: z.object({...}).optional().

  4. Why use custom regex validation on slugs? Answer: To enforce consistent URL-safe naming (kebab-case) and catch invalid characters before deployment.

Challenge

Create a three-collection system: authors, categories, and posts. The post schema should reference author and category slugs, and validate that referenced entries exist.

Mini Project

Build a course platform content structure with collections for instructors, courses, and lessons. Include nested lesson objects with duration validation and image references for instructor avatars.

FAQ

Can I use async validation in schemas?

: Yes. z.refine() and z.superRefine() support async validators for cross-collection checks.

What happens if validation fails?

: The build fails with an error message showing the file path, field name, and validation error. No invalid content is deployed.

Can I reuse schemas across Astro projects?

: Yes. Extract shared schemas to a separate npm package or import them from a common directory.

Does Zod support custom error messages?

: Yes. Pass a string as the second argument to z.refine() or use z.string({ required_error: "Title is required" }).

What's Next

Learn about Astro Dynamic Routes to create pages with parameters and catch-all routes.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro