Documenting geometry columns in dbt YAML
This page shows you how to document a geometry column in dbt schema YAML so its SRID, geometry type, and precision are captured as structured, reusable metadata that surfaces in the generated dbt docs catalog.
When to use this approach
Document geometry columns in YAML — rather than leaving the type to be inferred or explained in a wiki — when any of these hold:
- Consumers keep asking “what projection is this in?” A
data_typeofgeometry(MultiPolygon, 4326)answers the question in the catalog, once, instead of in a recurring Slack thread. The pipeline-wide picture these column docs roll up into is spatial data lineage documentation. - You want the documentation to double as a contract. dbt schema contracts read the same
data_typeyou write for documentation, so a documented SRID can be enforced, not just described — the enforcement side is enforcing a canonical SRID across dbt models. - Geometry precision or type changes between layers and readers need to know which model is safe to measure against. If instead you are tracking breaking changes to those columns over time, pair this with versioning spatial schemas in dbt.
Prerequisites
- dbt Core 1.6+ so column-level
data_type,meta, and{{ doc() }}blocks all resolve, and contracts can consume thedata_type. - A spatial adapter that reports geometry types to the catalog —
dbt-postgreson PostGIS 3.x, ordbt-duckdbwith the spatial extension. - A canonical project SRID agreed up front (this guide stores downstream geometry in EPSG:4326), and a place to serve
dbt docs. docs-pathsconfigured so the project globs your documentation.mdfiles:
# dbt_project.yml
docs-paths: ["models/docs"]
vars:
canonical_srid: 4326
Step-by-step instructions
1. Declare the column with its concrete data_type
Start with the single most valuable fact: the typed declaration. PostGIS and DuckDB both accept a parameterized geometry type, and writing it into data_type records the geometry type and the SRID in one string the catalog will display.
# models/staging/_staging.yml
version: 2
models:
- name: stg_parcels
columns:
- name: geom
description: "Parcel boundary polygon in the canonical CRS."
data_type: geometry(MultiPolygon, 4326)
Verify the catalog picks up the declared type:
dbt docs generate
# Open target/catalog.json and confirm the geom column shows
# "type": "geometry(MultiPolygon,4326)" rather than a bare "geometry" or "USER-DEFINED".
2. Add machine-readable SRID, geometry type, and precision to meta
description is for humans; meta is for machines. Splitting the SRID, geometry type, and precision into discrete meta keys makes them queryable out of manifest.json later, and keeps the facts from being trapped inside a prose sentence no script can parse.
# models/staging/_staging.yml
columns:
- name: geom
description: "Parcel boundary polygon in the canonical CRS."
data_type: geometry(MultiPolygon, 4326)
meta:
srid: 4326
geometry_type: MultiPolygon
precision: full
source_srid: 2263 # county source was US survey feet
Verify the metadata is attached to the node:
dbt parse
python -c "import json; m=json.load(open('target/manifest.json')); \
print([c['meta'] for c in m['nodes']['model.my_project.stg_parcels']['columns'].values()])"
# Expect the srid, geometry_type, and precision keys in the printed dict.
3. Write reusable doc blocks for CRS and precision conventions
Conventions that apply to many columns — what the canonical CRS means, what a precision tolerance implies — belong in a docs block written once and referenced everywhere. This is the difference between documentation that stays consistent and documentation that drifts the first time one copy is edited.
<!-- models/docs/geometry_docs.md -->
{% docs geom_canonical %}
Stored in **EPSG:4326** (WGS 84, lon/lat degrees). Coordinates are geographic,
not metric — reproject to a projected SRID before measuring distance or area.
{% enddocs %}
{% docs geom_simplified %}
Simplified with `ST_Simplify` for map tiling; coordinates are lossy. Use the
upstream intermediate model for measurement-grade geometry.
{% enddocs %}
Verify the block resolves before wiring it into columns:
dbt parse # fails loudly with "doc block not found" if the {% docs %} name is wrong
4. Reference the doc blocks from column descriptions
Now compose each column’s description from the shared blocks plus one column-specific sentence. The convention lives in one place; only the specifics are local.
# models/marts/_marts.yml
version: 2
models:
- name: mart_service_areas
columns:
- name: geometry
description: "Dissolved service-area boundary. {{ doc('geom_canonical') }} {{ doc('geom_simplified') }}"
data_type: geometry(MultiPolygon, 4326)
meta:
srid: 4326
geometry_type: MultiPolygon
precision: lossy
simplify_tolerance_deg: 0.0001
Verify the rendered description in the catalog contains the block prose:
dbt docs generate && dbt docs serve
# In the browser, open mart_service_areas and confirm the geometry column
# description shows the canonical-CRS and simplified sentences inline.
5. Enforce the documented type with a contract
Documentation that can silently disagree with the data is a liability. Turning on a contract makes dbt compare the declared data_type against the physical column at build time, so a description claiming EPSG:4326 cannot ship on a column that holds something else.
# models/marts/_marts.yml
config:
contract:
enforced: true
Verify the contract catches drift by temporarily changing the model’s output SRID and rebuilding:
dbt run --select mart_service_areas
# With enforced contract, a mismatch between data_type and the produced column
# fails the run with a contract error instead of publishing wrong documentation.
Configuration reference
| Key | Where | Example | Notes |
|---|---|---|---|
data_type |
column | geometry(MultiPolygon, 4326) |
Documents type + SRID; consumed by contracts when enforced: true |
description |
column / model | "{{ doc('geom_canonical') }}" |
Prose; may compose {{ doc() }} blocks and plain text |
meta.srid |
column | 4326 |
Machine-readable SRID; queryable from manifest.json |
meta.geometry_type |
column | MultiPolygon |
Concrete geometry type, not the generic geometry |
meta.precision |
column | full / lossy |
Flags whether coordinates were simplified downstream |
meta.simplify_tolerance_deg |
column | 0.0001 |
Records the ST_Simplify tolerance for lossy columns |
{% docs name %} |
.md under docs-paths |
geom_canonical |
Reusable prose block referenced via {{ doc('name') }} |
contract.enforced |
model config |
true |
Makes data_type a checked contract, not just a label |
Gotchas & edge cases
data_typestring must match the adapter’s rendering. dbt compares your string against the catalog’s reported type; a mismatch in spacing or casing (geometry(MultiPolygon,4326)vsgeometry(MultiPolygon, 4326)) can fail a contract. Generate the catalog once and copy the exact form the adapter emits.metais not enforced. Unlikedata_typeunder a contract,meta.sridis free-text documentation — nothing stops it disagreeing with the data. Back it with anST_SRIDtest, described in detecting SRID mismatches with dbt tests, if the value must be trusted.- Generic geometry columns show as
USER-DEFINED. If a column is typed as baregeometryin the database, the catalog cannot report the subtype or SRID — declare the parameterized type in the model’s SQL (::geometry(MultiPolygon, 4326)) so the catalog has something concrete to display. - Doc blocks are project-global.
{% docs geom_canonical %}shares one namespace across the whole project; a duplicate name in two.mdfiles raises a compilation error. Prefix block names by domain (geom_,crs_) to avoid collisions. {{ doc() }}only renders indescription. It does not interpolate insidemetavalues — keep machine-readable metadata as literal values, not doc references.
FAQ
What is the difference between data_type and meta for documenting SRID?
data_type like geometry(MultiPolygon, 4326) is a typed declaration the catalog displays and a contract can enforce against the real column, so it cannot silently drift. meta.srid is free-form structured metadata — convenient to query out of manifest.json but not checked against the data. Use data_type for the enforceable truth and meta for the machine-readable annotations you script against, and back meta with an ST_SRID test if it must be trusted.
How do I document that a geometry column was simplified and is lossy?
Record it in two places: a human sentence in description (ideally via a shared {{ doc() }} block that says the coordinates are lossy and points to the full-precision upstream model) and a machine-readable pair in meta, such as precision: lossy and simplify_tolerance_deg: 0.0001. That way both a person reading the catalog and a script walking the manifest can tell the column is for presentation, not measurement.
Can I reuse the same CRS note across many models?
Yes — that is exactly what docs blocks are for. Write the note once as {% docs geom_canonical %}...{% enddocs %} in a .md file under docs-paths, then reference it from every column description with {{ doc('geom_canonical') }}. Editing the block updates every model that references it, so the convention stays consistent instead of drifting across pasted copies.
Why does my geometry column show as USER-DEFINED in the catalog?
The warehouse is reporting a bare geometry type with no declared subtype or SRID, so dbt has nothing concrete to catalog. Cast to a parameterized type in the model SQL, for example geom::geometry(MultiPolygon, 4326), and declare the matching data_type in YAML. After the next dbt docs generate the catalog shows the concrete geometry type and SRID.
Does documenting data_type enforce anything on its own?
Not by itself. data_type is documentation until you set contract.enforced: true on the model, at which point dbt compares the declared type against the produced column at build time and fails on a mismatch. Without the contract flag it is a label that can drift from reality — turn the contract on for geometry columns whose SRID and type must be guaranteed.
Related
- Spatial Data Lineage Documentation — how column docs roll up into pipeline-wide lineage and exposures.
- Enforcing a Canonical SRID Across dbt Models — turn the documented SRID into an enforced contract.
- Versioning Spatial Schemas in dbt — track breaking changes to documented geometry columns over time.
Up: Part of Spatial Data Lineage Documentation.