A templated servers[].url with a variables block — the OpenAPI-native way to describe "sandbox vs. production," or a per-tenant subdomain — validates cleanly everywhere. But a generated SDK typically bakes the variables' default values into one hardcoded base-URL constant and generates no typed way to pick a different value, so the enum constraint you declared in the spec never reaches the client at all. Reproduced against real generator output on 2026-09-21.
What does this actually look like?
A server block with two variables — one constrained to a small set of environments, one a free-form path segment:
servers:
- url: https://{environment}.api.example.com/{basePath}
description: Configurable server
variables:
environment:
default: sandbox
enum: [sandbox, production]
basePath:
default: v1
Both swagger-cli validate v4.0.4 and @redocly/cli lint v2.53.3 accept this without complaint — it's a fully valid Server Object per the OpenAPI 3.1 spec, which requires only that default be present and, if enum is given, that it be non-empty. Nothing here looks wrong at the document level.
Generating a TypeScript client with OpenAPI Generator v7.25.0 (-g typescript-fetch) produces this real, unedited runtime.ts:
export const BASE_PATH = "https://sandbox.api.example.com/v1".replace(/\/+$/, "");
That's the entire result of the variables block: both placeholders substituted with their default values, baked into a single string constant. There is no generated type, enum, or constructor parameter anywhere in the client that represents environment or basePath as a variable — the enum: [sandbox, production] constraint you wrote in the spec is not present anywhere in the generated code.
Why does the generator throw the variable away instead of exposing it?
The OpenAPI 3.1 spec's Server Variable Object defines exactly three fields — enum, default (required), and description — and describes substitution only at the level of "the default value... SHALL be sent if an alternate value is not supplied." It says nothing about how a client library should expose that choice to calling code, because the Server Object is a documentation and default-connection construct, not a code-generation contract. Nothing in the spec obligates a generator to turn a server variable into a runtime-configurable, type-checked option — most target a single working base URL for local testing and treat everything else as the integrator's problem.
The practical effect: the one piece of information a caller most needs — "these are the only two valid environment values" — is exactly the piece that gets discarded. The generated client still lets you override the base path entirely (as an arbitrary string via Configuration.basePath), but by then you've lost the enum's type safety and are back to hand-typing a full URL.
How do you fix it?
You generally can't make the generator respect the enum at build time — this is generator behavior, not a spec defect, so the working fix is to stop relying on generated code to carry that choice at all. Two patterns that hold up:
Pass the resolved base URL at runtime instead of a template. Keep the servers block for documentation, but configure the generated client's basePath explicitly from your own typed constant, so your code — not the generated code — owns the enum:
// Before — relying on the generator to expose `environment` as a choice
const api = new DefaultApi(); // always sandbox, per the baked-in default
// After — your own typed enum drives the base URL explicitly
type Environment = "sandbox" | "production";
const BASE_URLS: Record<Environment, string> = {
sandbox: "https://sandbox.api.example.com/v1",
production: "https://production.api.example.com/v1",
};
const api = new DefaultApi(
new Configuration({ basePath: BASE_URLS[env] })
);
List every environment as its own full servers entry instead of one templated entry, when the set of values is genuinely fixed and small:
# Before — one templated entry, generator flattens it to the default
servers:
- url: https://{environment}.api.example.com/{basePath}
variables:
environment:
default: sandbox
enum: [sandbox, production]
basePath:
default: v1
# After — explicit entries; a human or generator reading the spec sees both options directly
servers:
- url: https://sandbox.api.example.com/v1
description: Sandbox
- url: https://production.api.example.com/v1
description: Production
This doesn't restore variable substitution, but it removes the false impression that the spec is communicating a constraint the generated code will actually enforce — the constraint now lives where callers can actually see and rely on it.
Tool reaction table
| Tool (version) | Result on a templated servers[].url with enum |
|---|---|
swagger-cli validate v4.0.4 |
Passes — fully valid Server Object |
@redocly/cli lint v2.53.3 |
Passes (only unrelated cosmetic warnings) |
OpenAPI Generator v7.25.0 (typescript-fetch) |
Bakes default values into one string constant; enum not represented anywhere |
How to catch this before it ships
- Don't assume a
servers[].urlvariable with anenumwill reach a generated SDK as a typed option — as reproduced above, it currently doesn't for the mainstream TypeScript target. This is the same category of gap covered in mixed-type enum errors: the spec permits more than a generator's target language construct can cleanly represent. - If an environment or tenant choice is safety-critical (wrong environment = wrong data), model it explicitly in your own client wrapper rather than trusting the spec's
variablesblock to enforce it downstream. - Check the generated
runtime.ts(or equivalent) after any change toservers— a silently baked-in default is easy to miss since the client still compiles and runs, just always against one environment. - Run the spec through the free in-browser OpenAPI validator to see the resolved server URL the way tooling will read it, and pair it with
/openapi-diff/when aserversblock changes between spec versions, since a variable'sdefaultflipping silently changes what "no configuration" means for every existing integration. General guidance on writing generator-friendly specs is in OpenAPI best practices for SDK-friendly specs.
If your team is generating a client SDK from a spec that already uses server variables for sandbox/production, Sourced's hosted docs and SDK pipeline previews the actual generated TypeScript and Python before you publish, so a base URL that's quietly pinned to sandbox shows up in review instead of in production traffic. Start a free report.
FAQ
Is a templated servers[].url invalid OpenAPI?
No. It's exactly what the Server Object and Server Variable Object are designed for, and both swagger-cli validate and redocly lint accept it cleanly, as reproduced above. This is a code-generation limitation, not a spec-validity problem.
Does every generator throw away the enum constraint, or is this specific to one target?
This post reproduces the behavior with OpenAPI Generator's typescript-fetch target specifically. Generators vary in how much of the Server Object they expose to calling code — some accept a base-path override at construction time (which loses the enum's type safety) and none tested here generate a compile-time-checked choice from the enum list itself. Verify the specific generator and target you use rather than assuming either behavior.
Should I stop using server variables in my spec, then?
Not necessarily — the servers block is still useful documentation, and tools that render docs (rather than generate code) typically do show the variable and its allowed values to a human reader. The gap is specifically between "documented in the spec" and "enforced in generated client code," and closing that gap is on your integration code, not the spec.
What's the difference between this and hardcoding multiple full servers entries?
A templated entry with variables is more compact and is the pattern OpenAPI itself recommends for parameterized URLs (auth-scoped subdomains, versioned paths). Multiple explicit entries are more verbose but make the actual set of valid base URLs visible directly in the spec text, with no substitution step required to see them — which matters when the audience is a generator that isn't going to perform that substitution usefully anyway.
Does OpenAPI 3.1 change any of this from 3.0?
No — the Server Variable Object's fields and substitution semantics are unchanged between OpenAPI 3.0.x and 3.1.x. This is generator behavior sitting on top of an unchanged part of the spec, not a version-specific issue.
Can I catch a silently-pinned base URL in code review?
Yes, but only by reading the generated output, not the spec — since the spec itself is valid and the diff between "generator respects the enum" and "generator flattens to default" is invisible in the YAML. Diffing the generated runtime.ts (or your target language's equivalent) after any servers change is the reliable check.