Skip to main content

openapi-to-zod-playwright

@spartaficial/openapi-to-zod-playwright generates a Playwright API client from an OpenAPI 3.x specification in two layers: a thin client that sends whatever you give it, and a service that asserts the status code and validates every request and response against Zod schemas generated from the same specification. This page is for developers writing API tests with @playwright/test, and it covers the generated files, every option, and the patterns the two layers are designed for.

What it generates​

Take a small API, simple-api.yaml:

openapi: 3.0.0
info:
title: Simple API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
summary: Get all users
responses:
"200":
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
post:
operationId: createUser
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Bad request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/users/{userId}:
get:
operationId: getUserById
parameters:
- name: userId
in: path
required: true
schema: { type: string }
responses:
"200":
description: User found
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
operationId: deleteUser
parameters:
- name: userId
in: path
required: true
schema: { type: string }
responses:
"204":
description: User deleted
components:
schemas:
User:
type: object
required: [id, email, name]
properties:
id: { type: string, format: uuid }
email: { type: string, format: email }
name: { type: string }
age: { type: integer, minimum: 0 }
CreateUserRequest:
type: object
required: [email, name]
properties:
email: { type: string, format: email }
name: { type: string }
age: { type: integer, minimum: 0 }
Error:
type: object
required: [message]
properties:
message: { type: string }
code: { type: string }

and a configuration that asks for all three files:

// openapi-to-zod-playwright.config.ts
import { defineConfig } from "@spartaficial/openapi-to-zod-playwright";

export default defineConfig({
specs: [
{
input: "simple-api.yaml",
outputTypes: "tests/api/schemas.ts",
outputClient: "tests/api/client.ts",
outputService: "tests/api/service.ts",
},
],
});

npx openapi-to-zod-playwright writes three files. The schemas file is what openapi-to-zod would generate, plus a schema for every response body that is declared inline rather than as a $ref:

// tests/api/schemas.ts
import { z } from "zod";

export const userSchema = z.strictObject({
id: z.uuid(),
email: z.email(),
name: z.string(),
age: z.number().int().gte(0).optional(),
});
export type User = z.infer<typeof userSchema>;

export const createUserRequestSchema = z.strictObject({
email: z.email(),
name: z.string(),
age: z.number().int().gte(0).optional(),
});
export type CreateUserRequest = z.infer<typeof createUserRequestSchema>;

export const errorSchema = z.strictObject({
message: z.string(),
code: z.string().optional(),
});
export type Error = z.infer<typeof errorSchema>;

// Inline Response Schemas
export const getUsersResponseSchema = z.array(userSchema);
export type GetUsersResponse = z.infer<typeof getUsersResponseSchema>;

The client wraps Playwright's APIRequestContext with one method per operation and no validation:

// tests/api/client.ts
import type { APIRequestContext, APIResponse } from "@playwright/test";
import type { ApiRequestContextOptions } from "@spartaficial/openapi-to-zod-playwright/runtime";
import { serializeParams } from "@spartaficial/openapi-to-zod-playwright/runtime";

export class ApiClient {
constructor(private readonly request: APIRequestContext) {}

/**
* @summary Get all users
* @method GET /users
* @returns Raw Playwright APIResponse
*/
async getUsers(options?: ApiRequestContextOptions): Promise<APIResponse> {
const serializedOptions = options ? { ...options, params: serializeParams(options.params) } : undefined;
return await this.request.get(`/users`, serializedOptions);
}

async postUsers(options?: ApiRequestContextOptions): Promise<APIResponse> {
/* ... */
}

async getUsersByUserId(userId: string, options?: ApiRequestContextOptions): Promise<APIResponse> {
const serializedOptions = options ? { ...options, params: serializeParams(options.params) } : undefined;
return await this.request.get(`/users/${encodeURIComponent(String(userId))}`, serializedOptions);
}

async deleteUsersByUserId(userId: string, options?: ApiRequestContextOptions): Promise<APIResponse> {
/* ... */
}
}

The service wraps the client with one method per documented success response, typed by what the response declares:

// tests/api/service.ts
import { expect } from "@playwright/test";
import { parseWithPrettifyErrorWithValues } from "@spartaficial/openapi-to-zod-playwright/runtime";
import { ApiClient } from "./client";
import { userSchema, getUsersResponseSchema } from "./schemas";
import type { User, CreateUserRequest, GetUsersResponse } from "./schemas";

