Skip to main content

Recipe

This recipe shows how to validate data structures using Zod (TypeScript) or Pydantic (Python), including synchronous and asynchronous validation patterns.
TypeScript: Zod version 3.22+ is supported, which provides the zod/v3 export path required by libraries like json-schema-to-zod. Python: Examples target Pydantic v2 (pydantic>=2.0).

Code example

Synchronous validation

TypeScript
import { z } from "zod";

// Define the Contract schema
const ContractSchema = z.object({
  contractId: z.string().uuid("Invalid contract ID format"),
  clientName: z.string().min(1, "Client name is required"),
  contractValue: z.number().min(0, "Contract value must be non-negative"),
  status: z.union([
    z.literal("active"),
    z.literal("completed"),
    z.literal("terminated"),
  ]),
  startDate: z.string().datetime("Invalid start date format"),
  endDate: z.string().datetime("Invalid end date format").optional(),
});

// Infer TypeScript type from schema
type Contract = z.infer<typeof ContractSchema>;

// Example data
const validContract = {
  contractId: "550e8400",
  clientName: "Client X",
  contractValue: 50000,
  status: "active" as const,
  startDate: "2024-01-01T00:00:00Z",
  endDate: "2024-12-31T23:59:59Z",
};

const invalidContract = {
  contractId: "invalid-uuid",
  clientName: "",
  contractValue: -1000, // invalid: negative value
  status: "active" as const,
  startDate: "2024-01-01T00:00:00Z",
};

// Synchronous validation with safeParse()

const result1 = ContractSchema.safeParse(validContract);
if (!result1.success) {
  console.error("Validation errors:", result1.error.format());
} else {
  console.log("✓ Valid contract:", result1.data);
}

// Handling validation errors
const result2 = ContractSchema.safeParse(invalidContract);
if (!result2.success) {
  console.error("Validation errors:", result2.error.format());
} else {
  console.log("Valid contract:", result2.data);
}

// Alternative: parse() method (throws on error)
try {
  const validated = ContractSchema.parse(validContract);
  console.log("\n✓ Parsed successfully:", validated);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error("Validation failed:", error.issues);
  }
}

Asynchronous validation

TypeScript
import { z } from "zod";

const asyncContract = {
  contractId: "660e8400",
  clientName: "Client Y",
  contractValue: 75000,
  status: "active" as const,
  startDate: "2024-06-01T00:00:00Z",
};

// Asynchronous validation with refinements

const AsyncContractSchema = ContractSchema.extend({
  clientName: z.string().refine(
    async (val) => {
      // Simulate database check for duplicate client
      await new Promise((resolve) => setTimeout(resolve, 100));
      return val !== "Client X"; // Fail if client already has active contract
    },
    { message: "Client already has an active contract" }
  ),
});

AsyncContractSchema.safeParseAsync(asyncContract)
  .then((result) => {
    if (!result.success) {
      console.error("✗ Async validation errors:", result.error.format());
    } else {
      console.log("✓ Valid contract (async):", result.data);
    }
  })
  .catch((err) => console.error("Unexpected error:", err));
Pydantic validators run synchronously. For async checks (like database lookups), validate the data structure first with model_validate(), then run async logic afterward—as shown above.

Comparison: TypeScript vs Python

PatternTypeScript (Zod)Python (Pydantic)
Safe validation (no throw)safeParse(){ success, data, error }try/except ValidationError
Strict validation (throws)parse() → throws ZodErrormodel_validate() → raises ValidationError
Async validation.safeParseAsync() / .refine(async fn)Validate sync first, then run async checks
Skip validationN/Amodel_construct()
Access errorserror.format() / error.issuese.errors() → list of dicts

Common pitfalls

Date parsing — Pydantic automatically parses ISO 8601 strings into datetime objects. Watch out for timezone-aware vs. naive datetimes: "2024-01-01T00:00:00Z" becomes timezone-aware, while "2024-01-01T00:00:00" is naive. Mixing them raises an error. Optional fields — Mark optional fields with Optional[X] = None (or X | None = None in Python 3.10+). A field typed as Optional[X] without a default is still required on input. Custom validators — Use @field_validator for single-field checks and @model_validator(mode='after') for cross-field checks:
from pydantic import BaseModel, field_validator, model_validator


class Contract(BaseModel):
    start_date: datetime
    end_date: Optional[datetime] = None

    @field_validator("client_name")
    @classmethod
    def name_must_not_be_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Client name cannot be blank")
        return v.strip()

    @model_validator(mode="after")
    def end_after_start(self) -> "Contract":
        if self.end_date and self.end_date <= self.start_date:
            raise ValueError("end_date must be after start_date")
        return self

TypeScript SDK

Learn about the Intuned Browser SDK for TypeScript.

Python SDK

Learn about the Intuned Browser SDK for Python.