UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

494 lines (354 loc) 16.3 kB
# Migration Guide: Chanfana v2 to v3 (Zod v4) This guide helps you migrate from chanfana v2 (Zod v3) to chanfana v3 (Zod v4). Chanfana v3 brings Zod v4 support with improved tree-shakeability and better performance to your API projects. ## What Changed Chanfana v3 has been updated to use Zod v4 and `@asteasolutions/zod-to-openapi` v8. While most of the chanfana API remains the same, there are some important changes to be aware of. ## Breaking Changes ### Error Message Formats Zod v4 improved error messages to be more descriptive and consistent. If your application parses or depends on specific error message formats, you'll need to update them. **Common changes:** - `"Required"` → `"Invalid input: expected <type>, received undefined"` - `"Expected number, received nan"` → `"Invalid input: expected number, received NaN"` - `"Invalid email"` → `"Invalid email address"` - `"Invalid uuid"` → `"Invalid UUID"` (capitalization) - `"Invalid ip"` → `"Invalid IPv4 address"` or `"Invalid IPv6 address"` (more specific) - Enum errors now use format: `'Invalid option: expected one of "option1"|"option2"'` **Example:** ```typescript // Before (Zod v3) { "code": "invalid_type", "message": "Required", "path": ["username"] } // After (Zod v4) { "code": "invalid_type", "message": "Invalid input: expected string, received undefined", "path": ["username"] } ``` ### Error Object Structure The `received` field may no longer be present in some error objects. If your code relies on this field, you should update it to handle cases where it's absent. ## Changes Required for Custom Zod Schemas If you're using Zod directly in your schemas (not through chanfana's parameter helpers), you'll need to update deprecated string format methods: ### String Format Methods (BREAKING) Zod v4 moved string format validations to top-level functions for better tree-shakeability: ```typescript // Before (Zod v3) import { z } from 'zod'; const schema = z.object({ email: z.string().email(), userId: z.string().uuid(), createdAt: z.string().datetime(), website: z.string().url(), birthDate: z.string().date(), // For date-only strings like "2024-01-20" }); // After (Zod v4) import { z } from 'zod'; const schema = z.object({ email: z.email(), // Top-level function userId: z.uuid(), // Top-level function createdAt: z.iso.datetime(), // Under z.iso namespace website: z.url(), // Top-level function birthDate: z.iso.date(), // Under z.iso namespace for YYYY-MM-DD strings }); ``` **Common replacements:** - `z.string().email()` → `z.email()` - `z.string().uuid()` → `z.uuid()` - `z.string().url()` → `z.url()` - `z.string().datetime()` → `z.iso.datetime()` - `z.string().date()` → `z.iso.date()` - `z.string().ip({ version: "v4" })` → `z.ipv4()` - `z.string().ip({ version: "v6" })` → `z.ipv6()` **Note:** Chanfana's parameter helpers (`Email()`, `Uuid()`, `DateTime()`, etc.) have been removed in v3. See [Parameter Helper Functions Removed](#parameter-helper-functions-removed-breaking) below for migration instructions. ### Native Enums (BREAKING) Zod v4 consolidated enum handling. If you're using `z.nativeEnum()`, switch to `z.enum()`: ```typescript // Before (Zod v3) enum Status { Active = 'active', Inactive = 'inactive', } const schema = z.object({ status: z.nativeEnum(Status), }); // After (Zod v4) const schema = z.object({ status: z.enum(['active', 'inactive']), // Use string array for enum values }); ``` ## Bug Fixes ### UpdateEndpoint and Optional Fields with Defaults **Fixed in this release:** Zod 4 changed how optional fields with `.default()` values are handled. Previously in Zod 3, defaults were only applied if a field was present but invalid. In Zod 4, defaults are **always applied** even when the field is absent from the input. This caused an issue where `UpdateEndpoint` would incorrectly reset optional fields to their default values during partial updates, even when those fields weren't included in the update request. **Example:** ```typescript const UserSchema = z.object({ id: z.number().int(), username: z.string(), email: z.email(), age: z.number().int().optional().default(18), }); // Database record: { id: 1, username: "john", age: 30 } // Update only username: PUT /users/1 { "username": "johndoe", "email": "john@example.com" } // ✅ Correctly keeps age as 30 (not reset to default 18) ``` **What we fixed:** - `UpdateEndpoint` now checks the raw request body to determine which fields were actually sent - Only fields present in the request are used to update the record - This preserves existing values for fields not included in partial updates **No action required** - This fix is automatic and restores the expected behavior for partial updates. ## New Features ### New `getUnvalidatedData()` Method A new method `getUnvalidatedData()` is now available on `OpenAPIRoute`. This returns the raw request data **before** Zod applies defaults or transformations. This is useful when you need to distinguish between: - A field that was explicitly sent with a value - A field that was absent from the request (but may have a Zod default) ```typescript import { OpenAPIRoute } from 'chanfana'; import { z } from 'zod'; class MyEndpoint extends OpenAPIRoute { schema = { request: { body: { content: { 'application/json': { schema: z.object({ name: z.string(), status: z.enum(['active', 'inactive']).optional().default('active'), }), }, }, }, }, }; async handle() { const validated = await this.getValidatedData(); // validated.body = { name: "test", status: "active" } (default applied) const raw = await this.getUnvalidatedData(); // raw.body = { name: "test" } (no status field) // Check if status was actually sent if ('status' in raw.body) { // User explicitly provided status } else { // Status is using default value } return { success: true }; } } ``` ## Parameter Helper Functions Removed (BREAKING) All parameter helper functions have been removed from Chanfana. You must now use native Zod schemas directly. **Removed functions:** - `Str()`, `Num()`, `Int()`, `Bool()` - `DateTime()`, `DateOnly()` - `Email()`, `Uuid()`, `Hostname()` - `Ipv4()`, `Ipv6()`, `Ip()` - `Regex()`, `Enumeration()` - `convertParams()` ### Migration Guide Replace the helper functions with their Zod equivalents: | Old Helper | New Zod Equivalent | |------------|-------------------| | `Str()` | `z.string()` | | `Num()` | `z.number()` | | `Int()` | `z.number().int()` | | `Bool()` | `z.boolean()` | | `DateTime()` | `z.iso.datetime()` | | `DateOnly()` | `z.iso.date()` | | `Email()` | `z.email()` | | `Uuid()` | `z.uuid()` | | `Ipv4()` | `z.ipv4()` | | `Ipv6()` | `z.ipv6()` | | `Ip()` | `z.union([z.ipv4(), z.ipv6()])` | | `Hostname()` | `z.string().regex(/^(([a-zA-Z0-9]\|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]\|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/)` | | `Regex({ pattern })` | `z.string().regex(pattern)` | | `Enumeration({ values })` | `z.enum([...])` | **Example migration:** ```typescript // Before import { Str, Int, Email, Enumeration } from 'chanfana'; const schema = z.object({ name: Str({ description: 'User name', example: 'John' }), age: Int({ description: 'User age', default: 18 }), email: Email(), status: Enumeration({ values: ['active', 'inactive'], default: 'active' }), }); // After import { z } from 'zod'; const schema = z.object({ name: z.string().describe('User name').openapi({ example: 'John' }), age: z.number().int().default(18).describe('User age'), email: z.email(), status: z.enum(['active', 'inactive']).default('active'), }); ``` For case-insensitive enumerations: ```typescript // Before Enumeration({ values: ['json', 'csv'], enumCaseSensitive: false }) // After z.preprocess((val) => String(val).toLowerCase(), z.enum(['json', 'csv'])) .openapi({ enum: ['json', 'csv'] }) ``` ## Removed Exports The following items have been removed from the public API: **Utility Functions** (relied on Zod's internal APIs): - `isAnyZodType()` - `isSpecificZodType()` **Type Aliases** (unnecessary abstraction over Zod v4 types): - `ZodEffects<T, Output, Input>` - Use `ZodPipe<T, any>` from Zod directly instead If you were using these, you should use Zod v4's public APIs instead: ```typescript // isAnyZodType replacement // Before import { isAnyZodType } from 'chanfana'; if (isAnyZodType(schema)) { ... } // After import { z } from 'zod'; if (schema instanceof z.ZodType) { ... } // ZodEffects replacement // Before import type { ZodEffects } from 'chanfana'; type MyParam = ZodEffects<SomeSchema, Output, Input>; // After import type { ZodPipe } from 'zod'; type MyParam = ZodPipe<SomeSchema, any>; ``` **Note:** `AnyZodObject` remains exported as it's a commonly used type in the public API. ## Migration Steps ### 1. Update Dependencies Update your `package.json`: ```json { "dependencies": { "chanfana": "^3.0.0", "zod": "^4.0.0" } } ``` Then run: ```bash npm install ``` ### 2. Update Deprecated Zod Methods (If Using Custom Schemas) If you're using Zod directly in your schemas, search for and replace deprecated string format methods: ```bash # Search for patterns that need updating grep -r "z\.string()\.(email\|uuid\|datetime\|url\|date)" . grep -r "z\.nativeEnum" . ``` Update according to the "Changes Required for Custom Zod Schemas" section above. ### 3. Update Error Message Handling (If Applicable) If your code depends on specific error message formats (e.g., for testing or client-side validation display), update those expectations to match the new Zod v4 formats shown above. ### 4. Test Your Application Run your test suite to catch any issues: ```bash npm test ``` Pay special attention to: - Validation error handling tests - API response format tests - Error message assertions ## Benefits of Zod v4 After migrating, you'll benefit from: - **Better Tree-Shakeability:** Smaller bundle sizes thanks to improved code splitting - **Improved Error Messages:** More descriptive and consistent validation errors - **Better Performance:** Optimized validation logic - **Enhanced Type Safety:** Improved TypeScript inference ## Hono Base Path Changes (v3.1) Chanfana v3.1 introduces improved handling of Hono's `basePath()` method. These changes affect how you configure base paths for Hono applications. ### Auto-detection of Hono's `basePath()` Chanfana now automatically detects when a Hono instance was created with `basePath()`. You no longer need to pass the `base` option separately: ```typescript // Before: Had to pass base to both Hono and chanfana const app = new Hono().basePath('/api'); const router = fromHono(app, { base: '/api' }); // ❌ Now throws an error // After: Just use basePath() — chanfana detects it automatically const app = new Hono().basePath('/api'); const router = fromHono(app); // ✅ Base path "/api" auto-detected ``` ### `base` option now applies `basePath()` for Hono When using the `base` option with Hono (without a pre-existing `basePath()`), Chanfana now calls Hono's `basePath()` internally. This means routes actually match at the prefixed path, not just in the OpenAPI schema: ```typescript const router = fromHono(new Hono(), { base: '/api' }); router.get('/users', UserEndpoint); // Matches at /api/users ``` ### Combining `basePath()` and `base` throws an error Using both Hono's `basePath()` and chanfana's `base` option now throws a descriptive error to prevent double-prefixing: ```typescript // This throws an error with migration guidance: fromHono(new Hono().basePath('/api'), { base: '/v1' }); ``` ### Base path format validation The `base` option is now validated: - Must start with `/` (e.g., `/api` not `api`) - Must not end with `/` (e.g., `/api` not `/api/`) ### Migration steps 1. If you use `new Hono().basePath('/api')` with `fromHono(app, { base: '/api' })`, remove the `base` option from `fromHono()`. 2. If you use `fromHono(app, { base: '/api' })` without `basePath()`, no changes needed — this now also configures Hono's route matching. 3. Ensure your `base` values start with `/` and don't end with `/`. ## Hono Error Handling Changes (v3.1) Chanfana v3.1 changes how errors are handled when using the Hono adapter. ### Errors now flow through Hono's `onError` Previously, chanfana caught all errors (validation errors, `ApiException` subclasses) internally and returned formatted JSON responses directly. Hono's `app.onError` handler never saw these errors. Now, chanfana converts these errors into Hono `HTTPException` instances and re-throws them, so they flow through `app.onError`. The `HTTPException` wraps chanfana's standard JSON error response, accessible via `err.getResponse()`. **If you don't have an `onError` handler:** No action needed. Hono's default handler calls `HTTPException.getResponse()`, which returns the same formatted response as before. **If you have an `onError` handler:** You will now receive chanfana errors (validation failures, `NotFoundException`, etc.) as `HTTPException` instances. Update your handler to check for `HTTPException`: ```typescript import { HTTPException } from 'hono/http-exception'; app.onError((err, c) => { if (err instanceof HTTPException) { // Chanfana error -- return the pre-formatted response return err.getResponse(); } // Handle other errors return c.json({ error: 'Internal Server Error' }, 500); }); ``` ### `handleValidationError()` removed The `handleValidationError()` method has been removed from `OpenAPIRoute`. If you were overriding this method to customize validation error formatting, use Hono's `app.onError` handler instead to customize error responses centrally. **No changes to itty-router behavior.** ## Migrating to Chanfana 3.1 ### `contentJson()` Requires Zod Schemas (BREAKING) `contentJson()` no longer accepts plain objects or JavaScript constructors. It now requires a Zod schema: ```typescript // Before (Chanfana v2/v3.0) contentJson({ success: Boolean, result: { id: String } }) // After (Chanfana v3.1) import { z } from 'zod'; import { contentJson } from 'chanfana'; contentJson(z.object({ success: z.boolean(), result: z.object({ id: z.string() }) })) ``` ### `raiseUnknownParameters` Now Enforced The `raiseUnknownParameters` router option was previously accepted but not enforced. It is now fully functional. If you had this option set to `true`, unknown query/path/header parameters will now cause 400 validation errors. ```typescript // If you see unexpected 400 errors after upgrading, check your router options: const router = fromHono(app, { raiseUnknownParameters: false, // Set to false to allow unknown parameters }); ``` ### D1 Endpoint Security Improvements Several D1 endpoint behaviors have changed for security: - **Delete and Update operations** now only apply primary key filters to WHERE clauses. If you relied on filtering by non-primary-key fields, that will no longer work. - **Database error messages** are no longer exposed in responses. Use the `constraintsMessages` property to map constraint violations to user-friendly errors. - **`per_page` is now capped** at 100 by default (configurable via `maxPerPage` class property). ### New Exception Types Chanfana 3.1 adds a full set of HTTP exception classes. Consider replacing generic `ApiException` usage with specific types: | Exception | Status | Use Case | |-----------|--------|----------| | `UnauthorizedException` | 401 | Authentication failures | | `ForbiddenException` | 403 | Authorization failures | | `ConflictException` | 409 | Duplicate resources | | `TooManyRequestsException` | 429 | Rate limiting (sets `Retry-After` header) | | `ServiceUnavailableException` | 503 | Maintenance mode (sets `Retry-After` header) | See [Error Handling](./error-handling.md) for the full list. ## Need Help? If you encounter issues during migration: 1. Check the [Troubleshooting FAQ](/troubleshooting-and-faq) 2. Review the [Zod v4 changelog](https://zod.dev/v4/changelog) 3. Open an issue on [GitHub](https://github.com/cloudflare/chanfana/issues)