Fetch
Create Freestyle Fetch

Validation

Runtime validation with Zod in generated API clients.

Overview

The create-freestyle-fetch creates Zod schemas for all data types in your API specification.

These schemas provide runtime validation for requests and responses, ensuring type safety at both compile-time and runtime.

Automatic Validation

A. Response Validation

All responses are automatically validated before being returned:

const user = await api.users.$userId.GET({
  params: { userId: '123' }
})

// If the response doesn't match the User schema, Zod throws an error
// Otherwise, user is guaranteed to match UserModel

The validation happens in the .def_response() method:

.def_response(async ({ json }) => Model.User.parse(await json()))

B. Request Body Validation

Request bodies are validated using the .def_body() method:

POST: f.builder()
  .def_json()
  .def_body(Model.CreateUser.parse)

When you make a request, the body is validated:

await api.users.POST({
  body: {
    username: 'john',
    email: 'invalid-email' // ❌ Zod error: Invalid email
  }
})

C. Query Parameter Validation

Query parameters are validated with .def_searchparams():

GET: f.builder()
  .def_json()
  .def_searchparams(z.object({
    page: z.number().optional(),
    limit: z.number().max(100).optional()
  }).parse)

Invalid query parameters throw errors:

await api.users.GET({
  query: {
    page: 1,
    limit: 200 // ❌ Zod error: Number must be less than or equal to 100
  }
})

Handling Validation Errors

A. Zod Error Structure

When validation fails, Zod throws a ZodError:

import { ZodError } from 'zod'

try {
  const user = await api.users.$userId.GET({
    params: { userId: '123' }
  })
} catch (error) {
  if (error instanceof ZodError) {
    console.error('Validation failed:', error.errors)
    // [{ path: ['email'], message: 'Invalid email' }]
  }
}

B. Custom Error Handling

Use the fetch library's error handlers for more control:

const user = await api.users.$userId.GET({
  params: { userId: '123' }
}).fetch({
  onError: (error) => {
    if (error instanceof ZodError) {
      // Handle validation errors
      console.error('Data validation failed:', error.flatten())
    } else {
      // Handle other errors
      console.error('Request failed:', error)
    }
  }
})

Schema Constraints

The generator translates OpenAPI constraints to Zod:

A. String Constraints

// OpenAPI
{
  "type": "string",
  "minLength": 3,
  "maxLength": 50,
  "pattern": "^[a-zA-Z]+$"
}

// Generated Zod
z.string().min(3).max(50).regex(/^[a-zA-Z]+$/)

B. Number Constraints

// OpenAPI
{
  "type": "number",
  "minimum": 0,
  "maximum": 100
}

// Generated Zod
z.number().min(0).max(100)

C. Array Constraints

// OpenAPI
{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "maxItems": 10
}

// Generated Zod
z.array(z.string()).min(1).max(10)

D. Object Constraints

// OpenAPI
{
  "type": "object",
  "required": ["name", "email"],
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer" }
  }
}

// Generated Zod
z.object({
  name: z.string(),
  email: z.email(),
  age: z.number().int().optional()
})

Advanced Validation

A. Nullable Types (OAS 3.1)

The generator supports OpenAPI 3.1 nullable types:

// OpenAPI
{
  "type": ["string", "null"]
}

// Generated Zod
z.string().nullable()

B. Enums

Enums are converted to Zod enums:

// OpenAPI
{
  "type": "string",
  "enum": ["active", "inactive", "pending"]
}

// Generated Zod
z.enum(['active', 'inactive', 'pending'])

C. Discriminated Unions

The generator creates discriminated unions for oneOf with discriminators:

// OpenAPI
{
  "oneOf": [
    { "$ref": "#/components/schemas/Cat" },
    { "$ref": "#/components/schemas/Dog" }
  ],
  "discriminator": {
    "propertyName": "type"
  }
}

// Generated Zod
z.discriminatedUnion('type', [Cat, Dog])

D. Intersection Types

allOf creates intersection types:

// OpenAPI
{
  "allOf": [
    { "$ref": "#/components/schemas/BaseEntity" },
    { "$ref": "#/components/schemas/Timestamped" }
  ]
}

// Generated Zod
BaseEntity.and(Timestamped)

Performance Considerations

A. Schema Caching

Zod schemas are created once and reused for all requests:

// The schema is defined once
export const User = z.object({
  id: z.string(),
  name: z.string()
})

// And reused for every request
Model.User.parse(data) // No schema recreation

B. Lazy Validation

Consider using .safeParse() for non-critical validations:

const result = Model.User.safeParse(data)

if (result.success) {
  const user = result.data
} else {
  console.warn('Invalid user data:', result.error)
}