| name | erd-contract |
| description | Use whenever any change touches the shape of the /erd JSON payload (nodes, edges, columns, relationships) — whether in the FastAPI Pydantic models, the generated TypeScript types, or the React consumers. Ensures all three layers stay in sync. |
The /erd contract
The /erd endpoint returns one JSON object consumed by the webview. Three layers must agree on its shape:
server/src/dbterd_server/schemas/erd.py — Pydantic models. Source of truth.
webview/src/types/erd.ts — TypeScript types. Generated from #1, never hand-edited. Consumed by the api layer (webview/src/api/{client,stream}.ts).
@datnguye/erd-flow's ErdPayload — the renderer's structural type. App.tsx casts the fetched payload to it (next as ErdPayload), so a contract change must stay compatible with the package's expected shape (or the package must be updated and released first).
The shape
class Column(BaseModel):
name: str
data_type: str | None = None
description: str | None = None
is_primary_key: bool = False
is_foreign_key: bool = False
class ErdNode(BaseModel):
id: str
name: str
label: str | None = None
description: str | None = None
resource_type: Literal["model", "source", "seed", "snapshot"]
schema_name: str | None = None
database: str | None = None
columns: list[Column]
compiled_sql: str | None = None
class ErdEdge(BaseModel):
id: str
from_id: str
to_id: str
from_column: str | None = None
to_column: str | None = None
from_columns: list[str] = []
to_columns: list[str] = []
relationship_type: Literal["fk", "lineage"] = "fk"
name: str | None = None
label: str | None = None
cardinality: Literal["n1", "11", "1n", "nn", ""] = ""
class ErdMetadata(BaseModel):
generated_at: datetime
dbt_project_name: str
class ErdPayload(BaseModel):
nodes: list[ErdNode]
edges: list[ErdEdge]
metadata: ErdMetadata
Source-of-truth note: dbterd >=1.28's built-in json target emits this
nodes/edges/metadata shape natively. The server maps it near-passthrough
(deriving the singular from_column/to_column pair and injecting
edge-referenced columns missing from partial catalogs); it no longer
registers a custom dbterd target.
Rules for changes
- Never add a field only to the TS side. If the webview needs it, add it to
schemas.py first.
- Never rename a field without bumping a version header. Add
X-Erd-Version: N to the response and make the webview read it.
- After any
schemas.py change, run /sync-contract to regenerate webview/src/types/erd.ts.
- Stable edge IDs. The hash must be deterministic — we use it as a React key.
- Optional fields stay optional. If a field can be missing from the catalog, mark it
| None.
Checklist before finishing a contract change