export class ApiService {
constructor(private readonly _client: ApiClient) {}

/**
* @summary Get all users
* @method GET /users (200)
* @returns GetUsersResponse
*/
async getUsers(): Promise<GetUsersResponse> {
const response = await this._client.getUsers();

// Validate status code
expect(response.status(), await response.text()).toBe(200);

// Parse and validate response body
const body = await response.json();
return await parseWithPrettifyErrorWithValues(getUsersResponseSchema, body);
}

/**
* @summary Create a new user
* @method POST /users [application/json] (201)
* @returns User
*/
async postUsers(options: { data: CreateUserRequest }): Promise<User> {
const response = await this._client.postUsers(options);
expect(response.status(), await response.text()).toBe(201);
const body = await response.json();
return await parseWithPrettifyErrorWithValues(userSchema, body);
}

async getUsersByUserId(userId: string): Promise<User> {
/* asserts 200, validates userSchema */
}

/**
* @summary Delete user
* @method DELETE /users/{userId} (204)
*/
async deleteUsersByUserId(userId: string): Promise<void> {
const response = await this._client.deleteUsersByUserId(userId);
expect(response.status(), await response.text()).toBe(204);
}
}

Three details carry most of the value. The status assertion passes the response body as the assertion message, so a failing test shows what the server actually said. The body is parsed with the schema that the specification declares for that status code, and a mismatch fails the test with the offending path and value. And the request body parameter is typed CreateUserRequest, so the compiler catches a malformed test before it runs. The 400 and 404 responses did not get methods: by default only success responses do, and Status-code filters explains how to opt the error responses in.

Install​

npm install --save-dev @spartaficial/openapi-to-zod-playwright @playwright/test zod
npx openapi-to-zod-playwright init
npx openapi-to-zod-playwright

The two peer dependencies are @playwright/test (1.48.0 or later) and zod (^4.0.0); the generated files import both. @spartaficial/openapi-to-zod, which produces the schemas, is a regular dependency and is installed automatically. Node.js 20 or later is required; see requirements.md.

init asks for the specification, the schemas path (tests/schemas.ts), the client path (tests/client.ts), an optional service path, the configuration format and whether to include a defaults block with mode: "strict" and validateServiceRequest: false. The bare command generates; there is no generate subcommand. Generation needs a licence key in SPARTAFICIAL_LICENSE_KEY, while playwright test against the generated files, including the import of the package's runtime module, never does; see licensing.md.

Configure​

The configuration file is { defaults?, specs: [...], executionMode? }. Every spec names an input, an outputTypes (where the schemas go; output is a deprecated alias that must agree with it if both are set) and an outputClient. outputService and outputZodSchemas are optional. The output paths cannot be set in defaults, since each spec needs its own files. For this generator executionMode defaults to sequential, because each spec writes several interdependent files; set parallel to run specs concurrently. Discovery and merging are described in configuration-files.md.

import { defineConfig } from "@spartaficial/openapi-to-zod-playwright";

export default defineConfig({
defaults: {
mode: "strict",
includeDescriptions: true,
showStats: false,
validateServiceRequest: false,
},
specs: [
{
input: "specs/api-v1.yaml",
outputTypes: "tests/api/v1/schemas.ts",
outputClient: "tests/api/v1/client.ts",
outputService: "tests/api/v1/service.ts",
stripPathPrefix: "/api/v1",
basePath: "/api/v1",
ignoreHeaders: ["Authorization", "X-*"],
operationFilters: {
includeTags: ["public"],
includeStatusCodes: ["2xx", "4xx"],
},
},
],
});

The options that belong to this generator; the full table is options-zod-playwright.md.

OptionValuesDefaultSection
outputClientfile path, requiredThe two layers
outputServicefile pathnone (no service)The two layers
validateServiceRequestbooleanfalseRequest validation
zodErrorFormatstandard, prettify, prettifyWithValuesprettifyWithValuesValidation errors
preferredContentTypescontent types in order of preference["application/json"]Responses and content types
fallbackContentTypeParsingtext, json, bodytextResponses and content types
basePathpath prefixnonePaths
operationFilters.includeStatusCodes, excludeStatusCodescodes or Nxx rangesnoneStatus-code filters
useOperationIdbooleanfalseMethod names
ignoreHeadersheader names or glob patternsnoneParameters

