Skip to content

feat!: validate API responses with zod schemas generated from the OpenAPI spec - #1016

Open
vdusek wants to merge 5 commits into
v3from
feat/openapi-zod-schemas
Open

feat!: validate API responses with zod schemas generated from the OpenAPI spec#1016
vdusek wants to merge 5 commits into
v3from
feat/openapi-zod-schemas

Conversation

@vdusek

@vdusek vdusek commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes: #1027

Description

This PR adds runtime validation of API responses using Zod schemas generated from the OpenAPI specification, similar to how the Python API client validates responses with Pydantic.

It follows up on #985, which introduced generated TypeScript types via openapi-typescript.

Schema generation

  • pnpm generate:models now generates both:
    • src/generated/api.ts — TypeScript types
    • src/generated/schemas.ts — Zod schemas
  • The nightly regenerate_models.yaml workflow regenerates both.
    • Schemas are generated by a small custom OpenAPI-to-Zod emitter in scripts/schema_emitter.mts.
  • The generated schemas intentionally:
    • use loose objects, so unknown response fields are preserved,
    • treat string enums as open, so unknown enum values are accepted,
    • use z.date() for date-time, because parseDateFields() runs before validation,
    • fail generation on unsupported JSON Schema keywords instead of silently ignoring them.

I also considered existing generators - hey-api, orval, kubb. None of them supports all the behaviors above natively, so the custom code would not disappear; it would move into resolvers or post-processing. Since our specification uses only a small subset of JSON Schema and all 240 generated schemas are cross-checked at compile time against the independently generated TypeScript types, keeping the emitter local seems like a better solution, and thanks to that to also avoid another dependency.

Validation

  • parseResponse() now unwraps the API response, parses date fields, and validates the result. All resource methods in the base clients go through it.
  • Invalid responses throw a new ResponseValidationError, which includes:
    • the request method and URL,
    • validation issues,
    • the original cause,
    • a message identifying the offending fields.
  • The API deviations found along the way were fixed in the specification itself (fix(openapi): align schemas with actual API responses apify-docs#2932), so there are no hand-written schema overrides now.

Tests

  • The mock server now uses spec-shaped fixtures generated from OpenAPI examples (test/mock_server/fixtures.ts), with their validity checked in fixtures.test.ts.
  • Tests that intentionally use synthetic responses, such as pagination, timeout, and URL-encoding tests, mock the validation step.
  • The integration suite remains the main check for real API/spec drift and passes on this branch.

Breaking changes

  • Responses that do not match the OpenAPI specification now throw ResponseValidationError.
  • ScheduleClient.getLog() now returns ScheduleInvoked[] instead of string, matching the actual API response.
  • TaskPublicConfig now follows the specification:
    • publishedAt is optional and read-only,
    • categorization is removed.

All breaking changes are documented in the v3 upgrading guide.

Bundle size

The browser bundle grows from 288 kB to 327 kB due to the generated schemas. The limit in rsbuild.config.ts is increased to 360 kB.

✍️ Drafted by Claude Code

vdusek added 2 commits August 27, 2026 14:22
The spec now describes `Task.isPublic`, `Task.publicConfig` and `TaskPublicConfig`, so the
hand-written gap filling them is dropped and `TaskPublicConfig` is declared on top of the
generated schema. That removes `categorization`, which the API does not return, and makes
`publishedAt` optional and read-only, as the spec states.
…nAPI spec

Every response a resource method turns into a typed value is now checked against a zod schema
generated from the specification (`scripts/generate_schemas.mts` -> `src/generated/schemas.ts`),
widened in `src/schemas.ts` where the API is known to deviate from it. Unknown fields and enum
values pass through; anything else that does not match throws `ResponseValidationError`. The
pipeline is `pnpm generate:models` and the nightly workflow regenerates both artifacts.

BREAKING CHANGE: a response that does not match the OpenAPI specification throws
`ResponseValidationError` instead of being returned as it is. `ScheduleClient.getLog()` returns
`ScheduleInvoked[]` rather than `string`, which is what the endpoint has always answered with.
@vdusek vdusek added adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. labels Aug 27, 2026
@vdusek vdusek self-assigned this Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

⚠️ There are broken links in the documentation.

See more at https://github.com/apify/apify-client-js/actions/runs/33656440345#summary-100336057352

vdusek added a commit to apify/apify-docs that referenced this pull request Aug 31, 2026
The OpenAPI spec now describes what the API actually returns: nullable
`generalAccess` on storages and runs (and not required on `Run`),
nullable `readme`/`input`/`changelog` in `ActorDefinition`, task `input`
as an object or an array of objects, the private user fields optional on
`UserPrivateInfo`, and `format: date-time` on `DailyServiceUsages.date`.
These are the six deviations the JS client had to widen its generated
zod schemas for in apify/apify-client-js#1016.

*✍️ Drafted by Claude Code*
@vdusek
vdusek marked this pull request as ready for review September 2, 2026 07:37
@vdusek
vdusek requested a review from szaganek as a code owner September 2, 2026 07:37
@vdusek
vdusek requested a review from B4nan September 2, 2026 07:37
Every conflict was the same import block: #1015 appended `.js` to the relative
specifiers while this branch changed what those lines import. Resolved by keeping
this branch's imports and adding the `.js` extensions, including on the files it
adds, which #1015 never saw.
@B4nan

B4nan commented Sep 2, 2026

Copy link
Copy Markdown
Member

The date parsing changes reminded me of #522, we should open this topic on slack in some public channel and agree on the direction.

@vdusek

vdusek commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

The date parsing changes reminded me of #522, we should open this topic on slack in some public channel and agree on the direction.

Sure, but I guess it does not block this PR, right? And we can do it later as a separate PR.

@B4nan

B4nan commented Sep 3, 2026

Copy link
Copy Markdown
Member

Yeah, that should be resolved separately, including an entry in the upgrading guide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants