OGC API - Records: PySTAC Item subclass

An OGC API Records GeoJSON model with PySTAC Item identity, link/asset APIs, and access to existing Item-oriented extension factories.

Example

from cwl2ogcrecords.ogc_record import OGCRecord, OrganizationContact
from pystac.extensions.eo import EOExtension
from pystac.extensions.projection import ProjectionExtension
import pystac

record = OGCRecord(
    id='elevation-record',
    geometry=None,
    properties={'type': 'dataset'},
    time={'interval': ['2020-01-01', '..']},
)
record.title = 'Elevation dataset'
record.description = 'An example catalog record'
record.keywords = ['elevation']
record.contacts = [OrganizationContact(organization='Example organization')]
record.properties['my-extension:quality'] = 'reviewed'

ProjectionExtension.ext(record, add_if_missing=True).code = 'EPSG:4326'
# EO fields are appropriate only when the described resource has EO semantics.
EOExtension.ext(record, add_if_missing=True).cloud_cover = 5.0
record.add_asset('data', pystac.Asset('https://example.org/elevation.tif'))
record.add_link(pystac.Link('license', 'https://example.org/license'))

payload = record.to_dict()  # OGC representation; no invented stac_version/date
restored = OGCRecord.from_dict(payload)
assert EOExtension.ext(restored).cloud_cover == 5.0

record.set_self_href('/tmp/record.json')
record.save_object()
restored = OGCRecord.from_file('/tmp/record.json')

See typed metadata for all nested models and construction examples.

Contract and defaults

Source schemas:

  • https://schemas.opengis.net/ogcapi/records/part1/1.0/openapi/schemas/recordGeoJSON.yaml
  • https://schemas.opengis.net/ogcapi/records/part1/1.0/openapi/schemas/recordCommonProperties.yaml
  • https://schemas.opengis.net/ogcapi/records/part1/1.0/openapi/schemas/time.yaml

The root schema requires id, type, geometry, and properties. It does not supply application defaults for title, contacts, dates, or other metadata. This implementation emits type: Feature, defaults geometry to null, and requires an identifier. Omitting properties produces null; pass {} for an empty object. Internally properties is always a dictionary so extension accessors work; a parsed null is preserved on export until properties are populated. Links default to an empty list. No title, date, or conformance claim is fabricated.

OGCRecord provides direct live accessors for all fields currently listed by recordCommonProperties: created, updated, type, title, description, keywords, themes, language, languages, resourceLanguages, externalIds, formats, contacts, license, and rights. Python names use underscores for compound names. The older record_metadata view remains available and accesses the same dictionary. record.type maps to properties.type; the top-level GeoJSON type stays Feature. Dates remain JSON strings; nested values remain dictionaries/lists. Optional accessor assignment of None removes a property. Unknown properties are retained.

time, conforms_to, and link_templates map to their OGC top-level fields. Reading the two list accessors creates an empty list if missing, allowing append. To preserve an explicitly null time, set record.time = None; to omit it, remove record.extra_fields['time']. from_dict preserves missing versus null time.

OGC integer identifiers survive export through record_id; the inherited id is their string form for PySTAC utilities. Changing id changes the exported identifier (to a string). Inputs with boolean identifiers are rejected.

Extension compatibility

The class really inherits Item: isinstance(record, pystac.Item) is true. It retains properties, assets, asset ownership, links, collection_id, datetime, stac_extensions, common_metadata and the inherited extension accessor. No fixed extension allowlist is used. Existing and future Item-oriented factories can therefore accept it, subject to their own requirements.

This is not a guarantee for every extension. Extensions may require valid geometry, an actual temporal extent, assets, or other STAC-specific semantics. An extension may also change its implementation or serialize internally. Review new extensions against your Records and pin/test PySTAC upgrades.

Extension declarations remain in stac_extensions, and assets remain in assets, as foreign members in the OGC representation. They are never silently removed or renamed to conformsTo. Keep conformance identifiers separate and only declare conformance you actually implement. Unknown top-level fields are preserved through extra_fields (managed fields cannot be supplied there).

Temporal model and serialization

to_dict() and to_record_dict() serialize the OGC representation; inherited save_object() and __geo_interface__ therefore use that representation too. from_dict() accepts Records without STAC version or STAC datetime. Its migrate argument is accepted for PySTAC signature compatibility but does not invoke STAC migrations. It always copies input metadata; preserve_dict=False is only a hint.

The constructor initializes STACObject directly, then initializes Item's required attributes. It intentionally does not call Item.init, avoiding the mandatory STAC datetime/interval check without synthetic dates. This is the principal maintenance seam and the reason for the PySTAC version constraint.

OGC time is independent of STAC datetime / start_datetime / end_datetime. Date-only and open OGC intervals cannot be losslessly converted automatically. Explicit STAC datetime constructor arguments are preserved in properties. Do not assume that changing OGC time will update STAC time, or vice versa.

to_stac_item() creates a separate ordinary Item only when genuine STAC datetime or start/end values exist. It preserves Record foreign fields and extensions. It does not guarantee full STAC validity: geometry/bbox and extension constraints still need validation via the returned Item's validate().

Validation and loading boundaries

This initial implementation performs basic structural recognition, not complete validation of geometry, nested metadata, times or extension schemas. The supplied OGC schemas use OpenAPI 3.0 constructs such as nullable and external references; do not feed them blindly to a Draft 2020-12 JSON Schema validator. In particular, the object/oneOf branches in the published root schema deserve review with your chosen validator because their alternatives can overlap.

record.validate(validator=...) accepts an explicitly configured validator with a validate(document) method. Resolve the OGC schema references and dialect before supplying it. Calling validate() without one raises an actionable error instead of incorrectly running STAC validation. The returned schema URI identifies the intended contract; configuring the validator correctly is the caller's duty. No complete OGC or extension-schema validation is claimed by the tests.

Use OGCRecord.from_file and OGCRecord.from_dict. Global pystac.read_file, automatic link resolution, STAC catalog traversal, and pystac-client do not register this class automatically. Root/parent/collection traversal still assumes STAC catalogs. Generic GeoJSON and STAC Features overlap structurally; the matches_object_type method is not a unique Record discriminator.