A STAC item is rejected by the validator
Problem statement
pystac.validation.validate(item) raises, the message is a JSON Schema error several levels deep, and the offending value is at the bottom of a wall of schema text. The errors are almost always one of five things, and each has a one-line fix.
The more dangerous problem is the opposite: items that validate and are still wrong. A STAC validator is a JSON Schema check, so it enforces shape and not meaning. A bounding box whose west is east of its east passes validation and never matches a search.
This guide reads the real error messages, fixes each cause, and then covers what validation cannot see.
Quick answer
import pystac
try:
pystac.validation.validate(item)
except Exception as exc:
text = str(exc)
print(text.splitlines()[0]) # which schema failed
print(text.split("On instance")[-1]) # the offending value
The five failures and their fixes:
| message fragment | cause | fix |
|---|---|---|
does not match '(\+00:00|Z)$' |
a date without a time zone | RFC 3339 with Z |
'datetime' is a required property |
no datetime and no range | set it, or both range properties |
is too short on bbox |
3 or 5 numbers | four (or six for 3D) |
is not of type 'integer', 'null' |
an extension field of the wrong type | correct the type |
'โฆ' is not valid under any of the given schemas |
wrong item shape | check type is Feature |
Step-by-step solution
1. Read the first and last lines of the error
The first line names the schema that failed โ the core item schema or a specific extension. The last block, after On instance, is the value that failed. Everything between is the schema path.
2. Fix the datetime
STAC requires RFC 3339 with an explicit UTC designator. A bare date produces:
'2026-03-14' does not match '(\\+00:00|Z)$'
Failed validating 'pattern' in schema[โฆ]['properties']['datetime']:
{'title': 'Date and Time',
'format': 'date-time',
'pattern': '(\\+00:00|Z)$'}
On instance['properties']['datetime']:
'2026-03-14'
Use datetime.datetime(2026, 3, 14, tzinfo=datetime.timezone.utc) and let pystac serialise it, or write "2026-03-14T00:00:00Z".
3. Fix a missing datetime
Removing datetime without adding the range properties gives 'datetime' is a required property. For a composite:
item = pystac.Item(..., datetime=None, properties={
"start_datetime": "2026-04-02T00:00:00Z",
"end_datetime": "2026-04-27T23:59:59Z",
})
Both are required when datetime is null.
4. Fix the bbox length
bbox must have four numbers for 2D or six for 3D, in [west, south, east, north] order. A three-element array is rejected outright.
5. Fix extension field types
An extension field with the wrong type fails only when the extension is declared:
'EPSG:4326' is not of type 'integer', 'null'
Failed validating 'type' in schema[0][โฆ]['properties']['proj:epsg']:
{'title': 'EPSG code', 'type': ['integer', 'null']}
proj:epsg is an integer. The same error shape appears for every extension field.
6. Declare the extensions you use
An undeclared extension field is unvalidated, not invalid. Adding proj:epsg to properties without listing the projection extension in stac_extensions passes validation and is silently unchecked โ so a typo or a wrong type survives to publication. Use the pystac extension helpers, which add the schema URL for you.
7. Cache the schemas for CI
Validation fetches schemas over HTTPS. In CI that is a network dependency and a rate limit; pystac supports a local schema cache, and using one makes the check deterministic.
8. Then run the checks the validator does not
Bounding box order, geometry against the data, asset hrefs, licence, media types.
Code examples
Example 1 โ a readable error report
import pystac, json
def explain(item_dict):
try:
pystac.validation.validate_dict(item_dict)
return {"valid": True}
except Exception as exc:
text = str(exc)
head = text.splitlines()[0]
instance = text.split("On instance")[-1].strip() if "On instance" in text else ""
prop = None
if "['properties']['" in text:
prop = text.split("['properties']['")[-1].split("']")[0]
return {"valid": False, "schema": head, "property": prop,
"value": instance.splitlines()[-1].strip() if instance else None}
for mutate, label in [
(lambda d: d["properties"].__setitem__("datetime", "2026-03-14"), "bare date"),
(lambda d: d["properties"].pop("datetime"), "no datetime"),
(lambda d: d.__setitem__("bbox", d["bbox"][:3]), "three-element bbox"),
]:
d = json.loads(json.dumps(base_item))
mutate(d)
print(f"{label:20}", explain(d))
Running the mutations against your own item template is the fastest way to learn which messages mean what.
Example 2 โ build the item with the extension helpers
import pystac
from pystac.extensions.projection import ProjectionExtension
proj = ProjectionExtension.ext(item, add_if_missing=True)
proj.epsg = 27700 # int, and the extension URL is added for you
proj.shape = [height, width]
proj.transform = list(transform)[:6]
print(item.stac_extensions)
add_if_missing=True is the flag that stops the silent-unvalidated case: the schema URL goes into stac_extensions, so the field is checked from then on.
Example 3 โ the semantic checks, as a test
import pytest, requests
from shapely.geometry import shape, box
def test_item_is_sane(item):
d = item.to_dict()
w, s, e, n = d["bbox"]
assert w <= e, f"bbox west {w} is east of east {e}"
assert s <= n, f"bbox south {s} is north of north {n}"
assert -180 <= w <= 180 and -90 <= s <= 90, "bbox is not in geographic coordinates"
assert shape(d["geometry"]).intersects(box(w, s, e, n)), "geometry does not match bbox"
assert d["properties"].get("license"), "no licence declared"
for key, asset in d["assets"].items():
assert asset.get("type"), f"asset {key} has no media type"
if asset["href"].startswith("http"):
assert requests.head(asset["href"], timeout=20,
allow_redirects=True).status_code < 400
The first assertion is the one that earns its keep. A reversed bbox validates, publishes, and quietly never matches a spatial search.
Explanation
Why the errors are so verbose
pystac surfaces the underlying jsonschema error, which includes the failing sub-schema and the path through the document. That is genuinely useful once you know to read the first line and the On instance block and skip the middle.
Why an undeclared extension field is worse than a rejected one
stac_extensions is the list of schemas the validator applies. The core item schema permits additional properties, so an undeclared proj:epsg is simply an unknown key: never checked, and potentially ignored by clients that look for declared extensions. A validation error is a fixed bug; an unvalidated field is a bug that ships.
Why the bbox order cannot be validated
JSON Schema describes types and shapes, not relationships between array elements. [10, 50, 5, 55] is four numbers, so it satisfies the schema. The ordering constraint is in the specification's prose, which no validator reads.
Why validation belongs in CI along with the semantic checks
An item is published once and read for years. The five shape errors are caught by the validator; the semantic errors are caught only by assertions you write. Running both on every item before publication is a few seconds and removes the class of problem entirely.
Edge cases or notes
- Validation is a network call. Cache schemas for CI and offline work.
typemust beFeature. Items are GeoJSON Features; collections are not.stac_versionmust be a released version. A typo fails against no schema at all.- Antimeridian items need the two-bbox convention; a wrapping bbox looks reversed.
- Null geometry is allowed with a bbox in some profiles; check your catalogue's rules.
- Datetimes need seconds.
2026-03-14T10:30Zis not RFC 3339. - Collections validate too. Their extents and summaries have their own schema.
- Extension versions matter. The URL pins a version; upgrading changes the rules.
Internal links
- How to build a STAC item for your own raster โ building one that passes both checks
- STAC catalogues explained โ items, collections and links
- Metadata standards compared: ISO 19115, STAC and Frictionless โ when STAC is the right shape
- Metadata extents and dates do not match the data โ the semantic errors in more depth
- How to download satellite imagery from a STAC catalogue in Python โ what a client does with the item
- How to read a COG from a URL in Python โ why the media type matters
- How to test a spatial API with pytest โ where these assertions live
- How to standardise dates in spatial data with Python โ producing RFC 3339 reliably
FAQ
Why does my STAC datetime fail validation?
STAC requires RFC 3339 with an explicit UTC designator. A bare date fails with '2026-03-14' does not match '(\+00:00|Z)$'. Use 2026-03-14T00:00:00Z.
Can a STAC item have no datetime?
Only if start_datetime and end_datetime are both present. Otherwise the core schema reports 'datetime' is a required property.
Why is my extension field not being validated?
Because the extension is not declared in stac_extensions. Undeclared fields are unknown properties, which the core schema permits and never checks.
What does is not of type 'integer', 'null' mean?
An extension field has the wrong type โ most often proj:epsg given as "EPSG:4326" instead of 4326.
Does the validator check my bounding box order?
No. JSON Schema cannot express relationships between array elements, so a reversed bbox validates and then never matches a search. Assert it yourself.
How do I validate without network access?
Use a local schema cache. pystac supports one, and it also makes CI runs deterministic and immune to rate limits.