← Back to API Versioning for GIS Endpoints
This page shows how to retire a field, a property name or a default projection from a spatial API using machine-readable deprecation signals, with a removal date supported by evidence rather than optimism.
Context & When to Use
Spatial deprecations are heavier than ordinary ones. Removing a bbox property from a feature breaks any client that drew a rectangle from it. Changing the default output projection from 4326 to 3857 moves every coordinate by thousands of kilometres in a client that did not notice. Renaming geom to geometry looks trivial until you find a customer’s stored procedure parsing the old key. The blast radius is larger than the diff suggests, and the affected code is frequently outside your organisation.
A full version bump is the heavyweight answer and is often disproportionate — see Versioning Geospatial APIs Without Breaking Clients for when it is warranted. For a single field, the lighter path is to keep serving it while announcing its end date in the response itself, so any client with logging sees the warning without anybody reading a changelog.
Use this whenever the change is additive-then-subtractive: a new field replaces an old one, both are served for a window, and the old one goes away on a date announced in advance. Do not use it for changes that alter the meaning of an existing field — silently changing what distance is measured in deserves a version bump, not a header.
Runnable Implementation
from datetime import datetime, timezone
from email.utils import format_datetime
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request, Response
router = APIRouter(prefix="/v1/features", tags=["features"])
# One place to declare every in-flight deprecation
DEPRECATIONS: dict[str, dict[str, Any]] = {
"feature.bbox": {
"deprecated_at": datetime(2026, 3, 1, tzinfo=timezone.utc),
"sunset_at": datetime(2026, 11, 1, tzinfo=timezone.utc),
"replacement": "Compute from geometry, or request ?include=envelope",
"docs": "https://www.geospatial-api.com/core-geospatial-api-architecture-with-fastapi-postgis/api-versioning-for-gis-endpoints/",
},
}
def announce(response: Response, key: str) -> None:
"""Attach RFC 8594 Sunset plus a Deprecation header and a docs link."""
spec = DEPRECATIONS[key]
response.headers["Deprecation"] = format_datetime(spec["deprecated_at"], usegmt=True)
response.headers["Sunset"] = format_datetime(spec["sunset_at"], usegmt=True)
response.headers["Link"] = f'<{spec["docs"]}>; rel="deprecation"; type="text/html"'
# A human-readable hint for anyone reading a raw response
response.headers["Warning"] = (
f'299 - "field {key} is deprecated; {spec["replacement"]}"'
)
@router.get("")
async def list_features(
request: Request,
response: Response,
include: Annotated[str | None, None] = None,
) -> dict[str, Any]:
rows = await fetch_features(request) # your existing query
features = [_serialize(r) for r in rows]
# Still serving the deprecated field — so still announcing it
if any("bbox" in f for f in features):
announce(response, "feature.bbox")
await record_deprecated_use(request, "feature.bbox")
return {"type": "FeatureCollection", "features": features}The header set is deliberately redundant. Sunset is the machine-readable date, Deprecation marks when the clock started, Link points at the explanation, and Warning is the one a developer notices while poking at the API with curl.
Key Parameters & Options
| Header | Value | Purpose |
|---|---|---|
Deprecation | HTTP-date | When the field became discouraged |
Sunset | HTTP-date | When it stops working — the one clients automate against |
Link; rel="deprecation" | docs URL | Where the migration is explained |
Warning: 299 | free text | Human-readable; visible in a raw curl |
?include= | opt-in field list | Lets clients prove they no longer need the field |
| Brownout | 1 h, then 4 h | Converts silent dependence into a support ticket |
Sunset is the only one of these with a formal specification behind it, and it is the one worth getting exactly right: an HTTP-date in GMT, not an ISO 8601 timestamp, because clients parsing it will use an HTTP-date parser.
Finding out who still depends on it
The hard part is not announcing the change — it is knowing when it is safe to make. For a plain JSON response you cannot see which keys a client reads, so the usage signal has to be constructed.
The brownout is worth the discomfort. Schedule it, announce it in the same headers, run it during business hours in your customers’ time zones, and treat every ticket it generates as a success rather than an incident.
Which changes a header can carry, and which need a version
Not every change is a candidate. The test is whether a client that ignores the announcement gets a smaller response or a wrong one. Smaller is survivable and belongs in a deprecation window; wrong is not, and belongs behind a version.
Gotchas & Failure Modes
Sunsetin ISO 8601 format. RFC 8594 specifies an HTTP-date; a client using a strict parser silently ignores an ISO value and gets no warning at all.- Announcing on responses that do not contain the field. Deprecation headers on every response, including ones where the field is absent, train clients to ignore them. Announce only when actually serving the deprecated thing.
- A sunset date in the removal PR. By then it is too late to be a warning. The date must be published at announcement time and must not move earlier.
- Removing on the sunset date without a final check. Run the usage report the morning of the removal. If a large consumer appeared last week, a two-week extension is cheaper than an incident.
- Caches masking the headers. A CDN that strips or does not vary on these headers hides the warning from everyone behind it. Verify the headers survive the edge — the caching behaviour in Caching Vector Tiles at the Edge with Cache-Control applies to headers as well as bodies.
- Deprecating a geometry field without a replacement path. “Compute it yourself” is not a migration if the computation needs data the client does not have. Ship the replacement before starting the clock.
Verification Snippet
# The headers must be present, correctly formatted, and consistent
curl -sD - -o /dev/null "https://api.example.com/v1/features?bbox=-0.2,51.4,0,51.6" \
| grep -iE 'deprecation|sunset|link|warning'
# Deprecation: Sun, 01 Mar 2026 00:00:00 GMT
# Sunset: Sun, 01 Nov 2026 00:00:00 GMT
# Link: <https://www.geospatial-api.com/…/api-versioning-for-gis-endpoints/>; rel="deprecation"
# Warning: 299 - "field feature.bbox is deprecated; Compute from geometry, or request ?include=envelope"from email.utils import parsedate_to_datetime
def test_sunset_is_http_date_and_after_deprecation(client):
r = client.get("/v1/features", params={"bbox": "-0.2,51.4,0,51.6"})
dep = parsedate_to_datetime(r.headers["Deprecation"])
sunset = parsedate_to_datetime(r.headers["Sunset"])
assert sunset > dep
# A meaningful window, not a formality
assert (sunset - dep).days >= 90Related
- API Versioning for GIS Endpoints — when a header is not enough and a version bump is
- Versioning Geospatial APIs Without Breaking Clients — the heavyweight path
- Coordinate Reference Systems & SRID Handling — why changing a default projection is a breaking change
← Back to API Versioning for GIS Endpoints