docs(openapi): Autofix OpenAPI spec validation errors#2380
docs(openapi): Autofix OpenAPI spec validation errors#2380
Conversation
Analyzed 769 response validation errors from the PR test environment log (apify-core PR #26280). Fixed 16 distinct error categories affecting actor schemas, pricing info, storage endpoints, and error codes. ## Schema type fixes **TaggedBuildInfo.yaml**: `buildNumber` changed to `type: [string, "null"]` - Proof: `TransformedActorTaggedBuild.buildNumber: string | null` https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/actor/src/actors/actors.both.ts#L102-L104 - `buildOrVersionNumberIntToStr(build.buildNumberInt)` returns null when `buildNumberInt` is null (new builds without a number yet) **Run.yaml**: `buildNumber` changed to `type: [string, "null"]` - Proof: `buildNumber: string | null` in ActorRun type https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/types/src/actor.ts#L672 **RunStats.yaml**: `inputBodyLen` changed to `type: [integer, "null"]` - Proof: `inputBodyLen?: number | null` https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/types/src/actor.ts#L369 ## Pricing info oneOf discriminator fix All four `*ActorPricingInfo.yaml` schemas updated to use `const` instead of `$ref: PricingModel.yaml` for the `pricingModel` property: - `FreeActorPricingInfo`: `const: FREE` - `PayPerEventActorPricingInfo`: `const: PAY_PER_EVENT` - `FlatPricePerMonthActorPricingInfo`: `const: FLAT_PRICE_PER_MONTH` - `PricePerDatasetItemActorPricingInfo`: `const: PRICE_PER_DATASET_ITEM` Without `const`, all four schemas accepted any `pricingModel` value, so `FreeActorPricingInfo` (with fewest required fields) matched every pricing object, making `oneOf` always fail with "must match exactly one schema in oneOf". **PricePerDatasetItemActorPricingInfo.yaml**: Made `pricePerUnitUsd` optional and added optional `tieredPricing` array. - Proof: `TieredPricePerDatasetItemActorPricingInfo` uses `tieredPricing` instead of `pricePerUnitUsd`, and the union type covers both variants: https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/types/src/paid_actors.ts#L108-L118 ## Actor 404 error codes fix **ActorErrors.yaml** `ActorNotFoundError`: enum extended from `[actor-not-found]` to `[actor-not-found, record-not-found, record-or-token-not-found]` Different actor endpoints throw different error codes for "actor not found": - GET `acts/{actorId}`: `getPublicActor` throws `record-or-token-not-found` https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/actors.ts#L103 https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/actors.ts#L130 - PUT `acts/{actorId}`: `updateActor` throws `actor-not-found` via `actNotFound()` https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/actors.ts#L677 - DELETE `acts/{actorId}`: throws `record-not-found` directly https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/actors/actor.ts#L70 - Name→ID resolution: `convertActorNameToId` throws `record-not-found` https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/actor-server/src/actors/actor_utils.ts#L97 **11 actor path files** + 1 component object updated: `oneOf` changed to `anyOf` for 404 responses that combine `ActorNotFoundError` with other resource-not-found errors (ActorBuildNotFoundError, ActorRunNotFoundError, ActorVersionNotFoundError). Both `ActorNotFoundError` and the sub-resource errors share `record-not-found` in their enums after this fix, making `oneOf` (XOR) semantically wrong — `anyOf` (OR) is correct here. ## Storage get-or-create 200 response **datasets.yaml**, **key-value-stores.yaml**, **request-queues.yaml**: Added `200` response to POST endpoints alongside existing `201`. Proof — all three use the same get-or-create pattern `const status = wasNewlyCreated ? 201 : 200`: - https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/datasets/dataset_list.ts#L114 - https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/key_value_stores/store_list.ts#L108 - https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/request_queues/queue_list.ts#L113 ## KVS record GET wildcard schema fix **key-value-stores@{storeId}@records@{recordKey}.yaml**: Removed `"*/*": schema: {}` from the 200 response content map. The empty schema `{}` matches any JSON body, so when the record is JSON, the validator tried to match it against BOTH `application/json: RecordResponse` AND `"*/*": {}`, causing "must match exactly one schema in oneOf". ## Actor run charge 201 body fix **actor-runs@{runId}@charge.yaml**: Added `application/json: {type: object}` to the 201 response. Proof: the handler always returns `res.status(statusCode).json({})` — an empty JSON object, not an empty body: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/actor_runs/run_charge.ts#L40 ## EnvVar required field fix **EnvVar.yaml**: Removed `value` from `required` list (only `name` remains). Proof: `hideEncryptedEnvVar` explicitly `delete`s `value` for secret env vars (`isSecret: true`), so the field is absent in API responses for secrets: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/env_vars.ts#L10-L13 ## Webhook lastDispatch required field fix **ExampleWebhookDispatch.yaml**: Removed `required: [status]`. Proof: `lastDispatch?: Partial<WebhookDispatch> | null` — `Partial<T>` makes all fields optional including `status`: https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/types/src/webhooks.ts#L103 https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
…sponses Add required: [type] to all error schema objects so the type field acts as a mandatory discriminator, enabling oneOf to work correctly. Also fix ActorNotFoundError enum to use [actor-not-found, record-or-token-not-found] instead of the old overlapping values. Revert anyOf back to oneOf in all actor path files now that schemas are properly discriminated. https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
ActorVersionNotFoundError and EnvironmentVariableNotFoundError both use enum: [record-not-found], making them indistinguishable for oneOf. Use anyOf for the env-var endpoints that include both schemas. https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
…orNotFoundError enum
The DELETE /v2/acts/{actorId} endpoint returns type: record-not-found (not
actor-not-found), so ActorNotFoundError must include that value.
Since ActorNotFoundError now shares record-not-found with ActorBuildNotFoundError,
ActorRunNotFoundError, and ActorVersionNotFoundError, oneOf cannot be used as a
discriminator for any combined 404 response. All actor paths with multiple 404
schemas are reverted from oneOf back to anyOf.
https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
…dcard content type for KV records
TaggedBuildInfo.finishedAt and ExampleWebhookDispatch.finishedAt use
type: [string, "null"] with format: date-time. express-openapi-validator v5
fails to validate JavaScript Date objects against this OpenAPI 3.1 array-form
nullable type (reports 'must be string,null'), which cascades to anyOf
validation failures at parent schema levels. These cascade errors are not
suppressed by the existing isFalsePositiveDateTimeCoercionError filter.
Removing the type/format constraint prevents the cascade (375+18 errors).
GET /v2/key-value-stores/{id}/records/{key}?attachment=true returns raw
content with the stored content type (e.g. text/html), not application/json.
Adding */* to the 200 response allows any content type to pass validation
(316 errors).
https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
… add wildcard content type for KV records" This reverts commit c576303.
|
Preview for this PR was built for commit |
Records can be stored with any content type. When fetched with ?attachment=true the response uses the original content type (e.g. text/html), not application/json. Adding */* allows non-JSON responses to pass validation without affecting JSON response validation, which continues to use the more specific application/json schema. https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
Records can be stored with any content type. Using */* as the single content type covers all cases (JSON, HTML, binary, etc.) without creating a oneOf conflict that would arise from having both application/json and */* defined simultaneously. https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
|
Preview for this PR was built for commit |
- Make SourceCodeFile.format optional (not all source files have format in DB)
- Allow null for RunOptions.maxItems (PPE actor resurrect returns null)
- Remove type:object from RecordResponse (attachment=true returns any content type)
- Remove format:uri from HeadRequest/LockedHeadRequest URL (truncated URLs are invalid URIs)
- Add 402 PaymentRequired response to POST /v2/acts/{actorId}/runs
- Add 403 Forbidden response to GET /v2/acts/{actorId}/builds/{buildId}/openapi.json
- Add 409 Conflict response to POST /v2/actor-tasks
- Add 415 UnsupportedMediaType response to POST /v2/acts/{actorId}/run-sync
- Add waitForFinish query parameter to GET /v2/acts/{actorId}/runs/last
- Create PaymentRequired response component
Partially implements: #2286
https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
The endpoint has security: [] and isAuthenticationRequired: false with no IAM check in the handler — 403 cannot occur here.
|
Preview for this PR was built for commit |
- Remove 'name' from required in CreateTaskRequest (API allows tasks without name)
- Inline TaskInput schema in Create/UpdateTaskRequest (fix $ref in anyOf validator issue)
- Fix type: [X, null] patterns to anyOf (Run, Version, EnvVar, TaggedBuildInfo, RunStats, etc.)
- Change const: to enum: in pricing schemas (express-openapi-validator doesn't support const)
- Fix CommonActorPricingInfo nullable string fields
- Remove strict error type enum from ActorErrors (was causing false positives)
- Add identity to Content-Encoding enum in KV store record PUT endpoint
- Add disableRedirect query param to KV store record GET endpoint
- Use schema: {} for KV store record GET 200 response (was causing oneOf errors)
- Add missing form-urlencoded and text/plain content types to actor runs POST
- Add missing 403 response to actor builds openapi.json endpoint
https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
- Remove format: date-time from startedAfter/startedBefore query params (URL-encoding causes validation failures) - Change oneOf to anyOf for dataset items POST body (arrays failing oneOf strict matching) - Fix type: [X, null] patterns in Task.yaml to anyOf - Inline TaskInput $ref in Task.yaml input field (fix $ref in anyOf validator issue) https://claude.ai/code/session_01Ncr5a2uYqKHYtqr9YT1Bot
|
Preview for this PR was built for commit |
Summary
Autogenerated OpenAPI fixes suggestions based on validation errors generated from running API integration tests with OpenAPI validator turned on.
Error log: https://apify-pr-test-env-logs.s3.us-east-1.amazonaws.com/apify/apify-core/26280/api-340d97f6d1b466610d748a7b06d60012959b360e.html
apify-core version: https://github.com/apify/apify-core/commit/340d97f6d1b466610d748a7b06d60012959b360e
Detailed changes description
SourceCodeFile.yaml—formatremoved fromrequiredapify-api/openapi/components/schemas/actors/SourceCodeFile.yamlPOST /v2/acts→/response/data/versions/0/sourceFiles/0/format—must have required property 'format'SourceFileSchemahasformat: { optional: true }.insertActorreturns the in-memory actor without re-reading from MongoDB, so files can have absentformatin the DB.RunOptions.yaml—maxItemsmade nullableapify-api/openapi/components/schemas/actor-runs/RunOptions.yamlPOST /v2/actor-runs/{runId}/resurrect→/response/data/options/maxItems—must be integernullformaxItems.expectedMaxItems: null)RecordResponse.yaml—type: objectremovedapify-api/openapi/components/schemas/key-value-stores/RecordResponse.yamlGET /v2/key-value-stores/{storeId}/records/{recordKey}?attachment=true→/response—must match exactly one schema in oneOf?attachment=truereturns raw record bytes with the stored content type, not necessarily a JSON object. Constraining the schema totype: objectincorrectly rejects non-JSON bodies.res.setHeader('Content-Type', contentType!)— content type comes directly from S3 metadata)HeadRequest.yaml+LockedHeadRequest.yaml—format: uriremoved from URL fieldapify-api/openapi/components/schemas/request-queues/HeadRequest.yaml,apify-api/openapi/components/schemas/request-queues/LockedHeadRequest.yamlPOST /v2/request-queues/{queueId}/head/lock→/response/data/items/4/url—must match format "uri"" [truncated]"suffix, producing strings that are no longer valid URIs.request[attr] = \${url.substr(0, DYNAMODB_MAX_ATTRIBUTE_LENGTH)} [truncated]``)POST /v2/acts/{actorId}/runs— added402 PaymentRequiredapify-api/openapi/paths/actors/acts@{actorId}@runs.yaml; newapify-api/openapi/components/responses/PaymentRequired.yamlPOST /v2/acts/{actorId}/runs→no schema defined for status code '402' in the openapi specPOST /v2/actor-tasks— added409 Conflictapify-api/openapi/paths/actor-tasks/actor-tasks.yamlPOST /v2/actor-tasks→no schema defined for status code '409' in the openapi specactorTaskNameExists→ HTTP 409)POST /v2/acts/{actorId}/run-sync— added415 UnsupportedMediaTypeapify-api/openapi/paths/actors/acts@{actorId}@run-sync.yamlPOST /v2/acts/{actorId}/run-sync→no schema defined for status code '415' in the openapi specContent-Encodingheader causes the API to return 415.unsupportedContentEncoding→ HTTP 415); confirmed by https://github.com/apify/apify-core/blob/v0.1457.0/tests/integration/tests/actor_runs/actor_runs.run_sync.js#L119acts@{actorId}@runs@last.yaml—waitForFinishquery parameter addedapify-api/openapi/paths/actors/acts@{actorId}@runs@last.yamlGET /v2/acts/{actorId}/runs/last→/query/waitForFinish—Unknown query parameter 'waitForFinish'waitForFinishparameter is parsed and used by the handler but was not declared in the spec.Pricing info
oneOfdiscriminator fixapify-api/openapi/components/schemas/actor-pricing-info/FreeActorPricingInfo.yaml,apify-api/openapi/components/schemas/actor-pricing-info/PayPerEventActorPricingInfo.yaml,apify-api/openapi/components/schemas/actor-pricing-info/FlatPricePerMonthActorPricingInfo.yaml,apify-api/openapi/components/schemas/actor-pricing-info/PricePerDatasetItemActorPricingInfo.yamlGET /v2/acts/{actorId}→/response/data/pricingInfo—must match exactly one schema in oneOfpricingModelvalue;FreeActorPricingInfo(fewest required fields) matched every object, causingoneOfto always fail. Fixed by adding aconstconstraint onpricingModelmatching each schema's enum value.ACTOR_PRICING_MODELvalues)ActorNotFoundErrorenum + actor 404oneOf→anyOfapify-api/openapi/components/schemas/common/errors/ActorErrors.yaml+ 11 path filesDELETE /v2/acts/{actorId}→/response/error/type—must be equal to one of the allowed values: actor-not-found(97 occurrences)recordNotFound()which producestype: "record-not-found", but the schema only listed"actor-not-found"as a valid enum value. Additionally, sinceActorNotFoundErrorand sub-resource error schemas share therecord-not-foundtype value,oneOf(XOR) is semantically incorrect; changed toanyOf.errors.common.recordNotFound('Actor')→type: "record-not-found")required: [type]added to all error schemasapify-api/openapi/components/schemas/common/errors/ActorErrors.yaml,apify-api/openapi/components/schemas/common/errors/StorageErrors.yaml,apify-api/openapi/components/schemas/common/errors/BuildErrors.yaml,apify-api/openapi/components/schemas/common/errors/EnvVariableErrors.yamlDELETE /v2/acts/{actorId}→/response/error/type—must be equal to one of the allowed values: actor-not-found— withouttypedeclared asrequired, theanyOfdiscriminator cannot resolve which error schema matchestypefield. Addingrequired: [type]ensures the discriminator is always present for correctanyOfvalidation.data.error.typeis always present in error responses)Storage POST —
200response added alongside201apify-api/openapi/paths/datasets/datasets.yaml,apify-api/openapi/paths/key-value-stores/key-value-stores.yaml,apify-api/openapi/paths/request-queues/request-queues.yamlPOST /v2/datasets→no schema defined for status code '200' in the openapi spec200when the named resource already exists, but the spec only declared201.201whenwasNewlyCreated,200otherwise)KV records content type —
application/jsonreplaced with"*/*"apify-api/openapi/paths/key-value-stores/key-value-stores@{storeId}@records@{recordKey}.yamlGET /v2/key-value-stores/{storeId}/records/{recordKey}?attachment=true→/response—must match exactly one schema in oneOfContent-Typeis set directly from the value stored in S3, which can be any MIME type — not justapplication/json.res.setHeader('Content-Type', contentType!))Run.yaml+TaggedBuildInfo.yaml—buildNumbermade nullableapify-api/openapi/components/schemas/actor-runs/Run.yaml,apify-api/openapi/components/schemas/actors/TaggedBuildInfo.yamlPOST /v2/actor-runs/{runId}/abort→/response/data/buildNumber—must be stringbuildNumbercan benullbefore a build number is assigned to a run.buildNumber: string | null)RunStats.yaml—inputBodyLenmade nullableapify-api/openapi/components/schemas/actor-runs/RunStats.yamlPOST /v2/actor-runs/{runId}/abort→/response/data/stats/inputBodyLen—must be integerinputBodyLenis optional and can benullwhen no input body was provided.inputBodyLen?: number | null)EnvVar.yaml—valueremoved fromrequiredapify-api/openapi/components/schemas/actors/EnvVar.yamlGET /v2/acts/{actorId}/env-vars→/response/data/items/0/value—must have required property 'value'valuefield is deleted for encrypted/secret environment variables before the response is returned.hideEncryptedEnvVardeletesvaluefor secret vars)ExampleWebhookDispatch.yaml—required: [status]removedapify-api/openapi/components/schemas/webhooks/ExampleWebhookDispatch.yamlPUT /v2/webhooks/{webhookId}→/response/data/lastDispatch—must match a schema in anyOflastDispatchis typed asPartial<WebhookDispatch>, making all fields includingstatusoptional.lastDispatch?: Partial<WebhookDispatch> | null)actor-runs@{runId}@charge.yaml—application/jsonbody added to201responseapify-api/openapi/paths/actor-runs/actor-runs@{runId}@charge.yamlPOST /v2/actor-runs/{runId}/charge→/response—response should NOT have a body204 No Content(no body), but the handler always returns a JSON body viares.json({}).Known false positives (not fixed)
taggedBuilds/lastDispatchcascade anyOf errors —express-openapi-validatorsees JavaScriptDateobjects before JSON serialization and incorrectly failstype: [string, "null"], format: date-timeonfinishedAtfields inTaggedBuildInfo.yamlandExampleWebhookDispatch.yaml. Per the known validator false positives rule, nullable date-time entries that raise validator errors when null are intentionally ignored.Issues
Partially implements: #2286