Handling breaking changes in production config schemas
A rolling deployment runs the old and new schema against the same environment at the same time. Rename a variable, make an optional field required, or remove a key, and one side breaks the instant the change lands — unless you stage it. This page sequences breaking schema changes safely, extending Schema Evolution & Versioning.
Problem 1: rename in a single release
# ANTI-PATTERN: old pods set DB_DSN, new model only accepts DATABASE_URL
class Settings(BaseSettings):
database_url: str # old pods inject DB_DSN -> ValidationError during rollout
The moment you switch the injected variable, every pod still running the old image fails to boot.
Problem 2: optional made required immediately
# ANTI-PATTERN: a field that was optional is now mandatory
class Settings(BaseSettings):
region: str # environments that never set REGION now crash
Promoting a field to required without a grace period breaks every environment that did not already set it.
Secure implementation
# config/migration.py
from pydantic import AliasChoices, Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(populate_by_name=True, extra="forbid")
# Step 1: accept both names during the overlap window.
database_url: str = Field(validation_alias=AliasChoices("DATABASE_URL", "DB_DSN"))
# Step 2: introduce the new field as optional with a safe default first.
region: str = "us-east-1"
@model_validator(mode="after")
def warn_legacy(self) -> "Settings":
# Emit a metric/log when the legacy name is still in use so you know
# when it is safe to drop the alias.
return self
The change ships in stages: accept both names, roll out, switch every environment to the new name, confirm the legacy name is unused, then remove the alias in a later release. New required fields land as optional-with-default first and are promoted only once every environment sets them.
Gotchas & version-specific behaviour
AliasChoicesreturns the first matching source — document which name wins if both are set.extra="forbid"means a removed field becomes an error; stop injecting it before deleting it.- Use
populate_by_name=Trueso both the field name and its alias resolve. - Run CI with
PYTHONWARNINGS=error::DeprecationWarningso deprecations cannot be ignored.
Production parity checklist
- Every rename ships with a two-name alias overlap window.
- New required fields are optional-with-default first, promoted later.
- Removal happens only after telemetry shows the old name/field is unused.
- CI validates the model with both old and new variable names during the window.
- Schema changes are recorded in a changelog operators read before deploying.
Conclusion
Stage every breaking change — alias the rename, default the new field, remove last — and old and new pods coexist without a failed boot. For the alias mechanism itself, see Backward-Compatible Config with validation_alias.