Skip to content

Validate a bundle

Notebook source

Rendered from notebooks/validation/validate-bundles.ipynb.

A bundle is a JSON file of genomic knowledge records. Its schema describes what the bundle must contain. load_bundle checks both files before it returns a usable Bundle.

These small Python examples need no downloads.

When validation happens

load_bundle checks the stored bundle before it returns a Bundle. It checks these items in order:

  1. Reads the bundle and producer schema.
  2. Checks the producer schema.
  3. Checks that the bundle has the expected basic structure.
  4. Checks links inside the bundle, called bundle-local JSON Pointers.
  5. Checks the complete bundle against the schema.
  6. Checks GKM product versions and recognized GKM records.

The first failed check stops loading. Link checks report broken links together. Schema and GKM record validation stop at the first problem they find.

In [1]
import io
import json
from typing import Any

from ga4gh.gkm.bundles import (
    BundleReferenceError,
    BundleSerializationError,
    BundleValidationError,
    load_bundle,
    supported_gkm_versions,
)

producer_only_schema = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "records": {
            "type": "object",
            "additionalProperties": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "score": {"type": "number"},
                },
                "required": ["name", "score"],
            },
        }
    },
    "required": ["records"],
}


def as_stream(value: Any) -> io.StringIO:  # noqa: ANN401
    """Turn a Python value into an in-memory JSON file.

    :param value: JSON-compatible Python value to serialize.
    :returns: A readable in-memory JSON stream.
    """
    return io.StringIO(json.dumps(value))

1. Read a valid producer-only bundle

This producer-only schema requires every record to have a text name and numeric score. The bundle below passes every check.

In [2]
producer_only_document = {
    "records": {"example": {"name": "Example record", "score": 1}}
}

bundle_stream = as_stream(producer_only_document)
schema_stream = as_stream(producer_only_schema)

bundle = load_bundle(
    bundle_stream,
    schema=schema_stream,
)
bundle.collection_names()

Output:

('records',)

2. Check the producer schema

The producer supplies the bundle schema. This schema has no GKM references. The Toolkit accepts it because it is valid and declares JSON Schema Draft 2020-12.

A schema without that declaration fails before the Toolkit checks the bundle.

In [3]
schema_without_declaration = {"type": "object"}
bundle_stream = as_stream(producer_only_document)
schema_stream = as_stream(schema_without_declaration)

try:
    load_bundle(
        bundle_stream,
        schema=schema_stream,
    )
except BundleSerializationError as error:
    print(error)

Output:

Bundle JSON Schema must declare Draft 2020-12 with '$schema': 'https://json-schema.org/draft/2020-12/schema'

3. Check the basic structure

The valid bundle above is a JSON object, which looks like {...}. A JSON list, which looks like [...], cannot be loaded as a bundle.

In [4]
bundle_stream = as_stream([])
schema_stream = as_stream(producer_only_schema)

try:
    load_bundle(bundle_stream, schema=schema_stream)
except BundleSerializationError as error:
    print(error)

Output:

A bundle document must be a JSON object

A bundle can link one record to another with a bundle-local JSON Pointer, such as #/records/example. The Toolkit checks each link.

In [5]
document_with_link = {
    "records": {
        "example": "#/records/target",
        "target": {"name": "Target record", "score": 2},
    }
}
bundle_stream = as_stream(document_with_link)
schema_stream = as_stream(producer_only_schema)

linked_bundle = load_bundle(bundle_stream, schema=schema_stream)
linked_bundle.collection_names()

Output:

('records',)

This bundle has two broken links. The Toolkit reports both in one error.

In [6]
document_with_broken_link = {
    "records": {
        "example": "#/records/missing",
        "another": "#/records/also-missing",
    }
}
bundle_stream = as_stream(document_with_broken_link)
schema_stream = as_stream(producer_only_schema)
try:
    load_bundle(
        bundle_stream,
        schema=schema_stream,
    )
except BundleReferenceError as error:
    print(error)

Output:

Invalid bundle references (2 total):
- '#/records/missing' at /records/example: object member 'missing' does not exist
- '#/records/also-missing' at /records/another: object member 'also-missing' does not exist

5. Check fields against the producer schema

This document has a record, but it is missing the required score field. The producer schema check raises BundleValidationError.

In [7]
invalid_document = {"records": {"example": {"name": "Example record"}}}
bundle_stream = as_stream(invalid_document)
schema_stream = as_stream(producer_only_schema)

try:
    load_bundle(
        bundle_stream,
        schema=schema_stream,
    )
except BundleValidationError as error:
    print(error)

Output:

Bundle does not match its JSON Schema at records/example: 'score' is a required property

Fields with the wrong type also fail the producer schema check.

In [8]
invalid_type_document = {"records": {"example": {"name": 1, "score": "high"}}}
bundle_stream = as_stream(invalid_type_document)
schema_stream = as_stream(producer_only_schema)

try:
    load_bundle(
        bundle_stream,
        schema=schema_stream,
    )
except BundleValidationError as error:
    print(error)

Output:

Bundle does not match its JSON Schema at records/example/name: 1 is not of type 'string'

6. Check GKM product versions and records

A producer schema can refer to GKM product schemas, such as VRS. The Toolkit checks that their versions match the installed libraries.

This valid VRS SequenceReference is checked by the VRS schema and Python model.

In [9]
versions = supported_gkm_versions()
sequence_reference_schema = (
    f"https://w3id.org/ga4gh/schema/vrs/{versions['vrs']}/json/SequenceReference"
)
gkm_schema = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "sequenceReference": {
            "type": "object",
            "additionalProperties": {"$ref": sequence_reference_schema},
        }
    },
    "required": ["sequenceReference"],
}
gkm_document = {
    "sequenceReference": {
        "refseq": {
            "type": "SequenceReference",
            "refgetAccession": "SQ.6CnHhDq_bDCsuIBf0AzxtKq_lXYM7f0m",
        }
    }
}

bundle_stream = as_stream(gkm_document)
schema_stream = as_stream(gkm_schema)

gkm_bundle = load_bundle(bundle_stream, schema=schema_stream)
gkm_bundle.collection_names()

Output:

('sequenceReference',)

This VRS record is missing its required refgetAccession.

In [10]
invalid_gkm = {"sequenceReference": {"refseq": {"type": "SequenceReference"}}}
bundle_stream = as_stream(invalid_gkm)
schema_stream = as_stream(gkm_schema)

try:
    load_bundle(bundle_stream, schema=schema_stream)
except BundleValidationError as error:
    print(error)

Output:

Bundle does not match its JSON Schema at sequenceReference/refseq: 'refgetAccession' is a required property

This VRS record has an invalid refgetAccession.

In [11]
invalid_gkm = {
    "sequenceReference": {
        "refseq": {
            "type": "SequenceReference",
            "refgetAccession": "6CnHhDq_bDCsuIBf0AzxtKq_lXYM7f0m",
        }
    }
}
bundle_stream = as_stream(invalid_gkm)
schema_stream = as_stream(gkm_schema)

try:
    load_bundle(bundle_stream, schema=schema_stream)
except BundleValidationError as error:
    print(error)

Output:

Bundle does not match its JSON Schema at sequenceReference/refseq/refgetAccession: '6CnHhDq_bDCsuIBf0AzxtKq_lXYM7f0m' does not match '^SQ.[0-9A-Za-z_\\-]{32}$'