diff --git a/openapi/frameworks/zod.mdx b/openapi/frameworks/zod.mdx index 87daaa10..e9ef65e2 100644 --- a/openapi/frameworks/zod.mdx +++ b/openapi/frameworks/zod.mdx @@ -9,141 +9,15 @@ import { Callout } from "@/mdx/components"; Zod is a powerful and flexible schema validation library for TypeScript, which many developers use to define their TypeScript data parsing schemas. -This tutorial demonstrates how to use another TypeScript library, the `zod-openapi` npm package, to convert Zod schemas into a complete OpenAPI document, and then how to use Speakeasy to generate a production-ready SDK from that document. +This tutorial demonstrates how to use another TypeScript library, the zod-openapi NPM package, to convert Zod schemas into a complete OpenAPI document, and then how to use Speakeasy to generate a production-ready SDK from that document. ## Why use Zod with OpenAPI? -Combining Zod with OpenAPI generation offers the best of both worlds: runtime validation and automatic API documentation. Instead of writing schemas twice – once for runtime validation and again for your OpenAPI document – you define your data models once in Zod and generate both TypeScript types and OpenAPI documentation from the same source. +Combining Zod with OpenAPI generation offers the best of both worlds: runtime validation and automatic API documentation. Instead of writing schemas twice - once for runtime validation and again for your OpenAPI document - you define your data models once in Zod and generate both TypeScript types and OpenAPI documentation from the same source. This eliminates the task of keeping hand-written OpenAPI documents in sync with your actual API implementation. When paired with Speakeasy's SDK generation, you get type-safe client libraries that automatically stay up to date with your API changes. - -This guide uses `zod-openapi`, which is actively maintained and offers better TypeScript integration than the older `zod-to-openapi` library. - -We show how to use a dual import strategy, which means we use both Zod v3 and v4 in the same project. This approach is necessary because `zod-openapi` currently requires Zod v3 compatibility, but you may want to use Zod v4 features elsewhere in your application. - -While Zod v4 introduces new features like `z.strictObject()` and `z.email()`, you'll need to use the standard Zod import for OpenAPI schemas and the `/v4` subpath for new features until `zod-openapi` adds full v4 support. Check the [zod-openapi releases](https://github.com/samchungy/zod-openapi/releases) for the latest compatibility updates. - -Unlike most libraries, Zod is directly embedded in hundreds of other libraries' public APIs. A normal `zod@4.0.0` release would force every one of those libraries to publish breaking changes simultaneously - a massive "version avalanche". - -The subpath approach (inspired by Go modules) lets libraries support both versions with one dependency, providing a gradual migration path for the entire ecosystem. - -See [Colin's detailed explanation](https://github.com/colinhacks/zod/issues/4371) for the full technical reasoning. - - - -## zod-openapi overview - -The [`zod-openapi`](https://github.com/samchungy/zod-openapi) package is a TypeScript library that helps developers define OpenAPI schemas as Zod schemas. The stated goal of the project is to cut down on code duplication, and it does a wonderful job of this. - -Zod schemas map well to OpenAPI schemas, and the changes required to extract OpenAPI documents from a schema defined in Zod are often small. - - - If you're currently using the older `zod-to-openapi` library, the syntax will - be familiar, and you can use either library. For migration guidance, see the - [zod-openapi migration - documentation](https://github.com/samchungy/zod-openapi#migration). - - -### Key concepts - -Here's an overview of some key concepts in Zod and how they relate to Zod versioning. - -#### Schemas and z.strictObject - -Zod v4 introduces top-level helpers like `z.strictObject()` for objects that reject unknown keys and `z.email()` for email validation. - -```typescript filename="concept.ts" -import { z as z3 } from "zod"; -import { z as z4 } from "zod/v4"; - -// Use z4 for new Zod v4 features -const user = z4.strictObject({ - email: z4.email(), - age: z4.number().int().min(18), -}); -``` - -#### Field metadata - -The `.openapi()` method adds OpenAPI-specific metadata like descriptions and examples to any Zod schema. Use `z3` for OpenAPI schemas. - -```typescript filename="concept.ts" -import { z as z3 } from "zod"; -import { extendZodWithOpenApi } from "zod-openapi"; - -extendZodWithOpenApi(z3); - -const name = z3.string().min(1).openapi({ - description: "The user's full name", - example: "Alice Johnson", -}); -``` - -#### Operation objects - -Use `ZodOpenApiOperationObject` to define API endpoints with request and response schemas. - -```typescript filename="concept.ts" -import { z as z3 } from "zod"; -import { ZodOpenApiOperationObject } from "zod-openapi"; - -const getUser: ZodOpenApiOperationObject = { - operationId: "getUser", - summary: "Get user by ID", - requestParams: { path: z3.object({ id: z3.string() }) }, - responses: { - "200": { - description: "User found", - content: { "application/json": { schema: userSchema } }, - }, - }, -}; -``` - -#### Adding tags to operations - -Tags help organize your API operations in the generated documentation and SDKs. You can add a `tags` array to each operation object: - -```typescript filename="concept.ts" -const getBurger: ZodOpenApiOperationObject = { - operationId: "getBurger", - summary: "Get a burger by ID", - tags: ["burgers"], // <--- Add tags here - // ...rest of the operation -}; -``` - -#### Using Zod v4 features alongside OpenAPI documents - -While your OpenAPI documents must use the `z3` instance for compatibility, you can use `z4` features for internal validation, type checking, or other parts of your application: - -```typescript filename="concept.ts" -// OpenAPI-compatible schemas (use z3) -const apiUserSchema = z3 - .object({ - id: z3.string(), - name: z3.string(), - email: z3.string(), - }) - .openapi("User"); - -// Internal schemas can use Zod v4 features (use z4) -const internalUserSchema = z4.strictObject({ - // v4 feature - id: z4.string().uuid(), - name: z4.string().min(1), - email: z4.string().email(), // v4 feature - preferences: z4 - .object({ - darkMode: z4.boolean(), - notifications: z4.enum(["all", "mentions", "none"]), - }) - .optional(), -}); -``` - -## Step-by-step tutorial: From Zod to OpenAPI to an SDK +## Step-by-step tutorial: From Zod to OpenAPI to SDK Now let's walk through the process of generating an OpenAPI document and SDK for our Burgers and Orders API. @@ -153,39 +27,35 @@ This tutorial assumes basic familiarity with TypeScript and Node.js development. The following should be installed on your machine: -- [Node.js version 18 or above](https://nodejs.org/en/download) -- The [Speakeasy CLI](/docs/introduction#install-the-speakeasy-cli), which we'll use to generate an SDK from the OpenAPI document +- [Node.js version 20 or above](https://nodejs.org/en/download). +- The [Speakeasy CLI](/docs/introduction#install-the-speakeasy-cli), which we'll use to generate an SDK from the OpenAPI document. -### Creating your Zod project +### Create a Zod project - - The source code for our complete example is available in the - [`speakeasy-api/examples`](https://github.com/speakeasy-api/examples.git) - repository in the `zod-openapi` directory. The project contains a - pre-generated Python SDK with instructions on how to generate more SDKs. You - can clone this repository to test how changes to the Zod schema definition - result in changes to the generated SDK. - -Alternatively, you can initialize a new npm project and install the required dependencies if you're not using our burgers example. +The source code for our complete example is available in the +[`speakeasy-api/examples`](https://github.com/speakeasy-api/examples.git) +repository in the `zod-openapi` directory. The project contains a +pre-generated Python SDK with instructions on how to generate more SDKs. You +can clone this repository to test how changes to the Zod schema definition +result in changes to the generated SDK. -``` -npm init -y -npm install zod@^3.25 zod-openapi yaml -``` - -If you're following along, start by cloning the `speakeasy-api/examples` repository. +Start by cloning the `speakeasy-api/examples` repository. ```bash filename="Terminal" git clone https://github.com/speakeasy-api/examples.git cd zod-openapi +npm install ``` -Next, install the dependencies: + +Alternatively, initialize a new NPM project and install the required dependencies, and try to implement the suggested steps in this tutorial: -```bash filename="Terminal" -npm install ``` +npm init -y +npm install zod@^4.0.0 yaml zod-openapi +``` + ### Installing TypeScript development tools @@ -195,171 +65,85 @@ For this tutorial, we'll use `tsx` for running TypeScript directly: npm install -D tsx ``` -### Creating your app's first Zod schema +### Create the first Zod schema Save this TypeScript code in a new file called `index.ts`. Note the dual import strategy: ```typescript filename="index.ts" -// Import Zod v3 compatible instance for zod-openapi -import { z as z3 } from "zod"; -// Import Zod v4 for new features and future migration -import { z as z4 } from "zod/v4"; - -// For now, we'll use z3 for OpenAPI schemas since zod-openapi requires it -const burgerSchema = z3.object({ - id: z3.number().min(1), - name: z3.string().min(1).max(50), - description: z3.string().max(255).optional(), -}); -``` - -### Extending Zod with OpenAPI - -We'll add the `openapi` method to Zod by calling `extendZodWithOpenApi` once. Update `index.ts` to import `extendZodWithOpenApi` from `zod-openapi`, then call `extendZodWithOpenApi` on the `z3` instance. +import zod from "zod"; -```typescript filename="index.ts" -import { z as z3 } from "zod"; -import { extendZodWithOpenApi } from "zod-openapi"; -import { z as z4 } from "zod/v4"; - -// Extend the Zod v3 compatible instance for zod-openapi -extendZodWithOpenApi(z3); - -// Schemas defined with z3 for current zod-openapi compatibility -const burgerSchema = z3.object({ - id: z3.number().min(1), - name: z3.string().min(1).max(50), - description: z3.string().max(255).optional(), +const burgerSchema = zod.object({ + id: zod.number().min(1), + name: zod.string().min(1).max(50), + description: zod.string().max(255).optional(), }); ``` -### Registering and generating a component schema - -Next, we'll use the new `openapi` method provided by `extendZodWithOpenApi` to register an OpenAPI schema for the `burgerSchema`. Edit `index.ts` and add `.openapi({ref: "Burger"}` to the `burgerSchema` schema object. - -We'll also add an OpenAPI generator, `OpenApiGeneratorV31`, and log the generated component to the console as YAML. +### Extending Zod with OpenAPI ```typescript filename="index.ts" -import { z as z3 } from "zod"; -import { extendZodWithOpenApi } from "zod-openapi"; -import { z as z4 } from "zod/v4"; - -extendZodWithOpenApi(z3); - -const burgerSchema = z3.object({ - id: z3.number().min(1), - name: z3.string().min(1).max(50), - description: z3.string().max(255).optional(), -}); - -burgerSchema.openapi({ ref: "Burger" }); +const burgerSchema = zod + .object({ + id: zod.number().min(1).meta({ + description: "The unique identifier of the burger.", + example: 1, + }), + name: zod.string().min(1).max(50).meta({ + description: "The name of the burger.", + example: "Veggie Burger", + }), + description: zod.string().max(255).optional().meta({ + description: "The description of the burger.", + example: "A delicious bean burger with avocado.", + }), + }) + .meta({ + description: "A burger served at the restaurant.", + }); ``` -### Adding metadata to components - -To generate an SDK that offers a great developer experience, we recommend adding descriptions and examples to all fields in OpenAPI components. - -With `zod-openapi`, we'll call the `.openapi` method on each field, and add an example and description to each field. - -We'll also add a description to the `Burger` component itself. +### Reusing schemas with references -Edit `index.ts` and edit `burgerSchema` to add OpenAPI metadata. +To avoid duplication and promote reuse, we can define reusable schemas for common fields. For example, we can define a `BurgerIdSchema` for the burger ID field and use it in the `burgerSchema`. ```typescript filename="index.ts" -import { z as z3 } from "zod"; -import { extendZodWithOpenApi } from "zod-openapi"; -import { z as z4 } from "zod/v4"; - -extendZodWithOpenApi(z3); - -const burgerSchema = z3.object({ - id: z3.number().min(1).openapi({ - description: "The unique identifier of the burger.", - example: 1, - }), - name: z3.string().min(1).max(50).openapi({ - description: "The name of the burger.", - example: "Veggie Burger", - }), - description: z3.string().max(255).optional().openapi({ - description: "The description of the burger.", - example: "A delicious bean burger with avocado.", - }), +// Define a reusable BurgerId schema +const BurgerIdSchema = zod.number().min(1).meta({ + description: "The unique identifier of the burger.", + example: 1, + readOnly: true, }); -burgerSchema.openapi({ - ref: "Burger", - description: "A burger served at the restaurant.", -}); +const burgerSchema = zod + .object({ + id: BurgerIdSchema, // Use the BurgerIdSchema + name: zod.string().min(1).max(50).meta({ + description: "The name of the burger.", + example: "Veggie Burger", + }), + description: zod.string().max(255).optional().meta({ + description: "The description of the burger.", + example: "A delicious bean burger with avocado.", + }), + }) + .meta({ + description: "A burger served at the restaurant.", + }); ``` -### Preparing to generate an OpenAPI document +### Generating an OpenAPI document -Now that we know how to register components with metadata for our OpenAPI schema, let's generate a complete schema document. +Now that the Zod schemas are defined with OpenAPI metadata, it's time to generate an OpenAPI document. -Import `yaml` and `createDocument`. +For this two imports are needed from the `zod-openapi` package: `ZodOpenApiOperationObject` and `createDocument`. ```typescript filename="index.ts" -import * as yaml from "yaml"; -import { z as z3 } from "zod"; -import { createDocument, extendZodWithOpenApi } from "zod-openapi"; -import { z as z4 } from "zod/v4"; - -extendZodWithOpenApi(z3); - -const burgerSchema = z3.object({ - id: z3.number().min(1).openapi({ - description: "The unique identifier of the burger.", - example: 1, - }), - name: z3.string().min(1).max(50).openapi({ - description: "The name of the burger.", - example: "Veggie Burger", - }), - description: z3.string().max(255).optional().openapi({ - description: "The description of the burger.", - example: "A delicious bean burger with avocado.", - }), -}); - -burgerSchema.openapi({ - ref: "Burger", - description: "A burger served at the restaurant.", -}); +import { ZodOpenApiOperationObject, createDocument } from "zod-openapi"; ``` -### Generating an OpenAPI document - -We'll use the `createDocument` method to generate an OpenAPI document. We'll pass in the `burgerSchema` and a title for the document. +The `createDocument` method will help generate an OpenAPI document. Pass in the `burgerSchema` and a title for the document. ```typescript filename="index.ts" -import * as yaml from "yaml"; -import { z as z3 } from "zod"; -import { createDocument, extendZodWithOpenApi } from "zod-openapi"; -import { z as z4 } from "zod/v4"; - -extendZodWithOpenApi(z3); - -const burgerSchema = z3.object({ - id: z3.number().min(1).openapi({ - description: "The unique identifier of the burger.", - example: 1, - }), - name: z3.string().min(1).max(50).openapi({ - description: "The name of the burger.", - example: "Veggie Burger", - }), - description: z3.string().max(255).optional().openapi({ - description: "The description of the burger.", - example: "A delicious bean burger with avocado.", - }), -}); - -burgerSchema.openapi({ - ref: "Burger", - description: "A burger served at the restaurant.", -}); - const document = createDocument({ openapi: "3.1.0", info: { @@ -383,112 +167,82 @@ const document = createDocument({ console.log(yaml.stringify(document)); ``` -### Adding a burger ID schema - -To make the burger ID available to other schemas, we'll define a burger ID schema. We'll also use this schema to define a path parameter for the burger ID later on. -```typescript filename="index.ts" -const BurgerIdSchema = z3 - .number() - .min(1) - .openapi({ - ref: "BurgerId", - description: "The unique identifier of the burger.", - example: 1, - param: { - in: "path", - name: "id", - }, - }); -``` +### Varying read/write schemas -Update the `burgerSchema` to use the `BurgerIdSchema`. +One common pattern in OpenAPI documents is to have separate schemas for creating and updating resources. This allows you to define different validation rules for these operations. That would look sometime like this: ```typescript filename="index.ts" -const burgerSchema = z3.object({ - id: BurgerIdSchema, - name: z3.string().min(1).max(50).openapi({ - description: "The name of the burger.", - example: "Veggie Burger", - }), - description: z3.string().max(255).optional().openapi({ - description: "The description of the burger.", - example: "A delicious bean burger with avocado.", - }), +const burgerCreateSchema = burgerSchema.omit({ id: true }).meta({ + description: "A burger to create.", }); ``` -### Adding a schema for creating burgers - -We'll add a schema for creating burgers that doesn't include an ID. We'll use this schema to define the request body for the create burger path. +An easier approach is to utilize `readOnly` and `writeOnly` properties in OpenAPI. Marking the `id` field as `readOnly` indicates that it is only returned in responses and not expected in requests. ```typescript filename="index.ts" -const burgerCreateSchema = burgerSchema.omit({ id: true }).openapi({ - ref: "BurgerCreate", - description: "A burger to create.", +const BurgerIdSchema = zod.number().min(1).meta({ + description: "The unique identifier of the burger.", + example: 1, + readOnly: true, }); ``` -### Adding order schemas +This way, we can use the same schema for both creating and retrieving burgers. + +### More advanced schemas -To match the final OpenAPI output, let's add schemas and endpoints for orders. +Let's define a more complex schema for orders, which includes an array of burger IDs, timestamps, and status fields. ```typescript filename="index.ts" -const OrderIdSchema = z3 - .number() - .min(1) - .openapi({ - ref: "OrderId", - description: "The unique identifier of the order.", - example: 1, - param: { - in: "path", - name: "id", - }, - }); +const OrderIdSchema = zod.number().min(1).meta({ + description: "The unique identifier of the order.", + example: 1, + readOnly: true, +}); + +const orderStatusEnum = zod.enum([ + "pending", + "in_progress", + "ready", + "delivered", +]); -const orderSchema = z3 +const orderSchema = zod .object({ id: OrderIdSchema, - burger_ids: z3 + burger_ids: zod .array(BurgerIdSchema) - .min(1) - .openapi({ + .nonempty() + .meta({ description: "The burgers in the order.", example: [1, 2], }), - time: z3.string().openapi({ + time: zod.iso.datetime().meta({ description: "The time the order was placed.", example: "2021-01-01T00:00:00.000Z", - format: "date-time", }), - table: z3.number().min(1).openapi({ + table: zod.number().min(1).meta({ description: "The table the order is for.", example: 1, }), - status: z3.enum(["pending", "in_progress", "ready", "delivered"]).openapi({ + status: orderStatusEnum.meta({ description: "The status of the order.", example: "pending", }), - note: z3.string().optional().openapi({ + note: zod.string().optional().meta({ description: "A note for the order.", example: "No onions.", }), }) - .openapi({ - ref: "Order", + .meta({ description: "An order placed at the restaurant.", }); - -const orderCreateSchema = orderSchema.omit({ id: true }).openapi({ - ref: "OrderCreate", - description: "An order to create.", -}); ``` -### Defining burger and order operations +### Defining operations -Now, define the operations for creating and getting burgers and orders, and listing burgers: +Operations need to be defined before they can be registered in the OpenAPI document. Define an operation for creating and getting burgers and orders, and listing burgers: ```typescript filename="index.ts" import { ZodOpenApiOperationObject } from "zod-openapi"; @@ -502,7 +256,7 @@ const createBurger: ZodOpenApiOperationObject = { description: "The burger to create.", content: { "application/json": { - schema: burgerCreateSchema, + schema: burgerSchema, }, }, }, @@ -524,7 +278,7 @@ const getBurger: ZodOpenApiOperationObject = { description: "Gets a burger from the database.", tags: ["burgers"], requestParams: { - path: z3.object({ id: BurgerIdSchema }), + path: zod.object({ id: BurgerIdSchema }), }, responses: { "200": { @@ -548,13 +302,14 @@ const listBurgers: ZodOpenApiOperationObject = { description: "The burgers were retrieved successfully.", content: { "application/json": { - schema: z3.array(burgerSchema), + schema: zod.array(burgerSchema), }, }, }, }, }; +// Order operations const createOrder: ZodOpenApiOperationObject = { operationId: "createOrder", summary: "Create a new order", @@ -564,7 +319,7 @@ const createOrder: ZodOpenApiOperationObject = { description: "The order to create.", content: { "application/json": { - schema: orderCreateSchema, + schema: orderSchema, }, }, }, @@ -586,7 +341,7 @@ const getOrder: ZodOpenApiOperationObject = { description: "Gets an order from the database.", tags: ["orders"], requestParams: { - path: z3.object({ id: OrderIdSchema }), + path: zod.object({ id: OrderIdSchema }), }, responses: { "200": { @@ -603,7 +358,7 @@ const getOrder: ZodOpenApiOperationObject = { ### Adding a webhook that runs when a burger is created -We'll add a webhook that runs when a burger is created. We'll use the `ZodOpenApiOperationObject` type to define the webhook. +Webhooks are like operations that runs when a server-side action is triggered, e.g. when a burger has been created. They're similar enough that zod-openapi uses the same `ZodOpenApiOperationObject` type to define the webhook. ```typescript filename="index.ts" const createBurgerWebhook: ZodOpenApiOperationObject = { @@ -645,17 +400,6 @@ const document = createDocument({ description: "The production server.", }, ], - "x-speakeasy-retries": { - strategy: "backoff", - backoff: { - initialInterval: 500, - maxInterval: 60000, - maxElapsedTime: 3600000, - exponent: 1.5, - }, - statusCodes: ["5XX"], - retryConnectionErrors: true, - }, paths: { "/burgers": { post: createBurger, @@ -679,13 +423,23 @@ const document = createDocument({ components: { schemas: { burgerSchema, - burgerCreateSchema, BurgerIdSchema, orderSchema, - orderCreateSchema, OrderIdSchema, }, }, + // Adding Speakeasy extensions for better SDK generation + "x-speakeasy-retries": { + strategy: "backoff", + backoff: { + initialInterval: 500, + maxInterval: 60000, + maxElapsedTime: 3600000, + exponent: 1.5, + }, + statusCodes: ["5XX"], + retryConnectionErrors: true, + }, }); console.log(yaml.stringify(document)); @@ -735,14 +489,14 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/BurgerCreate" + $ref: "#/components/schemas/burgerSchema" responses: "201": description: The burger was created successfully. content: application/json: schema: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" get: operationId: listBurgers summary: List burgers @@ -757,7 +511,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" /burgers/{id}: get: operationId: getBurger @@ -768,17 +522,17 @@ paths: parameters: - in: path name: id - description: The unique identifier of the burger. schema: - $ref: "#/components/schemas/BurgerId" + $ref: "#/components/schemas/BurgerIdSchema" required: true + description: The unique identifier of the burger. responses: "200": description: The burger was retrieved successfully. content: application/json: schema: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" /orders: post: operationId: createOrder @@ -791,14 +545,14 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/OrderCreate" + $ref: "#/components/schemas/orderSchema" responses: "201": description: The order was created successfully. content: application/json: schema: - $ref: "#/components/schemas/Order" + $ref: "#/components/schemas/orderSchemaOutput" /orders/{id}: get: operationId: getOrder @@ -809,17 +563,17 @@ paths: parameters: - in: path name: id - description: The unique identifier of the order. schema: - $ref: "#/components/schemas/OrderId" + $ref: "#/components/schemas/OrderIdSchema" required: true + description: The unique identifier of the order. responses: "200": description: The order was retrieved successfully. content: application/json: schema: - $ref: "#/components/schemas/Order" + $ref: "#/components/schemas/orderSchemaOutput" webhooks: /burgers: post: @@ -840,130 +594,149 @@ webhooks: components: schemas: burgerSchema: + description: A burger served at the restaurant. type: object properties: id: - $ref: "#/components/schemas/BurgerId" + $ref: "#/components/schemas/BurgerIdSchema" name: - type: string - minLength: 1 - maxLength: 50 description: The name of the burger. example: Veggie Burger - description: - type: string - maxLength: 255 - description: The description of the burger. - example: A delicious bean burger with avocado. - required: - - id - - name - BurgerCreate: - type: object - properties: - name: type: string minLength: 1 maxLength: 50 - description: The name of the burger. - example: Veggie Burger description: - type: string - maxLength: 255 description: The description of the burger. example: A delicious bean burger with avocado. + type: string + maxLength: 255 required: + - id - name - description: A burger to create. - BurgerId: - type: number - minimum: 1 + BurgerIdSchema: description: The unique identifier of the burger. example: 1 - Order: + readOnly: true + type: number + minimum: 1 + orderSchema: + description: An order placed at the restaurant. type: object properties: id: - $ref: "#/components/schemas/OrderId" + $ref: "#/components/schemas/OrderIdSchema" burger_ids: - type: array - items: - $ref: "#/components/schemas/BurgerId" - minItems: 1 description: The burgers in the order. - example: &a1 + example: - 1 - 2 + minItems: 1 + type: array + items: + $ref: "#/components/schemas/BurgerIdSchema" time: - type: string - format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: - type: number - minimum: 1 description: The table the order is for. example: 1 + type: number + minimum: 1 status: + description: The status of the order. + example: pending type: string - enum: &a2 + enum: - pending - in_progress - ready - delivered - description: The status of the order. - example: pending note: - type: string description: A note for the order. example: No onions. + type: string required: - id - burger_ids - time - table - status + OrderIdSchema: + description: The unique identifier of the order. + example: 1 + readOnly: true + type: number + minimum: 1 + burgerSchemaOutput: + description: A burger served at the restaurant. + type: object + properties: + id: + $ref: "#/components/schemas/BurgerIdSchema" + name: + description: The name of the burger. + example: Veggie Burger + type: string + minLength: 1 + maxLength: 50 + description: + description: The description of the burger. + example: A delicious bean burger with avocado. + type: string + maxLength: 255 + required: + - id + - name + additionalProperties: false + orderSchemaOutput: description: An order placed at the restaurant. - OrderCreate: type: object properties: + id: + $ref: "#/components/schemas/OrderIdSchema" burger_ids: + description: The burgers in the order. + example: + - 1 + - 2 + minItems: 1 type: array items: - $ref: "#/components/schemas/BurgerId" - minItems: 1 - description: The burgers in the order. - example: *a1 + $ref: "#/components/schemas/BurgerIdSchema" time: - type: string - format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: - type: number - minimum: 1 description: The table the order is for. example: 1 + type: number + minimum: 1 status: - type: string - enum: *a2 description: The status of the order. example: pending - note: type: string + enum: + - pending + - in_progress + - ready + - delivered + note: description: A note for the order. example: No onions. + type: string required: + - id - burger_ids - time - table - status - description: An order to create. - OrderId: - type: number - minimum: 1 - description: The unique identifier of the order. - example: 1 + additionalProperties: false ``` ### Generating an SDK @@ -975,52 +748,40 @@ With our OpenAPI document complete, we can now generate an SDK using the Speakea First, install the Speakeasy CLI: ```bash filename="Terminal" -# Using Homebrew (recommended) +# Option 1: Using Homebrew (recommended) brew install speakeasy-api/tap/speakeasy -# Using curl +# Option 2: Using curl curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` -#### Linting your OpenAPI document +#### Linting OpenAPI documents -Before generating SDKs, lint your OpenAPI document to catch common issues: +Before generating SDKs, lint the OpenAPI document to catch common issues: ```bash filename="Terminal" speakeasy lint openapi --schema openapi.yaml ``` -#### Using AI to improve your OpenAPI document - -The Speakeasy CLI now includes AI-powered suggestions to automatically improve your OpenAPI documents: - -```bash filename="Terminal" -speakeasy suggest openapi.yaml -``` - -Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the schema and output path. - -Read the [Speakeasy Suggest](/docs/prep-openapi/maintenance) documentation for more information on how to use Speakeasy Suggest. - #### Generating your SDK -Now you can generate your SDK using the quickstart command: +Now generate your SDK using the quickstart command: ```bash filename="Terminal" speakeasy quickstart ``` -Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` (or your improved OpenAPI document if you used suggestions) when prompted for the OpenAPI document location, and select your preferred language when prompted. +Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` when prompted for the OpenAPI document location, and select preferred language when prompted. ## Using your generated SDK -Once you've generated your SDK, you can [publish](/docs/publish-sdk) it for use. For TypeScript, you can publish it as an npm package. +Once the SDK is generated, [publish](/docs/publish-sdk) it for use. For TypeScript, it can be published as an NPM package. -TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. +TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. The SDK documentation includes instructions for installing the MCP server. Note that the SDK is not ready for production use immediately after - generation. To get it production-ready, follow the steps outlined in your + generation. To get it production-ready, follow the steps outlined in the Speakeasy workspace. @@ -1028,19 +789,17 @@ TypeScript SDKs generated with Speakeasy include an installable [Model Context P The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into CI/CD pipelines to automatically regenerate SDKs when your Zod schemas change. -You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. +Speakeasy can be set up to automatically push a new branch to SDK repositories so that teammates can review and merge the SDK changes. -For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK workflow syntax reference](/docs/speakeasy-reference/workflow-file). +For an overview of how to set up SDK automation, see the Speakeasy [SDK workflow syntax reference](/docs/speakeasy-reference/workflow-file). ## Summary In this tutorial, we learned how to generate OpenAPI schemas from Zod and create client SDKs with Speakeasy. -By following these steps, you can ensure that your API is well-documented, easy to use, and offers a great developer experience. +By following these steps, it's possible to ensure an API is well-documented, easy to use, and offers a great developer experience. ### Further reading -This guide covered the basics of generating an OpenAPI document using `zod-openapi`. Here are some resources to help you learn more about Zod, OpenAPI, and Speakeasy: - - [The `zod-openapi` documentation](https://github.com/samchungy/zod-openapi): Learn more about the `zod-openapi` library, including advanced features like custom serializers and middleware integration. - [The Zod documentation](https://zod.dev/): Comprehensive guide to Zod schema validation, including the latest v4 features.