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_type of geometry(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_type you 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 the data_type.
  • A spatial adapter that reports geometry types to the catalog — dbt-postgres on PostGIS 3.x, or dbt-duckdb with 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-paths configured so the project globs your documentation .md files:
yaml
# 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.

yaml
# 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:

bash
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.

yaml
# 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:

bash
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.

markdown
<!-- 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:

bash
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.

yaml
# 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:

bash
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.

yaml
# models/marts/_marts.yml
    config:
      contract:
        enforced: true

Verify the contract catches drift by temporarily changing the model’s output SRID and rebuilding:

bash
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_type string 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) vs geometry(MultiPolygon, 4326)) can fail a contract. Generate the catalog once and copy the exact form the adapter emits.
  • meta is not enforced. Unlike data_type under a contract, meta.srid is free-text documentation — nothing stops it disagreeing with the data. Back it with an ST_SRID test, 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 bare geometry in 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 .md files raises a compilation error. Prefix block names by domain (geom_, crs_) to avoid collisions.
  • {{ doc() }} only renders in description. It does not interpolate inside meta values — 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.

Up: Part of Spatial Data Lineage Documentation.