Parsing GeoJSON Webhooks with FastAPI and Pydantic
Parsing GeoJSON webhooks with FastAPI and Pydantic requires strict schema enforcement, cryptographic verification, and idempotent processing. By defining RFC 7946-compliant Pydantic v2 models, you reject malformed spatial payloads at the HTTP layer before they reach your business logic. FastAPI’s native JSON deserialization handles this automatically, returning structured 422 Unprocessable Entity responses when coordinates, geometry types, or required fields violate the spec. This eliminates boilerplate validation and guarantees that your event-driven pipeline only receives spatially valid payloads.
Core Implementation: Strict Validation & Routing
The implementation below uses modern Pydantic v2 syntax, explicit type hints, and FastAPI’s dependency injection to ingest, verify, and route webhooks securely. It validates geometry structure, verifies HMAC signatures, and enforces idempotency keys.
import hashlib
import hmac
import json
import time
from typing import Any, Literal, Optional
from fastapi import FastAPI, Request, HTTPException, Header, Depends, status
from pydantic import BaseModel, Field, model_validator, ConfigDict
app = FastAPI(title="GeoJSON Webhook Ingestor")
# --- In-memory idempotency store (replace with Redis/DB in production) ---
IDEMPOTENCY_STORE: dict[str, float] = {}
REPLAY_WINDOW_SECONDS = 300 # 5 minutes
# --- Pydantic v2 GeoJSON Models ---
class Geometry(BaseModel):
model_config = ConfigDict(extra="forbid")
type: Literal["Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon"]
coordinates: list[Any]
@model_validator(mode="after")
def validate_coordinates(self) -> "Geometry":
if not self.coordinates:
raise ValueError("coordinates array cannot be empty")
return self
class GeoJSONFeature(BaseModel):
model_config = ConfigDict(extra="forbid")
type: Literal["Feature"]
geometry: Optional[Geometry] = None
properties: Optional[dict[str, Any]] = None
id: Optional[str | int] = None
class GeoJSONFeatureCollection(BaseModel):
model_config = ConfigDict(extra="forbid")
type: Literal["FeatureCollection"]
features: list[GeoJSONFeature]
@model_validator(mode="after")
def validate_features(self) -> "GeoJSONFeatureCollection":
if not self.features:
raise ValueError("FeatureCollection must contain at least one Feature")
return self
class WebhookPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
event_type: str
timestamp: float
data: GeoJSONFeature | GeoJSONFeatureCollection
# --- Security & Validation Dependencies ---
def verify_signature(payload_bytes: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
def check_idempotency(idempotency_key: str) -> None:
if idempotency_key in IDEMPOTENCY_STORE:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Duplicate request")
IDEMPOTENCY_STORE[idempotency_key] = time.time()
# --- Route Handler ---
@app.post("/webhooks/geojson", status_code=status.HTTP_202_ACCEPTED)
async def ingest_geojson_webhook(
request: Request,
x_webhook_signature: str = Header(..., alias="X-Webhook-Signature"),
x_idempotency_key: str = Header(..., alias="X-Idempotency-Key"),
webhook_secret: str = "your-secure-webhook-secret" # Load from env in production
):
# 1. Idempotency check
check_idempotency(x_idempotency_key)
# 2. Read raw bytes for signature verification
body = await request.body()
if not verify_signature(body, x_webhook_signature, webhook_secret):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")
# 3. Parse & validate JSON against Pydantic models
try:
payload = WebhookPayload.model_validate_json(body)
except Exception as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
# 4. Async processing (fire-and-forget or queue)
await process_geojson_event(payload)
return {"status": "accepted", "event_type": payload.event_type}
async def process_geojson_event(payload: WebhookPayload) -> None:
# Route to spatial pipeline, DB, or message broker
pass
Webhook Security & Idempotency Safeguards
Webhook ingestion is inherently untrusted. Relying solely on JSON parsing leaves you vulnerable to replay attacks, signature forgery, and duplicate event processing. The implementation above layers three critical defenses:
- HMAC-SHA256 Verification: The raw request body is hashed using a shared secret and compared against the
X-Webhook-Signatureheader. Usinghmac.compare_digest()prevents timing attacks that could leak secret material. - Idempotency Keys: Every webhook delivery should include a unique
X-Idempotency-Key. Checking this key against a short-lived store prevents duplicate processing if the sender retries due to network timeouts. - Strict Schema Boundaries: Pydantic’s
extra="forbid"configuration drops unexpected fields immediately. Combined with@model_validator(mode="after"), you catch structural violations (empty coordinate arrays, missing features) before they propagate downstream.
For production deployments, replace the in-memory IDEMPOTENCY_STORE with Redis or a relational database with TTL expiration. Store webhook secrets in a secrets manager, never in plaintext configuration.
Routing to Spatial Pipelines
Once validated, GeoJSON payloads can be safely routed to downstream services. FastAPI’s async architecture pairs naturally with message brokers like RabbitMQ or Kafka, allowing you to decouple ingestion from heavy spatial operations. Validated payloads can be indexed in PostGIS, streamed to real-time map renderers, or transformed into compact binary formats for low-latency transmission.
When designing event-driven spatial architectures, consider how payload validation feeds into broader Spatial Payload Routing & Parsing strategies. Routing decisions often depend on geometry type, bounding box, or custom properties. By enforcing schema compliance at the ingress layer, you guarantee that downstream consumers never handle malformed coordinate sequences or missing type declarations.
Production Considerations & Next Steps
- Coordinate Precision & Validation: Pydantic validates structure, not mathematical correctness. For strict RFC compliance, integrate libraries like
shapelyorgeojsonto verify closed polygons, correct coordinate ordering (longitude, latitude), and valid CRS references. - Binary Serialization: Large GeoJSON payloads consume bandwidth and increase JSON parsing latency. Converting validated features to Protocol Buffers reduces payload size by 40–70% and enables strongly typed streaming. See GeoJSON to Protobuf Mapping for schema translation patterns that preserve spatial semantics while optimizing throughput.
- Observability: Instrument webhook routes with structured logging. Capture
event_type, validation errors, and processing latency. Use OpenTelemetry to trace payloads from ingestion through spatial indexing or protobuf conversion. - Rate Limiting: Apply FastAPI’s
SlowAPIor API gateway rate limits to prevent abuse. Webhook endpoints are common targets for credential stuffing or payload flooding.
By combining FastAPI’s request lifecycle with Pydantic v2’s declarative validation, you build a resilient ingress layer that guarantees spatial correctness, cryptographic integrity, and exactly-once processing semantics.