oscal-bindings (Python)
Typed Python data bindings for OSCAL (Open Security Controls Assessment Language) v1.2.3, generated from the official NIST JSON Schema using datamodel-code-generator (Pydantic v2).
Installation
pip install oscal-bindings
# or
uv add oscal-bindings
The distribution is oscal-bindings; the import package is oscal_bindings.
To install from a local checkout (e.g., as a uv workspace path dependency):
uv add ../oscal-bindings-python
# or
pip install -e ../oscal-bindings-python
To install directly from a Git tag, branch, or commit:
pip install "git+https://github.com/cairn-proofs/oscal-bindings-python.git@main"
Known issue: Invalid OSCAL JSON keys can be silently lost
Verified on pydantic 2.13.5. What happens:
parse_oscal,parse_oscal_file, the typedparse_*functions, andmodel_validate_jsonaccept the document with the field unset.validate_oscalreturnsTrue, so it can't catch the mistake.- A missing required field is reported only as
missingat its OSCAL name, which hides the real cause. - If both spellings are present, the OSCAL-name value wins and the other key is ignored.
doc = parse_oscal('{"catalog": {..., "back_matter": {"resources": [...]}}}')
doc.root.catalog.back_matter # None: the back-matter is gone
You're most likely to hit this with hand-written JSON, or with output from model_dump(by_alias=False) that's fed back in. Unknown keys (bogus, back-matterr) are still rejected, and dict input is rejected on every supported pydantic version.
Workaround: if your input might not come from a conforming OSCAL tool, validate it as a dict. Dict validation rejects field-name keys with extra_forbidden:
import json
from oscal_bindings import CatalogDocument, OscalDocument
CatalogDocument.model_validate(json.loads(text)) # known type: clearest errors
OscalDocument.model_validate(json.loads(text)) # any of the 8 document types
This raises pydantic.ValidationError, not OscalParseError. Pinning pydantic<2.13 probably avoids the bug too, but that hasn't been verified.
This is an upstream pydantic-core bug in its new 2.13 JSON validation path. The fix is pydantic/pydantic#13275, which hasn't merged yet. Once a fixed release ships, this library will raise its pydantic floor to that release.
Import paths
The bindings live under a namespace scoped to the OSCAL major version:
from oscal_bindings.v1 import parse_oscal, OscalDoc, Catalog
The top-level package re-exports the current major version, so from oscal_bindings import parse_oscal (and oscal_bindings.models, oscal_bindings.parser, oscal_bindings.extensions) resolves to the same objects:
import oscal_bindings
import oscal_bindings.v1
oscal_bindings.Catalog is oscal_bindings.v1.models.Catalog # True
Examples below use the shorter top-level path. Importing from oscal_bindings.v1 is equivalent and states which OSCAL major version your code is written against.
OSCAL is backward-compatible within a major version, so a routine 1.x schema refresh regenerates v1 in place and never changes your imports. Only OSCAL 2.0 would introduce a second namespace.
Which OSCAL release?
__oscal_schema_version__ reports the exact release the models were generated from:
from oscal_bindings import __oscal_schema_version__
__oscal_schema_version__ # '1.2.3'
Worth checking, because models are strict about unknown fields: a document from a newer 1.x release than the vendored one can fail validation on a field these bindings have never seen. The constant makes that gap visible rather than a mystery.
Usage
Parsing
from oscal_bindings import parse_oscal, parse_oscal_file, parse_catalog, parse_profile
# Parse any OSCAL document
doc = parse_oscal(json_string)
# Parse with type-specific convenience functions
catalog = parse_catalog(json_string) # returns CatalogDocument
profile = parse_profile(json_string) # returns ProfileDocument
# Parse from a file
doc = parse_oscal_file("catalog.json")
parse_oscal and the per-type parsers accept either str or bytes. Passing bytes directly skips a UTF-8 decode step, which avoids one document-size string allocation — preferable when the document comes from a byte-source (HTTP response body, S3 GET, container layer pull):
import urllib.request
with urllib.request.urlopen("https://example.com/catalog.json") as resp:
doc = parse_oscal(resp.read()) # bytes → parsed model, no .decode() needed
Invalid UTF-8 bytes surface as OscalParseError, the same exception type used for JSON and schema validation failures.
Accessing Parsed Data
catalog = parse_catalog(json_string)
print(catalog.catalog.metadata.title) # plain str
print(catalog.catalog.metadata.version)
Accessors
OscalDoc is a uniform facade over any of the 8 parsed wrappers (CatalogDocument, ProfileDocument, SystemSecurityPlanDocument, …). It exposes the structural fields shared across every OSCAL top-level document — metadata, oscal_version, uuid — without making the caller dispatch on body type.
from oscal_bindings import OscalDoc
doc = OscalDoc.from_file("ssp.json")
doc.oscal_version # '1.2.0'
doc.uuid # body-level UUID, regardless of document type
doc.metadata.title
doc.body # the underlying Catalog | Profile | SSP | ...
Wrap an already-parsed document directly:
from oscal_bindings import OscalDoc, parse_oscal_file
doc = OscalDoc(parse_oscal_file("ssp.json"))
For Assessment Plans and Assessment Results, assessment_period() returns the (start, end) date range:
from oscal_bindings import OscalAccessError
doc = OscalDoc.from_file("assessment-results.json")
start, end = doc.assessment_period() # (date(2025, 1, 5), date(2025, 1, 30))
try:
OscalDoc.from_file("catalog.json").assessment_period()
except OscalAccessError as e:
print(e) # only defined for AP / AR
Building back-matter resources
from oscal_bindings import make_hash, make_resource, make_rlink
digest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
resource = make_resource(
title="Evidence: signed audit report",
description="Q1 audit deliverable, two equivalent mirrors.",
rlinks=[
make_rlink("https://example.com/audit-q1.pdf", media_type="application/pdf",
hashes=[make_hash(digest)]),
make_rlink("s3://evidence-bucket/audit-q1.pdf",
hashes=[make_hash(digest)]),
],
)
# resource.uuid is a freshly generated UUID4
Serialization
from oscal_bindings import serialize_oscal
json_str = serialize_oscal(doc) # pretty-printed (indent: 2)
json_str = serialize_oscal(doc, indent=None) # compact
Every model serializes to valid OSCAL by default: OSCAL key names (media-type, not media_type) and no null for unset fields, on every path. That covers model_dump(), model_dump(mode="json"), model_dump_json(), pydantic_core.to_json(...), and TypeAdapter(...).dump_python(...), including models nested in plain dicts and lists. Framework serializers that never pass kwargs (e.g. MCP or agent tool results) get valid OSCAL too. serialize_oscal is still the recommended call for whole documents.
from oscal_bindings import make_rlink
rlink = make_rlink("https://example.com/a.pdf", media_type="application/pdf")
rlink.model_dump_json() # '{"href":"https://example.com/a.pdf","media-type":"application/pdf"}'
rlink.model_dump(by_alias=False) # {'href': '...', 'media_type': 'application/pdf'}
Opt-outs:
by_alias=Falseemits Python field names, still withoutnull.exclude_none=Falsehas no effect. OSCAL forbidsnull, so None-valued fields are always omitted. To tell whether a field was set, use attribute access (rlink.hashes is None) ormodel_fields_set.
Field names vs. OSCAL names
Python code can use field names; OSCAL data must use OSCAL names.
- Constructor keyword arguments accept either:
Rlink(href=..., media_type=...)andRlink(href=..., **{"media-type": ...})build the same model. Plain dicts nested inside the kwargs are OSCAL data and must use OSCAL names. - Dict input (
Model.model_validate(...),validate_element) rejects a field name at any depth withextra_forbidden. - JSON input (
parse_oscal,parse_oscal_file,validate_oscal, the typedparse_*functions,model_validate_json) never populates a field from a field name key.
On pydantic 2.13.x, JSON input drops a field-name key without an error instead of rejecting it. See Known issue: Invalid OSCAL JSON keys can be silently lost.
Validation
from oscal_bindings import validate_oscal
if validate_oscal(json_string):
print("Valid OSCAL document")
Error Handling
from oscal_bindings import parse_oscal, OscalParseError
try:
parse_oscal(invalid_json)
except OscalParseError as e:
print(e) # Human-readable message
print(e.errors) # Pydantic validation error details
Typed Parser Functions
Each OSCAL document type has a dedicated parser:
parse_catalog(json)→CatalogDocumentparse_profile(json)→ProfileDocumentparse_component_definition(json)→ComponentDefinitionDocumentparse_system_security_plan(json)→SystemSecurityPlanDocumentparse_assessment_plan(json)→AssessmentPlanDocumentparse_assessment_results(json)→AssessmentResultsDocumentparse_plan_of_action_and_milestones(json)→PlanOfActionAndMilestonesDocumentparse_mapping_collection(json)→MappingCollectionDocument
Building
hatch build
hatch run release # generate + lint + typing + security + test + coverage + docs
Architecture
- Version package (
src/oscal_bindings/v1/) — everything below, scoped to the OSCAL major version. Exposes__oscal_schema_version__. - Generated models (
src/oscal_bindings/v1/models.py) — Pydantic v2 classes withAnnotatedfield constraints, produced bydatamodel-code-generatorfrom the OSCAL JSON Schema. - Model base (
src/oscal_bindings/v1/_base.py) — hand-writtenOscalBaseModel, which every generated object model inherits via--base-class: OSCAL keys and no nulls by default, field-name constructor kwargs. - Post-processing (
scripts/postprocess_models.py) — renames classes from schema namespace paths to clean short names and names each document wrapper after its root key (e.g.CatalogDocument). - Vendored schemas (
schemas/<release>/) — one directory per OSCAL release; the active bundle is chosen by explicit path. - Runtime (
src/oscal_bindings/v1/parser.py) — parse / serialize / validate utilities and per-type typed parsers. - Extensions (
src/oscal_bindings/v1/extensions/) — hand-written helpers layered on top of the generated models:document.py—OscalDocfacade (uniformmetadata/uuid/oscal_version/assessment_period()accessors).builders.py—make_hash,make_rlink,make_resourceconstructors forback-matterelements.validate_element.py— element-level schema validation utilities.
- Top-level package (
src/oscal_bindings/__init__.pyand themodels/parser/extensionsalias modules) — pure re-exports of the current version package.
Public symbols are re-exported from oscal_bindings.v1 and from the top-level oscal_bindings package, so callers don't need to know which submodule a name lives in.
Requirements
- Python 3.11+ (tested on 3.11–3.14)
- Pydantic 2.11+
License
Apache License 2.0; see LICENSE. The vendored OSCAL schemas are from NIST and are in the public domain; see NOTICE.
Metadata
Release files for oscal-bindings 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| oscal_bindings-0.2.1.tar.gz | 382.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| oscal_bindings-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 434.4 kB
Release files / oscal_bindings-0.2.1.tar.gz
| Download URL | oscal_bindings-0.2.1.tar.gz |
|---|---|
| Size | 382.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3a25e6558a40a00245626e995b82aa6e2ceeee088a643941bf6a8cd7299245e4
|
|
BLAKE2b-256 checksum How to use checksums |
a3948c1c003607feead62834b3f6c62c16188ba5e025686fe70c10ab5c479a66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / oscal_bindings-0.2.1-py3-none-any.whl
| Download URL | oscal_bindings-0.2.1-py3-none-any.whl |
|---|---|
| Size | 52.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c403c59fc4f3fc82bdd81f8d43ef3c30fee82e723646ec65bf68d2951f8fa708
|
|
BLAKE2b-256 checksum How to use checksums |
9cd2a938a9a355d58c8ba2a0f6154dfd3509dd7efc472d12e39fb1a5c03ec2fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log