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_endpointsin 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.
Exceptions
Section titled “Exceptions”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.
Getting started
Section titled “Getting started”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:
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
Fix incorrect types
Section titled “Fix incorrect types”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"Add missing fields
Section titled “Add missing fields”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.
Rename properties
Section titled “Rename properties”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"Add Stainless metadata
Section titled “Add Stainless metadata”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"]Remove problematic fields
Section titled “Remove problematic fields”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"Append text to strings
Section titled “Append text to strings”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: trueDeduplicate schemas
Section titled “Deduplicate schemas”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.
Chain transforms
Section titled “Chain transforms”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"JSONPath queries
Section titled “JSONPath queries”Transforms use JSONPath Plus for targeting. Common patterns:
# Exact pathtarget: "$.components.schemas.User.properties.id"
# Wildcard (all schemas)target: "$.components.schemas.*"
# Recursive (all parameters anywhere)target: "$..parameters"
# Filter expressionstarget: '$.paths..parameters[?(@.name == "account_id")]'target: '$.components.schemas[?(@.type == "object")]'
# Multiple targetstarget: - "$.components.schemas.User" - "$.components.schemas.Admin"- Use
appendfor temporary fixes (fails when the spec includes the field) - Use
mergefor 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
Troubleshooting
Section titled “Troubleshooting”“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.
Available commands
Section titled “Available commands”For complete documentation of all commands and options, see the OpenAPI transforms reference.