Metadata standards compared: ISO 19115, STAC and Frictionless
Problem statement
Four standards want to describe your dataset, and they disagree about almost everything except that it has a bounding box. ISO 19115 is what national spatial data infrastructures require, STAC is what imagery catalogues and cloud-native workflows speak, DCAT is what open-data portals index, and Frictionless is what data engineers reach for when they want something readable.
Choosing badly is expensive in a specific way: you write a complete record in the wrong shape, and then discover that the catalogue you need to appear in cannot read it. Choosing at all is the mistake โ the right move is to keep one internal record and generate whichever shapes you need.
This guide compares the four on what they express well, what they cost to produce, and which one to author in.
Quick answer
Author an internal record, emit the standards:
INTERNAL = {
"id": "east-sussex-flood-zones",
"title": "Coastal flood risk zones, East Sussex",
"description": "Modelled 1-in-200-year extent at 2 m resolution.",
"bbox": [-0.42, 50.72, 0.38, 51.02],
"crs": "EPSG:4326",
"datetime": "2025-11-01T00:00:00Z",
"licence": "OGL-UK-3.0",
"keywords": ["flood", "coastal", "risk"],
"lineage": ["EA LiDAR DTM 2024", "pysheds D8", "200-year threshold"],
"assets": {"data": {"href": "flood_zones.gpkg", "type": "application/geopackage+sqlite3"}},
"contact": "[email protected]",
}
to_stac(INTERNAL) # for a catalogue or a cloud-native pipeline
to_datapackage(INTERNAL) # for a data engineer
to_iso19115(INTERNAL) # for a national SDI, when required
Every one of the four standards is a projection of roughly the same facts. The internal record is where the facts live.
Step-by-step solution
1. Identify the consumer before the standard
The question is never "which standard is best". It is "who has to read this?" โ a national geoportal (ISO 19139 XML), an imagery catalogue or a cloud pipeline (STAC), a government open-data portal (DCAT), a colleague loading it with pandas (Frictionless), or a person (a README generated from the same record).
2. Know what each is actually good at
- ISO 19115 / 19139 โ exhaustive, with real slots for lineage, quality, spatial representation and responsible parties. Verbose XML, and in practice most published records fill in ten of its hundreds of elements.
- STAC โ built for spatio-temporal assets. Excellent at item-level search by time and footprint, at assets with roles and media types, and at extensions. Weak on lineage and rights beyond a licence string.
- DCAT โ a catalogue vocabulary, not a spatial one. Good for discovery and for linking distributions; GeoDCAT-AP adds the spatial parts.
- Frictionless Data Package โ a JSON descriptor with a table schema. Genuinely good at field-level metadata: types, constraints, units, descriptions. No spatial concepts of its own.
3. Put field-level metadata somewhere, whichever you choose
The single most useful thing missing from most spatial metadata is a description and a unit per column. STAC has no native slot for it; ISO has one nobody fills in; Frictionless is built around it. Whatever you publish, keep a table schema.
4. Use STAC if the data is tiled, dated or cloud-hosted
STAC's value is that a client can search by footprint and time and then read only the asset it needs. If you are publishing a series of rasters, or anything a pipeline will discover programmatically, STAC pays for itself. How to build a STAC item for your own raster covers the mechanics.
5. Use ISO only when something requires it
ISO 19139 records are expensive to author and almost never read by humans. Generate them from the internal record when a portal demands one, and do not treat the XML as the master copy.
6. Validate whatever you emit
Every standard has a validator, and each catches different things. Be aware of what they do not catch: a STAC validator will reject a three-element bbox and a non-RFC3339 datetime, and will happily accept a bounding box whose west is east of its east.
7. Keep the internal record in version control
It is a small text file that changes with the data and is reviewed with the code. That is where metadata quality actually comes from โ not from the standard.
Code examples
Example 1 โ emit a STAC item from the internal record
import datetime, pystac
def to_stac(rec):
w, s, e, n = rec["bbox"]
item = pystac.Item(
id=rec["id"],
geometry={"type": "Polygon",
"coordinates": [[[w, s], [w, n], [e, n], [e, s], [w, s]]]},
bbox=rec["bbox"],
datetime=datetime.datetime.fromisoformat(rec["datetime"].replace("Z", "+00:00")),
properties={"title": rec["title"], "description": rec["description"],
"license": rec["licence"], "keywords": rec["keywords"]},
)
for role, a in rec["assets"].items():
item.add_asset(role, pystac.Asset(href=a["href"], media_type=a["type"], roles=[role]))
return item
pystac.validation.validate(to_stac(INTERNAL))
Example 2 โ emit a Frictionless data package with a field schema
import json, geopandas as gpd
TYPE_MAP = {"int64": "integer", "float64": "number", "object": "string",
"string": "string", "bool": "boolean", "datetime64[ms]": "datetime"}
def to_datapackage(rec, gdf, units=None, descriptions=None):
units, descriptions = units or {}, descriptions or {}
fields = [{"name": c,
"type": TYPE_MAP.get(str(t), "any"),
"description": descriptions.get(c, ""),
"unit": units.get(c)}
for c, t in gdf.dtypes.items() if c != gdf.geometry.name]
return {
"name": rec["id"], "title": rec["title"], "description": rec["description"],
"licenses": [{"name": rec["licence"]}],
"spatial": {"bbox": rec["bbox"], "crs": rec["crs"]},
"resources": [{"name": rec["id"], "path": a["href"], "format": "gpkg",
"schema": {"fields": fields}}
for a in rec["assets"].values()],
}
The unit key is not part of the Frictionless spec's core, and it is the field that saves the most time downstream. Put it there anyway.
Example 3 โ what the standards have in common
| Concept | ISO 19115 | STAC | DCAT | Frictionless |
|---|---|---|---|---|
| title | MD_Identification/citation/title |
properties.title |
dct:title |
title |
| description | abstract |
properties.description |
dct:description |
description |
| extent | EX_GeographicBoundingBox |
bbox + geometry |
dct:spatial |
(none) |
| time | EX_TemporalExtent |
properties.datetime |
dct:temporal |
(none) |
| licence | MD_LegalConstraints |
properties.license |
dct:license |
licenses[] |
| lineage | LI_Lineage |
(extension) | prov:wasDerivedFrom |
(none) |
| field schema | MD_FeatureCatalogue |
(none) | (none) | schema.fields[] |
| assets | MD_DigitalTransferOptions |
assets{} |
dcat:distribution |
resources[] |
The two gaps worth noticing: STAC has no native lineage, and only Frictionless has a real place for per-field metadata.
Explanation
Why STAC won for imagery and not for everything
STAC is optimised for one question โ which assets cover this footprint and time โ and it answers it extremely well, over static JSON on object storage with no server. That is exactly the shape of a satellite archive. It is less suited to a single vector dataset with a complicated processing history, where there is no time series to search and the interesting metadata is the lineage STAC does not model.
Why ISO records are usually empty
ISO 19115 has hundreds of elements, most optional, and the profile a portal enforces typically requires a dozen. The result is records that validate and say nothing. That is a fact about how the standard is used rather than a flaw in it, but it means an ISO record is rarely a source of truth about a dataset.
Why validators are necessary and insufficient
A JSON Schema check finds shape errors โ a missing required property, a wrong type, a malformed datetime. It cannot find a bounding box in the wrong order, a licence that does not match what the data actually is, or a lineage that describes a different pipeline. Validation is a floor.
Why one internal record beats authoring in a standard
Standards change, and the set you must publish in changes faster. A small internal record in version control that emits STAC today and DCAT next year costs one function per target; a directory of hand-written XML costs a rewrite.
Edge cases or notes
- STAC collections describe the series, items describe the assets. Do not put per-scene facts in the collection.
- GeoDCAT-AP exists for EU portals and maps DCAT onto INSPIRE requirements.
- INSPIRE has its own profile of ISO. Meeting ISO is not meeting INSPIRE.
- Frictionless has no CRS concept. Add one anyway; consumers need it.
- Keywords should come from a controlled vocabulary if a portal indexes them.
- A
datetimeof null needs a range. STAC requiresstart_datetimeandend_datetimeinstead. - Media types matter in STAC. The COG media type is what tells a client it can range-request.
- Do not hand-edit generated records. The edit is lost on the next run.
Internal links
- Spatial metadata explained: what a dataset must tell you โ the fields all four standards express
- How to build a STAC item for your own raster โ the STAC path in detail
- A STAC item is rejected by the validator โ what validation does and does not catch
- How to write a metadata record for a dataset in Python โ the internal record
- STAC catalogues explained โ how the items are organised
- Provenance and lineage explained for spatial workflows โ the gap in STAC
- Open data licences explained for spatial data โ the licence field in each standard
- Metadata extents and dates do not match the data โ the errors no validator catches
FAQ
Which metadata standard should I use for spatial data?
The one your consumer reads: STAC for imagery and cloud-native pipelines, ISO 19115 for national spatial data infrastructures, DCAT for open-data portals, Frictionless for tabular clarity. Author an internal record and emit whichever you need.
Is STAC a replacement for ISO 19115?
No. STAC models spatio-temporal assets and search; ISO models the full documentation of a dataset including lineage and quality. They overlap on about eight fields.
Where do I put per-column descriptions and units?
Frictionless has a native place for them; STAC and ISO effectively do not. Keep a field schema whatever else you publish โ it is the metadata users miss most.
Does STAC handle vector data?
It can: an item can point at a GeoPackage or GeoParquet asset. It just adds less value when there is no time series or tiling to search across.
What does a STAC validator actually check?
JSON Schema conformance โ required properties, types, the RFC 3339 datetime pattern, bbox length. It will not catch a reversed bounding box or a wrong licence.
Can I publish more than one standard for the same dataset?
Yes, and you usually should. Generate them all from one internal record so they cannot drift apart.