Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)

### Changed

### Fixed
### Fixed

### Removed

## [v4.5.2] - 2026-08-05

### Fixed

- Fixed timeout issue when validating STAC Collections with many extensions by implementing three-tier base schema compilation strategy: standard patching → aggressive patching → cached jsonschema fallback with proper RefResolver. [#307](https://github.com/stac-utils/stac-validator/pull/307)

## [v4.5.1] - 2026-07-30

### Changed
Expand Down Expand Up @@ -484,7 +490,8 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
- With the newest version - 1.0.0-beta.2 - items will run through jsonchema validation before the PySTAC validation. The reason for this is that jsonschema will give more informative error messages. This should be addressed better in the future. This is not the case with the --recursive option as time can be a concern here with larger collections.
- Logging. Various additions were made here depending on the options selected. This was done to help assist people to update their STAC collections.

[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v4.5.1..main
[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v4.5.2..main
[v4.5.2]: https://github.com/sparkgeo/stac-validator/compare/v4.5.1..v4.5.2
[v4.5.1]: https://github.com/sparkgeo/stac-validator/compare/v4.5.0..v4.5.1
[v4.5.0]: https://github.com/sparkgeo/stac-validator/compare/v4.4.0..v4.5.0
[v4.4.0]: https://github.com/sparkgeo/stac-validator/compare/v4.3.0..v4.4.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "stac_valid"
version = "4.5.1"
version = "4.5.2"
description = "A package to validate STAC files"
authors = [
{name = "Jonathan Healy", email = "jon@healy-hyperspatial.dev"},
Expand Down
51 changes: 40 additions & 11 deletions stac_validator/fast_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,52 @@ def get_validator(stac_type: str, stac_version: str, extensions: List[str]):
else:
raise ValueError(f"Unknown STAC type for validation: {stac_type}")

# Fetch and compile the Base Schema directly
base_schema = fetch_schema(base_uri)
# Fetch the raw Base Schema directly
raw_base_schema = fetch_schema(base_uri)

try:
# Try to compile with fastjsonschema first
# Tier 1: Try to compile with standard patching first
optimized_base = optimize_schema_for_compiler(raw_base_schema)
base_validator = fastjsonschema.compile(
base_schema, handlers={"http": fetch_schema, "https": fetch_schema}
optimized_base, handlers={"http": fetch_schema, "https": fetch_schema}
)
logger.debug(
f"Base schema {stac_type} {stac_version} compiled with fastjsonschema"
)
except Exception:
# If base schema fails to compile, use jsonschema validator instead
import jsonschema
try:
# Tier 2: If it fails, try aggressive patching (stripping allOf/oneOf/anyOf)
optimized_base = optimize_schema_for_compiler(
raw_base_schema, remove_allof=True
)
base_validator = fastjsonschema.compile(
optimized_base, handlers={"http": fetch_schema, "https": fetch_schema}
)
logger.debug(
f"Base schema {stac_type} {stac_version} compiled with fastjsonschema (aggressive patching)"
)
except Exception:
# Tier 3: THE ULTIMATE FALLBACK
# If fastjsonschema completely chokes, instantiate a cached jsonschema validator
# that is strictly wired to use our high-speed fetch_schema session.
import jsonschema

resolver = jsonschema.RefResolver(
base_uri=base_uri,
referrer=raw_base_schema,
handlers={"http": fetch_schema, "https": fetch_schema},
)
ValidatorClass = jsonschema.validators.validator_for(raw_base_schema)

def base_validator(data):
jsonschema.validate(data, base_schema)
# Pre-compile the jsonschema object ONCE, outside the execution loop
js_validator = ValidatorClass(raw_base_schema, resolver=resolver)

logger.debug(
f"Base schema {stac_type} {stac_version} compiled with jsonschema fallback"
)
def base_validator(data):
js_validator.validate(data)

logger.debug(
f"Base schema {stac_type} {stac_version} compiled with cached jsonschema fallback"
)

ext_validators = []
skipped_extensions = []
Expand Down
90 changes: 90 additions & 0 deletions tests/test_heavy_extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
Test validation of STAC Items and Collections with heavy extension loads.
This ensures the three-tier fallback strategy (fastjsonschema -> aggressive patching -> jsonschema) works correctly.
"""


class TestHeavyExtensions:
"""Test validation with many extensions to stress the compiler."""

def test_item_with_15_extensions(self, tmp_path):
"""Test that a STAC Item with 15 extensions compiles without hanging."""
from stac_validator.fast_validator import get_validator

# Test that get_validator can compile 15 extensions without hanging
extensions = [
"https://stac-extensions.github.io/eo/v2.0.0/schema.json",
"https://stac-extensions.github.io/projection/v2.0.0/schema.json",
"https://stac-extensions.github.io/view/v1.1.0/schema.json",
"https://stac-extensions.github.io/raster/v2.0.0/schema.json",
"https://stac-extensions.github.io/classification/v2.0.0/schema.json",
"https://stac-extensions.github.io/processing/v1.2.0/schema.json",
"https://stac-extensions.github.io/sat/v1.1.0/schema.json",
"https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json",
"https://stac-extensions.github.io/authentication/v1.1.0/schema.json",
"https://stac-extensions.github.io/grid/v1.1.0/schema.json",
"https://stac-extensions.github.io/timestamps/v1.1.0/schema.json",
"https://stac-extensions.github.io/product/v1.0.0/schema.json",
"https://stac-extensions.github.io/file/v2.1.0/schema.json",
"https://stac-extensions.github.io/storage/v2.0.0/schema.json",
"https://stac-extensions.github.io/scientific/v1.0.0/schema.json",
]

# Should compile all 15 extensions without hanging or errors
validator, cached = get_validator("Item", "1.0.0", extensions)
assert validator is not None
assert callable(validator)

def test_collection_with_12_extensions(self, tmp_path):
"""Test that a STAC Collection with 12 extensions compiles without hanging."""
from stac_validator.fast_validator import get_validator

# Test that get_validator can compile 12 extensions for a collection without hanging
extensions = [
"https://stac-extensions.github.io/eo/v2.0.0/schema.json",
"https://stac-extensions.github.io/authentication/v1.1.0/schema.json",
"https://stac-extensions.github.io/projection/v2.0.0/schema.json",
"https://stac-extensions.github.io/processing/v1.2.0/schema.json",
"https://stac-extensions.github.io/product/v0.1.0/schema.json",
"https://stac-extensions.github.io/scientific/v1.0.0/schema.json",
"https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json",
"https://stac-extensions.github.io/raster/v2.0.0/schema.json",
"https://stac-extensions.github.io/sat/v1.1.0/schema.json",
"https://stac-extensions.github.io/classification/v2.0.0/schema.json",
"https://stac-extensions.github.io/ceos-ard/v0.2.0/schema.json",
"https://stac-extensions.github.io/storage/v2.0.0/schema.json",
]

# Should compile all 12 extensions without hanging or errors
validator, cached = get_validator("Collection", "1.1.0", extensions)
assert validator is not None
assert callable(validator)

def test_compilation_performance(self):
"""Test that compilation is fast and caching works."""
import time

from stac_validator.fast_validator import get_validator

extensions = [
"https://stac-extensions.github.io/eo/v2.0.0/schema.json",
"https://stac-extensions.github.io/projection/v2.0.0/schema.json",
"https://stac-extensions.github.io/view/v1.1.0/schema.json",
]

# First compilation (cache miss)
start = time.time()
validator1, cached1 = get_validator("Item", "1.0.0", extensions)
first_time = time.time() - start
assert cached1 is False, "First call should be a cache miss"

# Second compilation (cache hit)
start = time.time()
validator2, cached2 = get_validator("Item", "1.0.0", extensions)
second_time = time.time() - start
assert cached2 is True, "Second call should be a cache hit"

# Second compilation should be significantly faster (at least 10x)
assert (
second_time < first_time / 10
), f"Cache not working: {first_time:.3f}s -> {second_time:.3f}s"