codez.guru

The Scenario

You’re building a blog editor where authors submit the following data:

  • title: required, min 5 chars
  • body: required, min 50 chars
  • tags: array of unique strings (1–5 items)
  • metadata: includes published, publishedAt, and slug
  • If published is true, publishedAt and slug must be set

Let’s build a robust schema for this.


Building the BlogPostSchema

import { z } from "zod";

export const BlogPostSchema = z
  .object({
    title: z.string().min(5, "Title must be at least 5 characters"),
    body: z.string().min(50, "Content must be at least 50 characters"),
    tags: z
      .array(z.string().min(2))
      .min(1)
      .max(5)
      .refine((tags) => new Set(tags).size === tags.length, {
        message: "Tags must be unique",
      }),
    metadata: z.object({
      published: z.boolean(),
      publishedAt: z.string().optional(), // ISO string expected
      slug: z.string().optional(),
    }),
  })
  .superRefine((data, ctx) => {
    if (data.metadata.published) {
      if (!data.metadata.publishedAt) {
        ctx.addIssue({
          path: ["metadata", "publishedAt"],
          code: "custom",
          message: "Published date is required when published",
        });
      }

      if (!data.metadata.slug) {
        ctx.addIssue({
          path: ["metadata", "slug"],
          code: "custom",
          message: "Slug is required when published",
        });
      }
    }
  });

Handling Tags and Rich Content

Zod lets you validate both structure and content:

tags: z
  .array(z.string().min(2, "Each tag must be at least 2 characters"))
  .min(1)
  .max(5)

Add .refine() to enforce uniqueness.


Validating Nested Metadata

The metadata object is fully typed and validated:

metadata: z.object({
  published: z.boolean(),
  publishedAt: z.string().optional(),
  slug: z.string().optional(),
})

Use .superRefine() for cross-field logic like:

  • If published is true, then both publishedAt and slug must be filled.

Optional Draft Logic with Refinement

Want to allow drafts with just title and body?

if (!data.metadata.published) {
  // Allow missing `publishedAt` and `slug`
}

That’s the beauty of .superRefine() β€” you control the logic, not just the shape.


Summary

  • This schema combines strings, arrays, nested objects, optional fields, and cross-field rules
  • Zod helps enforce business logic cleanly and predictably
  • Perfect for CMS, editors, admin panels, or custom content tools

Next: Lesson 19 – Zod + React Hook Form Integration for Full Form Validation


Master the Code, Be the Guru!