Unit 05.04: Schema changes that do not break old data
Schema changes break in the direction people do not expect.
Optional additions are safe; widened enums are not
An old response against a new schema, and a new value against an old one.
The code tries both.
from pydantic import BaseModel
from typing import Literal, Optional
class TriageV1(BaseModel):
category: Literal["billing", "technical", "other"]
class TriageV2(BaseModel):
category: Literal["billing", "technical", "account", "other"]
confidence: Optional[float] = None
print("old response against the new schema:", TriageV2(category="billing"))
try:
TriageV1(category="account")
except Exception:
print("new value against the old schema: REJECTED")
print("""
Adding an optional field is backward compatible. Widening an enum breaks OLD
code reading NEW data -- which is the direction people do not expect.
During a rolling deploy both versions run at once, so deploy the readers before
anything starts producing the new value.
""")
The old response validates against the new schema because the added field is optional. The new value is rejected by the old schema - so the breakage is in old code reading new data.
During a rolling deploy both versions run simultaneously, which means readers have to be deployed before anything starts producing the new value.
The mistake this prevents
The mistake is deploying a schema change everywhere at once. There is always a window where both versions run, and the schema has to accept the data in flight for the length of it.
Takeaway
New fields optional with defaults; widened enums break old readers on new data. Deploy readers first, and assume both versions run during any rollout.
