> ## Documentation Index
> Fetch the complete documentation index at: https://intunedhq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate data with schemas

## Recipe

This recipe shows how to validate data structures using [Zod](https://zod.dev) (TypeScript) or [Pydantic](https://docs.pydantic.dev) (Python), including synchronous and asynchronous validation patterns.

<Info>
  **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`).
</Info>

## Code example

### Synchronous validation

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  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-e29b-41d4-a716-446655440000",
    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);
    }
  }
  ```

  ```python Python theme={null}
  from pydantic import BaseModel, Field, ValidationError
  from typing import Literal, Optional
  from datetime import datetime


  # Define the Contract model
  class Contract(BaseModel):
      contract_id: str
      client_name: str = Field(min_length=1, description="Client name is required")
      contract_value: float = Field(ge=0, description="Must be non-negative")
      status: Literal["active", "completed", "terminated"]
      start_date: datetime
      end_date: Optional[datetime] = None


  # Example data
  valid_contract = {
      "contract_id": "550e8400",
      "client_name": "Client X",
      "contract_value": 50000,
      "status": "active",
      "start_date": "2024-01-01T00:00:00Z",
      "end_date": "2024-12-31T23:59:59Z",
  }

  invalid_contract = {
      "contract_id": "invalid-uuid",
      "client_name": "",
      "contract_value": -1000,  # invalid: negative value
      "status": "active",
      "start_date": "2024-01-01T00:00:00Z",
  }

  # Validate with model_validate() — raises ValidationError on failure
  try:
      contract = Contract.model_validate(valid_contract)
      print("✓ Valid contract:", contract)
  except ValidationError as e:
      print("Validation errors:", e.errors())

  # Handling validation errors
  try:
      contract = Contract.model_validate(invalid_contract)
  except ValidationError as e:
      for error in e.errors():
          print(f"  - {error['loc']}: {error['msg']}")

  # Alternative: model_construct() skips validation (use only for trusted data)
  trusted = Contract.model_construct(**valid_contract)
  print("\n✓ Constructed without validation:", trusted)
  ```
</CodeGroup>

### Asynchronous validation

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  import { z } from "zod";

  const asyncContract = {
    contractId: "550e8400-e29b-41d4-a716-446655440000",
    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));
  ```

  ```python Python theme={null}
  from pydantic import BaseModel, Field, ValidationError
  from typing import Literal, Optional
  from datetime import datetime
  import asyncio


  class Contract(BaseModel):
      contract_id: str
      client_name: str = Field(min_length=1)
      contract_value: float = Field(ge=0)
      status: Literal["active", "completed", "terminated"]
      start_date: datetime
      end_date: Optional[datetime] = None


  async def check_no_duplicate_client(client_name: str) -> bool:
      """Simulate a database check for duplicate clients."""
      await asyncio.sleep(0.1)
      return client_name != "Client X"  # Fail if client already has active contract


  async def validate_contract_async(data: dict) -> Contract:
      # Step 1: structural validation (Pydantic validators are synchronous)
      contract = Contract.model_validate(data)

      # Step 2: async business logic checks after structural validation passes
      if not await check_no_duplicate_client(contract.client_name):
          raise ValueError("Client already has an active contract")

      return contract


  async def main():
      async_contract = {
          "contract_id": "660e8400",
          "client_name": "Client Y",
          "contract_value": 75000,
          "status": "active",
          "start_date": "2024-06-01T00:00:00Z",
      }

      try:
          contract = await validate_contract_async(async_contract)
          print("✓ Valid contract (async):", contract)
      except (ValidationError, ValueError) as e:
          print("✗ Validation error:", e)


  asyncio.run(main())
  ```
</CodeGroup>

<Info>
  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.
</Info>

## Comparison: TypeScript vs Python

| Pattern                    | TypeScript (Zod)                           | Python (Pydantic)                             |
| -------------------------- | ------------------------------------------ | --------------------------------------------- |
| Safe validation (no throw) | `safeParse()` → `{ success, data, error }` | `try/except ValidationError`                  |
| Strict validation (throws) | `parse()` → throws `ZodError`              | `model_validate()` → raises `ValidationError` |
| Async validation           | `.safeParseAsync()` / `.refine(async fn)`  | Validate sync first, then run async checks    |
| Skip validation            | N/A                                        | `model_construct()`                           |
| Access errors              | `error.format()` / `error.issues`          | `e.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:

```python theme={null}
from pydantic import BaseModel, field_validator, model_validator


class Contract(BaseModel):
    client_name: str
    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
```

## Related

<CardGroup cols={2}>
  <Card title="TypeScript SDK" href="/automation-sdks/overview">
    Learn about the Intuned Browser SDK for TypeScript.
  </Card>

  <Card title="Python SDK" href="/automation-sdks/intuned-sdk/python/overview">
    Learn about the Intuned Browser SDK for Python.
  </Card>
</CardGroup>
