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
Triage of five STAC validation errors and the fix for each.
All five are shape errors; the semantic ones are underneath.

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.

Two panels separating what a STAC validator checks from what it cannot check.
Everything on the right validates cleanly and breaks a catalogue.

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.

Two panels showing the same wrongly typed proj:epsg field validating when the extension is undeclared and being rejected when it is declared.
Declaring the extension is what turns a free-form property into a checked one.

Edge cases or notes

  • Validation is a network call. Cache schemas for CI and offline work.
  • type must be Feature. Items are GeoJSON Features; collections are not.
  • stac_version must 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:30Z is 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.

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.