Every schema option of openapi-to-zod is accepted as well and means the same thing: mode (default strict), emptyObjectBehavior, request and response, useDescribe, customDateTimeFormatRegex, uuidFormat, outputZodSchemas, enumFormat (union or const-object), typeAssertionThreshold and cacheSize are all explained in zod.md. The one exception is schemaType, which this generator does not accept: request bodies are always generated as request shapes and responses as response shapes, so a global direction would only break that. The shared options (includeDescriptions, showStats, showWarnings, fileHeader, batchSize, prefix, suffix, stripSchemaPrefix, stripPathPrefix, defaultNullable, operationFilters, refMappings) are covered by configuration-files.md, naming.md, operation-filters.md and specs-and-references.md.

The specification must declare at least one schema under components.schemas. A specification whose operations are all filtered out, or that has none, still gets its schemas file; the client and service are not written and a warning says why.

Use the output​

Playwright gives every test an APIRequestContext through the request fixture. Construct the client from it and the service from the client, and write the happy path against the service:

import { expect, test } from "@playwright/test";
import { ApiClient } from "./api/client";
import { ApiService } from "./api/service";

test("create and read back a user", async ({ request }) => {
const service = new ApiService(new ApiClient(request));

const created = await service.postUsers({
data: { email: "ada@example.com", name: "Ada" },
});
// Status 201 and the User schema have already been asserted
expect(created.email).toBe("ada@example.com");

const fetched = await service.getUsersByUserId(created.id);
expect(fetched).toEqual(created);
});

The base URL comes from Playwright's use.baseURL in playwright.config.ts, exactly as for any other request call; the generated paths are relative. Headers that every request needs, such as authentication, belong in use.extraHTTPHeaders.

To test that the API rejects bad input, use the client, which types nothing and validates nothing, and assert on the raw response:

test("rejects a user without a name", async ({ request }) => {
const client = new ApiClient(request);

const response = await client.postUsers({ data: { email: "nobody@example.com" } });

expect(response.status()).toBe(400);
expect(await response.json()).toMatchObject({ message: expect.any(String) });
});

When an error response is documented and you want it validated as well, opt it into the service with includeStatusCodes and call the method named after it:

// operationFilters: { includeStatusCodes: ["2xx", "4xx"] }
test("reports a missing user", async ({ request }) => {
const service = new ApiService(new ApiClient(request));

const error = await service.getUsersByUserIdNotFound("does-not-exist");
// Status 404 and the Error schema have been asserted
expect(error.message).toContain("not found");
});

The generated files contain no licence logic and can be committed with the tests.

The two layers​

The client is a passthrough. Each method takes the operation's path parameters as leading arguments and a single options object of type ApiRequestContextOptions, which is Playwright's request options with typed params, headers, data, form and multipart plus timeout, failOnStatusCode, ignoreHTTPSErrors, maxRedirects and maxRetries. Nothing is validated: you can send a body that violates the specification, omit a required parameter, or add a header the specification does not know, which is what a negative test needs. Path parameters are percent-encoded, so a value containing / cannot address a different endpoint, and params are serialised according to the specification before Playwright sees them (see Parameters). The method returns Playwright's APIResponse. Verbs that APIRequestContext has a named method for are sent through it; the others go through request.fetch.

The service is the validated layer, generated only when outputService is set. Each method takes the path parameters followed by an options object whose shape depends on the operation: params typed with the generated query schema, headers typed with the header schema, cookies for cookie parameters, and data, form or multipart for the body according to its content type. The object is optional when everything in it is optional and required when the body is required. The method calls the client, asserts the status code with expect(response.status(), await response.text()).toBe(…), reads the body in the way the content type calls for, validates it with the matching schema and returns the parsed value. A 204 or any response without a body returns Promise<void>.

The class names follow the file names: client.ts or api-client.ts produce ApiClient, service.ts produces ApiService, and petstore-client.ts produces PetstoreClient. With outputZodSchemas set, the schemas and types are split into two files (see zod.md) and the service imports values from one and types from the other.

Method names​

