Skip to content
FeedbackDashboard
Your OpenAPI spec
OpenAPI transforms

Make your OpenAPI spec appear differently to Stainless with transforms

Use spec transforms to modify auto-generated specs or customize your OpenAPI without modifying the source

Stainless transforms make your OpenAPI appear differently to Stainless without requiring you to modify the spec itself. Define transforms in your stainless.yaml config file to have them applied automatically every time we generate your code, docs, and other targets.

When to change your spec vs. when to use your Stainless config

Section titled “When to change your spec vs. when to use your Stainless config”

When you’re new to Stainless, using your Stainless config to experiment without changing your spec often makes the most sense. After your initial discovery and learning process, however, we recommend you make careful choices about when to modify your OpenAPI spec and when to use your Stainless config.

Your OpenAPI spec’s job is to accurately and comprehensively describe your API, and a good spec has a lot of utility and value. If changing your spec will improve its accuracy or comprehensiveness, you should change it instead of using your Stainless config.

Your Stainless config, on the other hand, is designed to provide additional information about your API not typically found in an OpenAPI spec, as well as Stainless-specific adjustments.

Here are some scenarios where using your Stainless config to make a change is the best option:

  • You need to rename a property in one of your Stainless SDKs to avoid a language-specific conflict, but you want to keep using the original name in your API and other SDKs
  • You need to add Stainless-specific metadata (e.g., x-stainless-skip)
  • You only want to support a subset of endpoints in your Stainless SDKs (you can add the endpoints you want Stainless to ignore under unspecified_endpoints in your Stainless config)

In short, whenever there’s a mismatch between your API and your OpenAPI spec, we recommend you change your OpenAPI spec so it aligns with your API. If a Stainless-specific change needs to be made, use your Stainless config.

There are situations where improving your spec would be ideal, but doing so isn’t practical. You might generate your spec using a system that doesn’t allow you to modify the output, or perhaps the process required to change your spec would take too long.

Ultimately, the goal is to have a working API with functional SDKs, docs, and so on. The main thing is to be aware of the situation you’re in, the tradeoffs you need to make, and that you document your choices for posterity.

If you’d like additional guidance in this area, let us know.

Suppose your API auto-generates a spec where account IDs are typed as number, but they’re actually strings. Add a transform to fix it:

stainless.yaml
openapi:
transforms:
- command: update
reason: "Account IDs are strings, not numbers"
args:
target: '$.paths..parameters[?(@.name == "account_id")].schema.type'
value: "string"

The target uses JSONPath to find all parameters named account_id. The transform replaces their type with "string". The reason documents why this transform exists.

Common uses for transforms:

  • Fix incorrect types in auto-generated specs
  • Add missing required fields while you fix the source spec
  • Rename properties to avoid language conflicts
  • Add Stainless metadata (x-stainless-naming, x-stainless-skip)
  • Remove incorrect defaults or deprecated fields
openapi:
transforms:
# Fix specific property
- command: update
reason: "User age should be integer"
args:
target: "$.components.schemas.User.properties.age.type"
value: "integer"
# Fix all instances across endpoints
- command: update
reason: "All limit parameters should be int32"
args:
target: '$.paths..parameters[?(@.name == "limit")].schema'
value:
type: "integer"
format: "int32"

Use append for temporary fixes. Fails if the field exists, which alerts you when the spec is fixed.

openapi:
transforms:
- command: append
reason: "Spec missing id field (TODO: fix in source)"
args:
target: "$.components.schemas.User.properties"
value:
id:
type: "string"
format: "uuid"
- command: append
reason: "Mark id as required"
args:
target: "$.components.schemas.User.required"
value: "id"

When the spec includes the id field, your transform will fail with “Properties already exist”, prompting you to remove it.

openapi:
transforms:
# Rename schema
- command: move
reason: "Normalize to PascalCase"
args:
from: "$.components.schemas.user_response"
to: "$.components.schemas.UserResponse"
# Rename property
- command: move
reason: "Fix snake_case to camelCase"
args:
from: "$.components.schemas.User.properties.first_name"
to: "$.components.schemas.User.properties.firstName"

Use merge for permanent additions like Stainless extensions:

openapi:
transforms:
# Language-specific naming
- command: merge
reason: "Avoid shadowing Python's timeout builtin"
args:
target: "$.components.schemas.Request.properties.timeout"
value:
x-stainless-naming:
python:
method_argument: "api_timeout"
# Skip parameters for specific SDKs
- command: merge
reason: "Terraform doesn't need pagination"
args:
target: '$.paths..parameters[?(@.name == "limit" || @.name == "offset")]'
value:
schema:
x-stainless-skip: ["terraform"]
openapi:
transforms:
# Remove specific keys
- command: remove
reason: "Remove incorrect default"
args:
target: "$.components.schemas.User.properties.middleName"
keys: ["default"]
# Remove entire nodes
- command: remove
reason: "Remove all deprecated parameters"
args:
target: '$.paths..parameters[?(@.deprecated == true)]'
# Remove examples
- command: remove
reason: "Remove auto-generated examples"
args:
target:
- "$.paths.*.*.examples"
- "$.components.schemas.*.example"

Use {{value}} templating to modify existing strings:

openapi:
transforms:
- command: update
reason: "Mark deprecated endpoint"
args:
target: "$.paths./v1/users.get.summary"
value: "{{value}}\n\n**DEPRECATED**: Use /v2/users"
template: true

Use matches_schema to find all schemas that match a template and replace them with a $ref.

openapi:
transforms:
- command: update
reason: "Replace duplicate error schemas with $ref"
args:
target:
- matches_schema: $.components.schemas.ErrorTemplate
ignore: $.components.schemas.ErrorTemplate
value:
$ref: "#/components/schemas/Error"

This finds all schemas that exactly match ErrorTemplate and replaces them with a reference. The ignore field prevents replacing the template itself.

Transforms run in order, so later transforms see the effects of earlier ones.

Extract an inline schema to components.

openapi:
transforms:
# Copy inline schema to components
- command: copy
reason: "Extract error schema for reuse"
args:
from: "$.paths./users.get.responses.404.content.application/json.schema"
to: "$.components.schemas.NotFoundError"
# Replace inline schema with $ref
- command: update
reason: "Use $ref instead of inline"
args:
target: "$.paths./users.get.responses.404.content.application/json.schema"
value:
$ref: "#/components/schemas/NotFoundError"

Transforms use JSONPath Plus for targeting. Common patterns:

# Exact path
target: "$.components.schemas.User.properties.id"
# Wildcard (all schemas)
target: "$.components.schemas.*"
# Recursive (all parameters anywhere)
target: "$..parameters"
# Filter expressions
target: '$.paths..parameters[?(@.name == "account_id")]'
target: '$.components.schemas[?(@.type == "object")]'
# Multiple targets
target:
- "$.components.schemas.User"
- "$.components.schemas.Admin"
  • Use append for temporary fixes (fails when the spec includes the field)
  • Use merge for permanent overrides such as x-stainless metadata
  • Document your reasons - future you will thank you
  • Test one transform at a time
  • Prefer explicit paths over broad wildcards when possible
  • Keep transforms focused - one transform should do one thing

“JSONPath did not match any nodes”

Your query didn’t find anything. Check for typos, verify the path exists, or simplify your query to test.

“Properties already exist”

You’re using append on a property that exists. Either remove the transform (if the spec was fixed) or switch to merge if you want to overwrite.

Transform does nothing

Check the order. If one transform renames a field and another targets the old name, the second won’t find anything. Adjust the order or update the target path.

For complete documentation of all commands and options, see the OpenAPI transforms reference.