chore!: unify argument validation to zod - #3935
Conversation
48101f6 to
acb4aab
Compare
e9dee4f to
5d71608
Compare
…gument results Message format matches #3716: the zod issue message plus the offending field path and the received value. parseArgument drops the label argument and returns the parsed (default-applied) value, typed as the input intersected with the schema output.
…nce validators Options are now destructured from the parse result with defaults declared in the schemas, all per-call schemas are hoisted to module scope, and instance-like options are validated with specific validators (instanceof for classes, objectWithKeys for interfaces) instead of anyObject.
Crawler and launcher classes build their strict options schema once as a static (optionsSchema) and consume the parse result. Adds missing optionsShape entries (requestManager, ignoreIframes, ignoreShadowRoots) and makes LazyDefaultHttpClient extend BaseHttpClient so it passes the new instanceof validation.
Consumers were mixing the class from @crawlee/http-client with the same-named interface from @crawlee/types, producing aliased duplicate imports (BaseHttpClient_2) in the public API reports. Since httpClient options are now validated with instanceof, the class is the actual contract — import it everywhere. Only the definition site keeps the interface (the class implements it).
…tionError Matches the formatter in apify-client-js#986 - a union's own message is a bare "Invalid input"; the offending arm and field only appear in the nested errors.
- validate Request.state with z.enum(RequestState), so SKIPPED (7) is accepted - hoist the shared urlPatternSchema into enqueue_links/shared.ts (was duplicated 4x) - move stranded imports above the schema consts in concurrency_system.ts - use safeParse instead of parse + try/catch for schema defaults in Configuration - inline catalog versions in optionalDependencies too when publishing
- parseArgument/ArgumentValidationError accept an optional label naming
the validated interface, appended to every error line the way ow's
errors ended with "in object `HttpCrawlerOptions`"; wired up at all
option-bag call sites (crawlers, launchers, enqueueLinks, storages)
- bare custom-schema failures now name the received type before the
value ("expected number, received string at `x`, got `3`"), skipped
when the two would read the same (NaN)
- new Request(...) rejects a bare URL string with a hint pointing at
the `{ url }` object form, and labels its errors with RequestOptions
"expected number, received the string `3` at `maxRequestRetries`" instead of a dangling ", got `3`" after the location. Messages that never name a received type (regex, min/max, enums) keep the plain suffix; NaN is named as itself and an empty string is made visible.
`schemas.arrayOf(item, 'numbers')` reports a top-level type miss as "expected an array of numbers, received the number `500`" instead of zod's bare "expected array"; per-element messages are unchanged. Adopted at every element-typed array option (status codes, urls, include/exclude patterns, mime types, domains).
… errors Also drop dead launchContext destructure defaults — BrowserCrawler's schema already defaults it to an empty object.
6e8f04e to
eeb92da
Compare
There was a problem hiding this comment.
Thank you @vladfrangu !
The diff looks like what I'd expect it to look like, I don't see much else to review 😅 If I understood correctly, this went through rounds of LLM reviews, right? Since the CI is green, I say let's merge and see what feedback we get. The longer we wait, the more conflicts there will be.
| launcher: schemas.anyObject.optional(), | ||
| }; | ||
|
|
||
| protected static override optionsSchema = z.strictObject(PlaywrightCrawler.optionsShape); |
There was a problem hiding this comment.
According to my findings on another issue (#3549 ), this will run during the import, which can add ~50ms to the @crawlee/playwright import (and other packages that use this pattern). Perhaps we can lazy-initialize this?
By no means is this something we have to solve now, though.
There was a problem hiding this comment.
Make an issue to track this and assign it to me please 🙏
Replaces the remaining `ow`-based argument validation with `zod` across all packages and reworks how validation results are consumed and reported. Closes #3716 - **`ow` is gone** — every argument check now goes through `parseArgument(value, schema, label?)` from `@crawlee/utils`, backed by shared zod schemas (`schemas`, exported via `@crawlee/utils/internal`). The `@sapphire/shapeshift` checks in `@crawlee/fs-storage` were converted too, so a single validation library remains. - **Parse results are used everywhere** — option defaults moved from destructuring into the schemas (`.default(...)`), and call sites destructure the typed parse result. `parseArgument` returns `TValue & z.output<TSchema>`, so call sites keep their declared TS types while gaining the defaults. - **Schemas are built once** — all per-call schemas are hoisted to module scope; crawler/launcher classes build their strict options schema once as a `static optionsSchema` next to `optionsShape`. The `urlPatternSchema` for `include`/`exclude` lives in `enqueue_links/shared.ts`, next to the type it validates. - **Specific validators instead of `anyObject`** — class-typed options use `z.instanceof(...)` (`BaseHttpClient`, `Configuration`, `EventManager`), interface-typed ones use duck-typed `objectWithKeys` validators (`storageBackend`, `requestManager`, `logger`, …), and element-typed arrays use the new `schemas.arrayOf(item, 'numbers')`. `ArgumentValidationError` (replacing ow's `ArgumentError`) renders one line per issue: the expected type, the received type and value folded into one clause, the offending field path, and the validated interface: ```text // v3 (ow) — first issue only Expected property `maxRequestRetries` to be of type `number` but received type `string` in object `HttpCrawlerOptions` // v4 (zod) — every issue, one line each Invalid input: expected number, received the string `many` at `maxRequestRetries` in `HttpCrawlerOptions` Invalid input: expected an array of numbers, received the number `500` at `additionalHttpErrorStatusCodes` in `HttpCrawlerOptions` Invalid input: expected boolean, received the string `yes` at `retryOnBlocked` in `HttpCrawlerOptions` ``` Details worth knowing: - Union failures expand into one line per failed arm (zod's own message is a bare "Invalid input"). - `NaN` is named as itself, an empty string renders as `''`, and arrays name their element type (``expected an array of URL patterns``) — none of which ow or stock zod reported. - `new Request('https://…')` gets a targeted hint pointing at the `{ url }` object form. - For programmatic handling, the error exposes zod's structured output: `error.issues` and the raw `ZodError` as a typed `cause`. The migration is documented in the v4 upgrading guide (`docs/upgrading/upgrading_v4.md`), including a rename-cheat-sheet entry. - Custom HTTP clients must now **extend `BaseHttpClient`** from `@crawlee/http-client` rather than just implementing the interface (all shipped clients already do; `LazyDefaultHttpClient` was converted). Same applies to test mocks — `Object.create(BaseHttpClient.prototype)` works. - One caveat of consuming parse results: zod object schemas return a pruned plain copy, so options holding class instances are validated with passthrough schemas (`z.custom`-based) to keep their prototypes — there are comments at the relevant schemas. - Fixes a few latent gaps surfaced along the way: `Request.state` now accepts `RequestState.SKIPPED` (validated via `z.enum(RequestState)`), and the publish-time catalog inlining covers `optionalDependencies`. - `ArgumentValidationError` and its formatter are intentionally kept close to the copy in apify/apify-client-js#986 — a follow-up may extract them into a shared package. --------- Co-authored-by: Martin Adámek <banan23@gmail.com>
…atalog dependency Two upstream v4 changes landed after the last E2E run and broke the suite: - the enqueueLinks split (#4010) changed the context helper's return value to the addRequestsBatched result, so the *-enqueue-links fixtures now assert on `addedRequests` being empty instead of deep-equality with the old shape - the zod validation unification (#3935) introduced a `catalog:` dependency, which npm cannot resolve when the platform builds the actor image; the E2E package-copy step now rewrites catalog deps to their pinned versions from pnpm-workspace.yaml, the same way it already rewrites `workspace:` deps
Replaces the remaining `ow`-based argument validation with `zod` across all packages and reworks how validation results are consumed and reported. Closes #3716 - **`ow` is gone** — every argument check now goes through `parseArgument(value, schema, label?)` from `@crawlee/utils`, backed by shared zod schemas (`schemas`, exported via `@crawlee/utils/internal`). The `@sapphire/shapeshift` checks in `@crawlee/fs-storage` were converted too, so a single validation library remains. - **Parse results are used everywhere** — option defaults moved from destructuring into the schemas (`.default(...)`), and call sites destructure the typed parse result. `parseArgument` returns `TValue & z.output<TSchema>`, so call sites keep their declared TS types while gaining the defaults. - **Schemas are built once** — all per-call schemas are hoisted to module scope; crawler/launcher classes build their strict options schema once as a `static optionsSchema` next to `optionsShape`. The `urlPatternSchema` for `include`/`exclude` lives in `enqueue_links/shared.ts`, next to the type it validates. - **Specific validators instead of `anyObject`** — class-typed options use `z.instanceof(...)` (`BaseHttpClient`, `Configuration`, `EventManager`), interface-typed ones use duck-typed `objectWithKeys` validators (`storageBackend`, `requestManager`, `logger`, …), and element-typed arrays use the new `schemas.arrayOf(item, 'numbers')`. `ArgumentValidationError` (replacing ow's `ArgumentError`) renders one line per issue: the expected type, the received type and value folded into one clause, the offending field path, and the validated interface: ```text // v3 (ow) — first issue only Expected property `maxRequestRetries` to be of type `number` but received type `string` in object `HttpCrawlerOptions` // v4 (zod) — every issue, one line each Invalid input: expected number, received the string `many` at `maxRequestRetries` in `HttpCrawlerOptions` Invalid input: expected an array of numbers, received the number `500` at `additionalHttpErrorStatusCodes` in `HttpCrawlerOptions` Invalid input: expected boolean, received the string `yes` at `retryOnBlocked` in `HttpCrawlerOptions` ``` Details worth knowing: - Union failures expand into one line per failed arm (zod's own message is a bare "Invalid input"). - `NaN` is named as itself, an empty string renders as `''`, and arrays name their element type (``expected an array of URL patterns``) — none of which ow or stock zod reported. - `new Request('https://…')` gets a targeted hint pointing at the `{ url }` object form. - For programmatic handling, the error exposes zod's structured output: `error.issues` and the raw `ZodError` as a typed `cause`. The migration is documented in the v4 upgrading guide (`docs/upgrading/upgrading_v4.md`), including a rename-cheat-sheet entry. - Custom HTTP clients must now **extend `BaseHttpClient`** from `@crawlee/http-client` rather than just implementing the interface (all shipped clients already do; `LazyDefaultHttpClient` was converted). Same applies to test mocks — `Object.create(BaseHttpClient.prototype)` works. - One caveat of consuming parse results: zod object schemas return a pruned plain copy, so options holding class instances are validated with passthrough schemas (`z.custom`-based) to keep their prototypes — there are comments at the relevant schemas. - Fixes a few latent gaps surfaced along the way: `Request.state` now accepts `RequestState.SKIPPED` (validated via `z.enum(RequestState)`), and the publish-time catalog inlining covers `optionalDependencies`. - `ArgumentValidationError` and its formatter are intentionally kept close to the copy in apify/apify-client-js#986 — a follow-up may extract them into a shared package. --------- Co-authored-by: Martin Adámek <banan23@gmail.com>
…atalog dependency Two upstream v4 changes landed after the last E2E run and broke the suite: - the enqueueLinks split (#4010) changed the context helper's return value to the addRequestsBatched result, so the *-enqueue-links fixtures now assert on `addedRequests` being empty instead of deep-equality with the old shape - the zod validation unification (#3935) introduced a `catalog:` dependency, which npm cannot resolve when the platform builds the actor image; the E2E package-copy step now rewrites catalog deps to their pinned versions from pnpm-workspace.yaml, the same way it already rewrites `workspace:` deps
Replaces the remaining
ow-based argument validation withzodacross all packages and reworks how validation results are consumed and reported.Closes #3716
What changed
owis gone — every argument check now goes throughparseArgument(value, schema, label?)from@crawlee/utils, backed by shared zod schemas (schemas, exported via@crawlee/utils/internal). The@sapphire/shapeshiftchecks in@crawlee/fs-storagewere converted too, so a single validation library remains..default(...)), and call sites destructure the typed parse result.parseArgumentreturnsTValue & z.output<TSchema>, so call sites keep their declared TS types while gaining the defaults.static optionsSchemanext tooptionsShape. TheurlPatternSchemaforinclude/excludelives inenqueue_links/shared.ts, next to the type it validates.anyObject— class-typed options usez.instanceof(...)(BaseHttpClient,Configuration,EventManager), interface-typed ones use duck-typedobjectWithKeysvalidators (storageBackend,requestManager,logger, …), and element-typed arrays use the newschemas.arrayOf(item, 'numbers').Error messages
ArgumentValidationError(replacing ow'sArgumentError) renders one line per issue: the expected type, the received type and value folded into one clause, the offending field path, and the validated interface:Details worth knowing:
NaNis named as itself, an empty string renders as'', and arrays name their element type (expected an array of URL patterns) — none of which ow or stock zod reported.new Request('https://…')gets a targeted hint pointing at the{ url }object form.error.issuesand the rawZodErroras a typedcause.The migration is documented in the v4 upgrading guide (
docs/upgrading/upgrading_v4.md), including a rename-cheat-sheet entry.Notes
BaseHttpClientfrom@crawlee/http-clientrather than just implementing the interface (all shipped clients already do;LazyDefaultHttpClientwas converted). Same applies to test mocks —Object.create(BaseHttpClient.prototype)works.z.custom-based) to keep their prototypes — there are comments at the relevant schemas.Request.statenow acceptsRequestState.SKIPPED(validated viaz.enum(RequestState)), and the publish-time catalog inlining coversoptionalDependencies.ArgumentValidationErrorand its formatter are intentionally kept close to the copy in refactor!: validate arguments with zod instead of ow apify-client-js#986 — a follow-up may extract them into a shared package.