Method names are the same in both layers. Without an operationId, or with useOperationId at its default of false, a name is built from the HTTP method and the path: GET /users is getUsers(), GET /users/{userId} is getUsersByUserId(userId), POST /auth/login is postAuthLogin(). With useOperationId: true the specification's operationId is used where present (createUser(), getUserById(userId)), and the path-derived name is the fallback.

Service methods additionally carry a status suffix when there is something to tell apart. The suffix is the HTTP reason phrase in PascalCase, not the number:

Responses declaredService methods
200getSuccessOnly()
200, 201postMultiStatusOk(), postMultiStatusCreated()
200, 404 (with 4xx opted in)getMixedStatus(), getMixedStatusNotFound()
418 only (with 4xx opted in)getErrorOnlyImATeapot()
204deleteNoContent() returning Promise<void>
defaultsuffix Default
a code without a phrase, 299suffix 299

A lone success response gets no suffix; error responses always get one, since they never collapse into the bare name. Inline response schemas carry the same suffix (getInlineMixedNotFoundResponseSchema), so a method and the schema it validates with always line up. An operation that declares no responses, or no success response, keeps one method that asserts 200 and returns nothing. The complete rules, including how special characters in paths are folded, are in naming.md.

Status-code filters​

The operation filters shared by all generators select operations. Two filters specific to this generator select which responses of an operation get a service method: includeStatusCodes and excludeStatusCodes, under operationFilters. Both accept exact codes ("404") and ranges ("4xx"), and an exclude always wins over an include. They affect only the service; the client has one method per operation regardless.

ConfigurationMethods for an operation with 200, 201, 404, 500
none200, 201 (success only is the default)
includeStatusCodes: ["2xx", "4xx"]200, 201, 404
includeStatusCodes: ["200"]200
includeStatusCodes: ["4xx"]404
excludeStatusCodes: ["5xx"]200, 201 (excluding never widens past 2xx)
excludeStatusCodes: ["201"]200

Method and schema names are derived from the unfiltered specification, so narrowing a filter never renames anything: an operation with 200 and 201 keeps postMultiStatusOk() even when filtered to ["200"]. When a filter drops every response of an operation, the operation disappears from the service and the statistics block reports how many were dropped that way.

Request validation​

By default the service validates what comes back and trusts what you send, because the compiler already checks the shape of data and a test may deliberately send something odd. validateServiceRequest: true adds validation of the outgoing request against the generated schemas, before anything is sent:

async putUsersByUserId(userId: string, options: { params?: PutUsersByUserIdQueryParams; headers?: PutUsersByUserIdHeaderParams; data: UpdateUserRequest }): Promise<User> {
// Validate query parameters
if (options?.params) {
await parseWithPrettifyErrorWithValues(putUsersByUserIdQueryParamsSchema, options.params);
}

// Validate header parameters
if (options?.headers) {
await parseWithPrettifyErrorWithValues(putUsersByUserIdHeaderParamsSchema, options.headers);
}

// Validate request body
await parseWithPrettifyErrorWithValues(updateUserRequestSchema, options.data);

const response = await this._client.putUsersByUserId(userId, options);
// ...
}

Query parameters and headers are validated when present, a required body always, and an optional body only when it is given. The failure is reported in the format zodErrorFormat selects. This is useful when the test data itself is built dynamically and you want a wrong fixture to fail at the call site rather than as a confusing 400 from the server.

Validation errors​

zodErrorFormat decides what a failed schema validation throws from a service method.

zodErrorFormatGenerated callWhat a failure looks like
prettifyWithValues (default)parseWithPrettifyErrorWithValues(schema, body)ZodValidationError whose message lists each issue with its path and the value received
prettifyparseWithPrettifyError(schema, body)ZodValidationError whose message is Zod's prettifyError output
standardschema.parseAsync(body)Zod's own ZodError, with the issues array

With the default, a test report reads:

ZodValidationError:
✖ Too small: expected number >= 0 (received: -5)
→ at items[0].age
✖ Invalid input: expected string, received number (received: 42)
→ at items[1].name

The received value is omitted for objects and arrays, for unrecognised-key issues, and where Zod's message already states it. ZodValidationError extends Error and exposes zodError (the original ZodError), issues (its issues) and, in the prettifyWithValues format, input, the data that failed; its cause is the ZodError too. Choose standard if your tests already inspect error.issues and you want no helper import in the generated service. The same format applies to request validation when validateServiceRequest is on.

