Skip to content

docs(openapi): Autofix OpenAPI spec validation errors#2380

Draft
Pijukatel wants to merge 12 commits intomasterfrom
claude/fix-openapi-spec-CldTk
Draft

docs(openapi): Autofix OpenAPI spec validation errors#2380
Pijukatel wants to merge 12 commits intomasterfrom
claude/fix-openapi-spec-CldTk

Conversation

@Pijukatel
Copy link
Copy Markdown
Contributor

@Pijukatel Pijukatel commented Mar 30, 2026

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.yamlformat removed from required

RunOptions.yamlmaxItems made nullable

RecordResponse.yamltype: object removed

  • Files: apify-api/openapi/components/schemas/key-value-stores/RecordResponse.yaml
  • Error: GET /v2/key-value-stores/{storeId}/records/{recordKey}?attachment=true/responsemust match exactly one schema in oneOf
  • Root cause: ?attachment=true returns raw record bytes with the stored content type, not necessarily a JSON object. Constraining the schema to type: object incorrectly rejects non-JSON bodies.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/key_value_stores.ts#L549 (res.setHeader('Content-Type', contentType!) — content type comes directly from S3 metadata)

HeadRequest.yaml + LockedHeadRequest.yamlformat: uri removed from URL field

  • Files: apify-api/openapi/components/schemas/request-queues/HeadRequest.yaml, apify-api/openapi/components/schemas/request-queues/LockedHeadRequest.yaml
  • Error: POST /v2/request-queues/{queueId}/head/lock/response/data/items/4/urlmust match format "uri"
  • Root cause: URLs exceeding 128 characters are truncated with a " [truncated]" suffix, producing strings that are no longer valid URIs.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/request_queues/request_queue.ts#L329 (request[attr] = \${url.substr(0, DYNAMODB_MAX_ATTRIBUTE_LENGTH)} [truncated]``)

POST /v2/acts/{actorId}/runs — added 402 PaymentRequired

POST /v2/actor-tasks — added 409 Conflict

POST /v2/acts/{actorId}/run-sync — added 415 UnsupportedMediaType

acts@{actorId}@runs@last.yamlwaitForFinish query parameter added

Pricing info oneOf discriminator fix

  • Files: apify-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.yaml
  • Error: GET /v2/acts/{actorId}/response/data/pricingInfomust match exactly one schema in oneOf
  • Root cause: All four pricing schemas accepted any pricingModel value; FreeActorPricingInfo (fewest required fields) matched every object, causing oneOf to always fail. Fixed by adding a const constraint on pricingModel matching each schema's enum value.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/packages/consts/src/actors.ts#L191-L196 (defines the four distinct ACTOR_PRICING_MODEL values)

ActorNotFoundError enum + actor 404 oneOfanyOf

  • Files: apify-api/openapi/components/schemas/common/errors/ActorErrors.yaml + 11 path files
  • Error: DELETE /v2/acts/{actorId}/response/error/typemust be equal to one of the allowed values: actor-not-found (97 occurrences)
  • Root cause: The DELETE endpoint returns recordNotFound() which produces type: "record-not-found", but the schema only listed "actor-not-found" as a valid enum value. Additionally, since ActorNotFoundError and sub-resource error schemas share the record-not-found type value, oneOf (XOR) is semantically incorrect; changed to anyOf.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/actors/actor.ts#L70 (errors.common.recordNotFound('Actor')type: "record-not-found")

required: [type] added to all error schemas

  • Files: apify-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.yaml
  • Error: DELETE /v2/acts/{actorId}/response/error/typemust be equal to one of the allowed values: actor-not-found — without type declared as required, the anyOf discriminator cannot resolve which error schema matches
  • Root cause: API error responses always include a type field. Adding required: [type] ensures the discriminator is always present for correct anyOf validation.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/tests/integration/tests/other/api.js#L67 (integration test asserts data.error.type is always present in error responses)

Storage POST — 200 response added alongside 201

  • Files: apify-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.yaml
  • Error: POST /v2/datasetsno schema defined for status code '200' in the openapi spec
  • Root cause: All three storage endpoints use a get-or-create pattern that returns 200 when the named resource already exists, but the spec only declared 201.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/routes/datasets/dataset_list.ts#L114 (returns 201 when wasNewlyCreated, 200 otherwise)

KV records content type — application/json replaced with "*/*"

  • Files: apify-api/openapi/paths/key-value-stores/key-value-stores@{storeId}@records@{recordKey}.yaml
  • Error: GET /v2/key-value-stores/{storeId}/records/{recordKey}?attachment=true/responsemust match exactly one schema in oneOf
  • Root cause: The response Content-Type is set directly from the value stored in S3, which can be any MIME type — not just application/json.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/key_value_stores.ts#L549 (res.setHeader('Content-Type', contentType!))

Run.yaml + TaggedBuildInfo.yamlbuildNumber made nullable

RunStats.yamlinputBodyLen made nullable

EnvVar.yamlvalue removed from required

  • Files: apify-api/openapi/components/schemas/actors/EnvVar.yaml
  • Error: GET /v2/acts/{actorId}/env-vars/response/data/items/0/valuemust have required property 'value'
  • Root cause: The value field is deleted for encrypted/secret environment variables before the response is returned.
  • Reference: https://github.com/apify/apify-core/blob/v0.1457.0/src/api/src/lib/env_vars.ts#L10-L13 (hideEncryptedEnvVar deletes value for secret vars)

ExampleWebhookDispatch.yamlrequired: [status] removed

actor-runs@{runId}@charge.yamlapplication/json body added to 201 response

Known false positives (not fixed)

  • taggedBuilds / lastDispatch cascade anyOf errorsexpress-openapi-validator sees JavaScript Date objects before JSON serialization and incorrectly fails type: [string, "null"], format: date-time on finishedAt fields in TaggedBuildInfo.yaml and ExampleWebhookDispatch.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

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
@github-actions github-actions bot added this to the 137th sprint - Tooling team milestone Mar 30, 2026
@github-actions github-actions bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Mar 30, 2026
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit f10bae4 and is ready at https://pr-2380.preview.docs.apify.com!

…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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit a2c8459 and is ready at https://pr-2380.preview.docs.apify.com!

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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit d1088d9 and is ready at https://pr-2380.preview.docs.apify.com!

…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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit 1ede663b and is ready at https://pr-2380.preview.docs.apify.com!

…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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit c5763039 and is ready at https://pr-2380.preview.docs.apify.com!

… add wildcard content type for KV records"

This reverts commit c576303.
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit f990c58a and is ready at https://pr-2380.preview.docs.apify.com!

claude added 2 commits March 31, 2026 07:55
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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit 80736925 and is ready at https://pr-2380.preview.docs.apify.com!

@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit bf45b1cc and is ready at https://pr-2380.preview.docs.apify.com!

- 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
@Pijukatel Pijukatel changed the title docs(openapi): Fix OpenAPI spec response validation errors docs(openapi): Autofix OpenAPI spec validation errors Mar 31, 2026
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit 8dfbf3d6 and is ready at https://pr-2380.preview.docs.apify.com!

The endpoint has security: [] and isAuthenticationRequired: false with no
IAM check in the handler — 403 cannot occur here.
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit f3f8b865 and is ready at https://pr-2380.preview.docs.apify.com!

- 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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit 0c26b92b and is ready at https://pr-2380.preview.docs.apify.com!

- 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
@apify-service-account
Copy link
Copy Markdown

Preview for this PR was built for commit 080c6b8d and is ready at https://pr-2380.preview.docs.apify.com!

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

Labels

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