Skip to main content
๐Ÿ›ก๏ธ Verified Technical Content: Written by Serhii Hrekov. | Last reviewed & updated in Git: July 21, 2026

Handling Environment Variables in OpenAPI Server URLs

ยท 4 min read
Serhii Hrekov
Senior Software Engineer & System Architect specializing in Python, Web Systems, Cloud Infrastructure & Automation

When defining server URLs in OpenAPI specs, you often need to reference environment-specific values (like staging or production subdomains). But using raw environment variables can result in placeholder values like default: unknown, which may confuse tools like Swagger UI, ReDoc, or code generators.

This guide explains the issue and offers clean solutions - with practical examples.


The Problemโ€‹

When using tools like swagger2openapi to convert from OpenAPI 2.0 (Swagger) to OpenAPI 3.0, this YAML:

host: ${RUN_APP}-gateway-${PROJECT_RUN_ID}.run.app
gets transformed into:

servers:
- url: https://${{RUN_APP}}-gateway-${{PROJECT_RUN_ID}}.run.app
variables:
RUN_APP:
default: unknown
PROJECT_RUN_ID:
default: unknown

This is valid OpenAPI 3.0, but:

default: unknown is not helpful in rendered docs. You don't want to expose real values (like UUIDs or production hostnames). Tools like Swagger UI may not render or generate SDKs correctly with unknown.

Solution 1: Use Safe Default Placeholdersโ€‹

Manually edit or post-process your servers: block to add safe, recognizable defaults:

servers:
- url: https://${{RUN_APP}}-gateway-${{PROJECT_RUN_ID}}.run.app
variables:
RUN_APP:
default: dev
PROJECT_RUN_ID:
default: xyz123

๐Ÿ“Œ This:

Keeps sensitive info private. Allows tools like Swagger UI to resolve a real-looking server URL. Keeps your schema valid and compatible with code generators.

Solution 2: Replace Server Block Dynamicallyโ€‹

Post-process the output YAML in a build script:

import yaml

with open("openapi-v3-autogen.yaml") as f:
data = yaml.safe_load(f)

data["servers"] = [
{ "url": "https://dev-gateway-abc123.run.app" }
]

with open("openapi-for-docs.yaml", "w") as f:
yaml.safe_dump(data, f)

โœ… Use this when:

Generating docs or SDKs. Wanting per-environment baked-in specs (dev, staging, prod).

Solution 3: Use Environment Substitution in CIโ€‹

If your OpenAPI spec is a template (e.g., openapi.template.yaml):

servers:
- url: https://${{RUN_APP}}-gateway-${{PROJECT_RUN_ID}}.run.app

Then in your CI/CD pipeline:

envsubst < openapi.template.yaml > openapi.yaml

Or with Python:

import os

with open("openapi.template.yaml") as f:
content = f.read()

rendered = content.format(
RUN_APP=os.getenv("RUN_APP", "dev"),
PROJECT_RUN_ID=os.getenv("PROJECT_RUN_ID", "abc123")
)

with open("openapi.yaml", "w") as f:
f.write(rendered)

โœ… Benefits:

Real values injected at runtime. Keeps config DRY. Sensitive data stays outside the repo.

Why You Should Avoid default: unknownโ€‹

Breaks Swagger UI rendering Confuses developers reading the spec Causes SDKs to use unknown as the base URL

Summary Tableโ€‹

Goal Solution Keep variables, safe defaults Add default: dev, default: xyz123 Bake in fixed server Override servers: in script Dynamic server per environment Use CI templating + envsubst

Final Thoughtsโ€‹

default: unknown is technically valid, but practically useless. Consider who will consume your OpenAPI spec: humans, tools, or both. Use the flexibility of OpenAPI 3.0 to keep things DRY and secure.