Responses and content types​

When a response declares several content types, preferredContentTypes picks the one the service method validates: the first entry that matches a declared type wins, exact matches before wildcard ones, and when nothing matches the first declared type is used. The same list picks the request content type when a request body offers several, which decides whether the method takes data (JSON), form (application/x-www-form-urlencoded, typed UrlEncodedFormData | FormData) or multipart (multipart/form-data, typed MultipartFormData). A request body offering both JSON and form encoding produces postUsers(options: { data: PostUsersRequest }) by default and postUsers(options: { form: UrlEncodedFormData | FormData }) with preferredContentTypes: ["application/x-www-form-urlencoded", "application/json"].

The content type also decides how the body is read before validation:

Response content typeRead with
application/json, text/json, anything ending in +jsonresponse.json()
text/*, application/xml, anything ending in +xml, JavaScriptresponse.text()
Images, PDF, archives, application/octet-stream and other binariesresponse.body()
Anything the generator does not recognisefallbackContentTypeParsing, with a warning naming the operation

A text/plain endpoint therefore becomes const body = await response.text() validated against z.string(), and an image/png endpoint reads a Buffer. fallbackContentTypeParsing defaults to text, which never throws; json is right when a vendor type such as application/x-custom-type is JSON in practice, and body keeps the raw bytes.

Parameters​

Query parameters follow the style and explode the specification declares, with OpenAPI's default of form and explode: true. An array tags: ["a", "b"] is sent as ?tags=a&tags=b; a parameter declared style: form, explode: false is sent as ?ids=1,2; spaceDelimited, pipeDelimited and deepObject are supported for arrays and objects. The client passes only the parameters that deviate from the default to serializeParams:

async getUsers(options?: ApiRequestContextOptions): Promise<APIResponse> {
const serializedOptions = options ? { ...options, params: serializeParams(options.params, { "ids": { style: "form", explode: false }, "codes": { style: "pipeDelimited", explode: false } }) } : undefined;
return await this.request.get(`/users`, serializedOptions);
}

serializeParams returns URLSearchParams, because repeated keys cannot be expressed in a plain object, and Playwright accepts it directly. Values that are null or undefined are omitted. A parameter with allowReserved: true keeps RFC 3986 reserved characters literal, which URLSearchParams cannot do, so the query is then handed to Playwright as a pre-built string. You can also pass params as a URLSearchParams or a raw string yourself and it is used as is.

Path parameters are percent-encoded and follow style and explode as well: the default simple style is unchanged, while label and matrix are rendered by serializePathParam, for primitives, arrays and objects.

Header parameters become an optional headers object typed with <Operation>HeaderParams, where every header is an optional string. ignoreHeaders removes headers by name or glob pattern, case-insensitively: no schema or type is generated for them and the service method no longer offers them, while the client, being a passthrough, still accepts any header. This is how Authorization stays out of the typed surface when extraHTTPHeaders supplies it. A pattern that matches no header produces a warning.

Cookie parameters have no Playwright request option of their own, so the service accepts them as a typed cookies object and serialises them into a Cookie header just before calling the client:

async getSession(options?: { params?: GetSessionQueryParams; cookies?: { tenant: string; sessionId?: string } }): Promise<Session> {
// Cookies travel as a Cookie header: Playwright has no per-request cookie option
const { cookies, ...restOptions } = options ?? {};
const cookieHeader = serializeCookieHeader(cookies);
const requestOptions = cookieHeader
? { ...restOptions, headers: { Cookie: cookieHeader } }
: restOptions;

const response = await this._client.getSession(requestOptions);
// ...
}

Parameters declared on the path item rather than the operation, and parameters reached through a $ref into components.parameters, are included everywhere the operation's own parameters are.

Paths​

Many specifications carry a prefix on every path, such as /api/v1.0. stripPathPrefix removes it before method names are derived, so GET /api/v1.0/users becomes getUsers() rather than getApiV10Users(), and basePath adds a prefix back when the request is sent. The two are usually set together:

{
input: "api.yaml",
outputTypes: "tests/schemas.ts",
outputClient: "tests/client.ts",
stripPathPrefix: "/api/v1.0",
basePath: "/api/v1.0",
}
/**
* @summary Get all users
* @method GET /api/v1.0/users
*/
async getUsers(options?: ApiRequestContextOptions): Promise<APIResponse> {
const serializedOptions = options ? { ...options, params: serializeParams(options.params) } : undefined;
return await this.request.get(`/api/v1.0/users`, serializedOptions);
}

