Zod-driven schema

Identical UI produced from a Zod schema via z.toJSONSchema(S). Zod is optional — use it when you want a single source of truth for rendering and validation.

Used for sign-in and account recovery.
3-24 letters, numbers, or underscores.
Optional — link to your site or profile.
You can upgrade or downgrade any time.
Billing cycle
Only relevant for team plans.
Topics you care aboutPick at least one.
Profile
Up to 280 characters.
examples/zod-schema.ts
/**
 * The same shape as `./schema.ts`, built with Zod and converted to JSON Schema.
 * Shown side-by-side in the Zod demo.
 */
import { z } from "zod";
import type { ObjectSchema } from "../src/render.tsx";

const S = z.object({
  email: z
    .email()
    .meta({ uiName: "Email" })
    .describe("Used for sign-in and account recovery."),
  handle: z
    .string()
    .min(3)
    .max(24)
    .regex(/^[a-zA-Z0-9_]+$/)
    .meta({ uiName: "Handle" })
    .describe("3-24 letters, numbers, or underscores."),
  homepage: z
    .url()
    .optional()
    .meta({ uiName: "Homepage" })
    .describe("Optional — link to your site or profile."),
  plan: z
    .enum(["free", "pro", "team"])
    .default("pro")
    .meta({ uiWidget: "select", uiName: "Plan" })
    .describe("You can upgrade or downgrade any time."),
  billing: z
    .enum(["monthly", "yearly"])
    .default("yearly")
    .meta({ uiName: "Billing cycle" }),
  seats: z
    .int()
    .min(1)
    .max(50)
    .default(5)
    .meta({ uiWidget: "range", uiName: "Seats" })
    .describe("Only relevant for team plans."),
  notifications: z
    .boolean()
    .default(true)
    .meta({ uiName: "Email me product updates" }),
  topics: z
    .array(z.enum(["frontend", "backend", "design", "ops"]))
    .default(["frontend", "backend"])
    .meta({ uiName: "Topics you care about" })
    .describe("Pick at least one."),
  channels: z
    .array(z.enum(["email", "in-app", "sms", "push", "rss"]))
    .default(["email", "in-app"])
    .meta({ uiWidget: "select", uiName: "Notification channels" }),
  profile: z
    .object({
      name: z.string().min(1).meta({ uiName: "Full name" }),
      bio: z
        .string()
        .max(280)
        .optional()
        .meta({ uiWidget: "textarea", uiName: "Short bio" })
        .describe("Up to 280 characters."),
    })
    .meta({ uiName: "Profile" }),
});

export const zodSchema = z.toJSONSchema(S) as ObjectSchema;
examples/schema.ts (plain JSON Schema equivalent)
/**
 * A realistic sign-up / profile schema, shared across most demos to keep
 * them comparable: the only thing that changes between pages is the CSS.
 */
import type { ObjectSchema } from "../src/render.tsx";

export const schema: ObjectSchema = {
  type: "object",
  required: ["email", "handle", "plan", "topics"],
  properties: {
    email: {
      type: "string",
      format: "email",
      uiName: "Email",
      description: "Used for sign-in and account recovery.",
    },
    handle: {
      type: "string",
      minLength: 3,
      maxLength: 24,
      pattern: "^[a-zA-Z0-9_]+$",
      uiName: "Handle",
      description: "3-24 letters, numbers, or underscores.",
    },
    homepage: {
      type: "string",
      format: "uri",
      uiName: "Homepage",
      description: "Optional — link to your site or profile.",
    },
    plan: {
      type: "string",
      enum: ["free", "pro", "team"],
      default: "pro",
      uiWidget: "select",
      uiName: "Plan",
      description: "You can upgrade or downgrade any time.",
    },
    billing: {
      type: "string",
      enum: ["monthly", "yearly"],
      default: "yearly",
      uiName: "Billing cycle",
    },
    seats: {
      type: "integer",
      minimum: 1,
      maximum: 50,
      default: 5,
      uiWidget: "range",
      uiName: "Seats",
      description: "Only relevant for team plans.",
    },
    notifications: {
      type: "boolean",
      default: true,
      uiName: "Email me product updates",
    },
    topics: {
      type: "array",
      items: { type: "string", enum: ["frontend", "backend", "design", "ops"] },
      default: ["frontend", "backend"],
      uiName: "Topics you care about",
      description: "Pick at least one.",
    },
    channels: {
      type: "array",
      items: {
        type: "string",
        enum: ["email", "in-app", "sms", "push", "rss"],
      },
      default: ["email", "in-app"],
      uiWidget: "select",
      uiName: "Notification channels",
    },
    profile: {
      type: "object",
      uiName: "Profile",
      properties: {
        name: {
          type: "string",
          minLength: 1,
          uiName: "Full name",
        },
        bio: {
          type: "string",
          uiWidget: "textarea",
          maxLength: 280,
          uiName: "Short bio",
          description: "Up to 280 characters.",
        },
      },
    },
  },
};