stripPathPrefix accepts a literal or a glob (/api/v* matches /api/v1, /api/v2.5 and /api/v10), with or without leading and trailing slashes, and only matches at a segment boundary, so /api never shortens /apikeys. One thing to know when combining it with filters: in this generator includePaths and excludePaths are matched against the stripped path, so with stripPathPrefix: "/api/v1.0" write includePaths: ["/users/**"], not ["/api/v1.0/users/**"]. basePath applies to every operation in the spec; for APIs served under several prefixes, use one spec per prefix. The naming rules are in naming.md.

Runtime helpers​

The generated client and service import a few helpers from @spartaficial/openapi-to-zod-playwright/runtime, a second entry point of the same package that has no dependency on the generator and needs no licence to import. You can use them in your own test code too.

ExportPurpose
serializeParams(params, serializations?)Turns a params object into URLSearchParams (or a string when allowReserved is involved)
serializePathParam(name, value, serialization?)Renders a path parameter in simple, label or matrix style
serializeCookieHeader(cookies)Joins cookie parameters into a Cookie header value, dropping undefined
parseWithPrettifyErrorWithValues(schema, data)safeParseAsync that throws ZodValidationError with paths and received values
parseWithPrettifyError(schema, data)The same with Zod's prettifyError message
formatZodErrorWithValues(error, input), formatZodErrorPath(path)The formatting behind the two helpers
ZodValidationErrorThe error class, with zodError, issues and input

The types ApiRequestContextOptions, QueryParams, HttpHeaders, RequestBody, UrlEncodedFormData, MultipartFormData, MultipartFormValue, CookieParams, ParameterSerialization, ParameterSerializationMap, QueryParameterStyle, PathParameterSerialization and PathParameterStyle are exported from the same module. Why this module is separate from the main entry point, and what that means for your build, is explained in runtime-entry-points.md.

Programmatic API​

import { OpenApiPlaywrightGenerator, defineConfig } from "@spartaficial/openapi-to-zod-playwright";

const generator = new OpenApiPlaywrightGenerator({
input: "simple-api.yaml",
outputTypes: "tests/api/schemas.ts",
outputClient: "tests/api/client.ts",
outputService: "tests/api/service.ts",
operationFilters: { includeStatusCodes: ["2xx", "4xx"] },
});

generator.generate(); // writes every configured file
const schemas = generator.generateSchemasString(); // the schemas file as a string
const client = generator.generateClientString(); // the client class, without imports
const service = generator.generateServiceString(); // the service class, without imports
const types = generator.generateTypesString(); // the types file of separate mode

OpenApiPlaywrightGenerator takes an OpenApiPlaywrightGeneratorOptions object, the same keys as one spec entry, and checks that input exists in its constructor. generate() writes the schemas (or types and schemas), the client and, when configured, the service, printing each path; the string methods return the corresponding code without writing. Warnings are printed at the end of each call. The licence is checked lazily on first use, as described in licensing.md.

defineConfig(config) returns its argument and gives .ts configuration files type checking; the package does not export a configuration loader. Any failure inside generate() is wrapped in a ClientGenerationError, whose message carries the original message and whose context.cause keeps it. The error classes CircularReferenceError, CliOptionsError, ConfigValidationError, FileOperationError, MissingDependencyError and SpecValidationError are re-exported from @spartaficial/openapi-core for instanceof checks; MissingDependencyError is what the CLI reports when a .ts configuration imports a package that is not installed where the configuration lives.

Exported types: OpenApiPlaywrightGeneratorOptions, PlaywrightConfigFile, PlaywrightOperationFilters, and, re-exported from openapi-to-zod, OpenApiGeneratorOptions, CommonSchemaOptions, RequestOptions, ResponseOptions, OperationFilters, ExecutionMode, OpenAPISpec and OpenAPISchema. The runtime helpers are deliberately not re-exported from the main entry point; import them from /runtime.