Skip to content

cdm_data_loaders.schemas.jsonschema

JSON Schema validation, custom KBase XSV metaschema, and $ref/allOf dereferencing utilities.

ValidatedSchema pydantic-model

Bases: BaseModel

A validated JSON Schema.

Show JSON schema:
{
  "description": "A validated JSON Schema.",
  "properties": {
    "jsonschema": {
      "additionalProperties": true,
      "title": "Jsonschema",
      "type": "object"
    }
  },
  "required": [
    "jsonschema"
  ],
  "title": "ValidatedSchema",
  "type": "object"
}

Fields:

  • jsonschema (dict[str, Any])

Validators:

Source code in src/cdm_data_loaders/schemas/jsonschema.py
class ValidatedSchema(BaseModel):
    """A validated JSON Schema."""

    jsonschema: dict[str, Any]

    @field_validator("jsonschema", mode="after")
    @classmethod
    def validate_jsonschema(cls, schema: dict[str, Any]) -> dict[str, Any]:
        """Validate the schema in the jsonschema field.

        :param schema: a JSON schema
        :type schema: dict[str, Any]
        :raises ValueError: if there is no ``$schema`` field to identify the metaschema used
        :raises jsonschema.exceptions.SchemaError: if the schema is invalid
        :raises jsonschema.exceptions.ValidationError: if the schema fails validation
        :return: validated schema
        :rtype: dict[str, Any]
        """
        if "$schema" not in schema or not isinstance(schema["$schema"], str):
            err_msg = "JSON Schema is missing the $schema keyword"
            raise ValueError(err_msg)

        declared = schema["$schema"]
        if declared == CUSTOM_META_SCHEMA_URI:
            register_xsv_validator()

        validator = jsonschema.validators.validator_for(schema, default=None)  # type: ignore[arg-type]
        if validator is None:
            err_msg = f"Unsupported JSON Schema draft: {declared}"
            raise ValueError(err_msg)

        try:
            validator.check_schema(schema)
        except (jsonschema.exceptions.SchemaError, jsonschema.exceptions.ValidationError):
            logger.exception("Error validating JSON Schema")
            raise
        return schema

validate_jsonschema pydantic-validator

validate_jsonschema(
    schema: dict[str, Any],
) -> dict[str, Any]

Validate the schema in the jsonschema field.

Parameters:

Name Type Description Default
schema dict[str, Any]

a JSON schema

required

Returns:

Type Description
dict[str, Any]

validated schema

Raises:

Type Description
ValueError

if there is no $schema field to identify the metaschema used

jsonschema.exceptions.SchemaError

if the schema is invalid

jsonschema.exceptions.ValidationError

if the schema fails validation

Source code in src/cdm_data_loaders/schemas/jsonschema.py
@field_validator("jsonschema", mode="after")
@classmethod
def validate_jsonschema(cls, schema: dict[str, Any]) -> dict[str, Any]:
    """Validate the schema in the jsonschema field.

    :param schema: a JSON schema
    :type schema: dict[str, Any]
    :raises ValueError: if there is no ``$schema`` field to identify the metaschema used
    :raises jsonschema.exceptions.SchemaError: if the schema is invalid
    :raises jsonschema.exceptions.ValidationError: if the schema fails validation
    :return: validated schema
    :rtype: dict[str, Any]
    """
    if "$schema" not in schema or not isinstance(schema["$schema"], str):
        err_msg = "JSON Schema is missing the $schema keyword"
        raise ValueError(err_msg)

    declared = schema["$schema"]
    if declared == CUSTOM_META_SCHEMA_URI:
        register_xsv_validator()

    validator = jsonschema.validators.validator_for(schema, default=None)  # type: ignore[arg-type]
    if validator is None:
        err_msg = f"Unsupported JSON Schema draft: {declared}"
        raise ValueError(err_msg)

    try:
        validator.check_schema(schema)
    except (jsonschema.exceptions.SchemaError, jsonschema.exceptions.ValidationError):
        logger.exception("Error validating JSON Schema")
        raise
    return schema

DereferencingError

Bases: ValueError

Raised when a JSON Schema document cannot be fully dereferenced.

Source code in src/cdm_data_loaders/schemas/jsonschema.py
class DereferencingError(ValueError):
    """Raised when a JSON Schema document cannot be fully dereferenced."""

DereferenceContext dataclass

Mutable state for a single dereference_schema() call.

Parameters:

Name Type Description Default
resolver Resolver

the referencing resolver rooted at the schema currently being dereferenced

required
spec Any

the JSON Schema specification/dialect in effect

required
max_ref_depth int

maximum depth of a $ref chain before giving up

required
ref_sibling_mode RefSiblingMode

how to handle keywords declared alongside $ref

required
active_refs set[str]

the set of $ref values currently being resolved, used for circular-reference detection

set()
Source code in src/cdm_data_loaders/schemas/jsonschema.py
@dataclass
class DereferenceContext:
    """Mutable state for a single ``dereference_schema()`` call.

    :param resolver: the ``referencing`` resolver rooted at the schema currently being dereferenced
    :type resolver: Resolver
    :param spec: the JSON Schema specification/dialect in effect
    :type spec: Any
    :param max_ref_depth: maximum depth of a ``$ref`` chain before giving up
    :type max_ref_depth: int
    :param ref_sibling_mode: how to handle keywords declared alongside ``$ref``
    :type ref_sibling_mode: RefSiblingMode
    :param active_refs: the set of ``$ref`` values currently being resolved, used for circular-reference detection
    :type active_refs: set[str]
    """

    resolver: Resolver
    spec: Any
    max_ref_depth: int
    ref_sibling_mode: RefSiblingMode
    active_refs: set[str] = dataclass_field(default_factory=set)

load_xsv_meta_schema

load_xsv_meta_schema(
    local_schema_path: Path | Traversable | None = None,
    remote_url: str = CUSTOM_META_SCHEMA_URI,
) -> frozendict[str, Any]

Load the custom XSV metaschema and validate it against its declared metaschema.

Reads the custom metaschema from schemas/xsv_config/0.1.0/schema.json if available, or from https://kbase.github.io/cdm-data-loaders/schemas/xsv_config/0.1.0/schema.json otherwise, and validates the document against its own $schema metaschema.

Parameters:

Name Type Description Default
local_schema_path Path | Traversable | None

optional local schema path or Traversable override

None
remote_url str

fallback URL to fetch the metaschema from when unavailable locally

CUSTOM_META_SCHEMA_URI

Returns:

Type Description
frozendict[str, Any]

validated custom metaschema as an immutable mapping

Raises:

Type Description
TypeError

if the document is not a JSON object

ValueError

if the document omits $schema, declares itself as its own metaschema, or uses an unsupported JSON Schema draft

jsonschema.exceptions.SchemaError

if the custom metaschema fails validation against its metaschema

Source code in src/cdm_data_loaders/schemas/jsonschema.py
def load_xsv_meta_schema(
    local_schema_path: Path | Traversable | None = None,
    remote_url: str = CUSTOM_META_SCHEMA_URI,
) -> frozendict[str, Any]:
    """Load the custom XSV metaschema and validate it against its declared metaschema.

    Reads the custom metaschema from ``schemas/xsv_config/0.1.0/schema.json`` if available,
    or from ``https://kbase.github.io/cdm-data-loaders/schemas/xsv_config/0.1.0/schema.json``
    otherwise, and validates the document against its own ``$schema`` metaschema.

    :param local_schema_path: optional local schema path or ``Traversable`` override
    :type local_schema_path: Path | Traversable | None
    :param remote_url: fallback URL to fetch the metaschema from when unavailable locally
    :type remote_url: str
    :return: validated custom metaschema as an immutable mapping
    :rtype: frozendict[str, Any]
    :raises TypeError: if the document is not a JSON object
    :raises ValueError: if the document omits ``$schema``, declares itself as its own metaschema,
        or uses an unsupported JSON Schema draft
    :raises jsonschema.exceptions.SchemaError: if the custom metaschema fails validation against its metaschema
    """
    raw_schema = _read_raw_xsv_meta_schema(local_schema_path=local_schema_path, remote_url=remote_url)
    if not isinstance(raw_schema, dict):
        err_msg = "Custom metaschema must be a JSON object"
        raise TypeError(err_msg)

    declared = raw_schema.get("$schema")
    if not isinstance(declared, str) or not declared:
        err_msg = "JSON Schema is missing the $schema keyword"
        raise ValueError(err_msg)

    if declared in {CUSTOM_META_SCHEMA_URI, remote_url}:
        err_msg = f"Custom metaschema cannot declare itself as its own $schema: {declared}"
        raise ValueError(err_msg)

    meta_validator = jsonschema.validators.validator_for(raw_schema, default=None)  # type: ignore[arg-type]
    if meta_validator is None:
        err_msg = f"Unsupported JSON Schema draft: {declared}"
        raise ValueError(err_msg)

    meta_validator.check_schema(raw_schema)
    return frozendict(raw_schema)

get_xsv_meta_schema cached

get_xsv_meta_schema() -> frozendict[str, Any]

Lazily load, validate, and cache the custom KBase XSV metaschema.

Returns:

Type Description
frozendict[str, Any]

validated custom metaschema mapping

Source code in src/cdm_data_loaders/schemas/jsonschema.py
@cache
def get_xsv_meta_schema() -> frozendict[str, Any]:
    """Lazily load, validate, and cache the custom KBase XSV metaschema.

    :return: validated custom metaschema mapping
    :rtype: frozendict[str, Any]
    """
    return load_xsv_meta_schema()

get_xsv_config_schema

get_xsv_config_schema() -> frozendict[str, Any]

Lazily extract the x-xsv-config extension schema from the custom XSV metaschema.

Returns:

Type Description
frozendict[str, Any]

immutable x-xsv-config JSON Schema mapping

Raises:

Type Description
ValueError

if the custom metaschema does not define properties['x-xsv-config']

TypeError

if properties['x-xsv-config'] is not a mapping

Source code in src/cdm_data_loaders/schemas/jsonschema.py
def get_xsv_config_schema() -> frozendict[str, Any]:
    """Lazily extract the ``x-xsv-config`` extension schema from the custom XSV metaschema.

    :return: immutable ``x-xsv-config`` JSON Schema mapping
    :rtype: frozendict[str, Any]
    :raises ValueError: if the custom metaschema does not define ``properties['x-xsv-config']``
    :raises TypeError: if ``properties['x-xsv-config']`` is not a mapping
    """
    meta_schema = get_xsv_meta_schema()
    properties = meta_schema.get("properties")
    xsv_config_schema = properties.get(X_XSV_CONFIG_KEY) if isinstance(properties, Mapping) else None
    if xsv_config_schema is None:
        err_msg = f"Custom metaschema is missing '{X_XSV_CONFIG_KEY}' object schema under 'properties'"
        raise ValueError(err_msg)
    if not isinstance(xsv_config_schema, Mapping):
        err_msg = f"Custom metaschema '{X_XSV_CONFIG_KEY}' schema must be a mapping"
        raise TypeError(err_msg)
    return frozendict(xsv_config_schema)

register_xsv_validator

register_xsv_validator() -> None

Lazily load, validate, and register the custom validator for the KBase XSV metaschema.

Returns:

Type Description
None

None

Source code in src/cdm_data_loaders/schemas/jsonschema.py
def register_xsv_validator() -> None:
    """Lazily load, validate, and register the custom validator for the KBase XSV metaschema.

    :return: None
    :rtype: None
    """
    meta_schema = get_xsv_meta_schema()
    effective_meta_schema = (
        meta_schema
        if meta_schema.get("$id") == CUSTOM_META_SCHEMA_URI
        else frozendict({**meta_schema, "$id": CUSTOM_META_SCHEMA_URI})
    )
    existing = jsonschema.validators.validator_for({"$schema": CUSTOM_META_SCHEMA_URI}, default=None)  # type: ignore[arg-type]
    if existing is not None and getattr(existing, "META_SCHEMA", None) == effective_meta_schema:
        return
    base_validator = jsonschema.validators.validator_for({**meta_schema})
    xsv_validator = validators.extend(base_validator)
    xsv_validator.META_SCHEMA = effective_meta_schema
    validators.validates(CUSTOM_META_SCHEMA_URI)(xsv_validator)

dereference_schema

dereference_schema(
    schema: dict[str, Any],
    additional_resources: (
        Mapping[str, dict[str, Any]] | None
    ) = None,
    *,
    max_ref_depth: int = 50,
    ref_sibling_mode: RefSiblingMode = "auto"
) -> dict[str, Any]

Resolve every $ref in a schema using additional_resources where needed.

Merges any allOf tags to return a fully self-contained schema.

Parameters:

Name Type Description Default
schema dict[str, Any]

the JSON Schema document to dereference

required
additional_resources Mapping[str, dict[str, Any]] | None

mapping of URI to schema dict, used to resolve external $refs

None
max_ref_depth int

maximum depth of a $ref chain before giving up

50
ref_sibling_mode RefSiblingMode

how to handle keywords declared alongside $ref: 'auto' (spec-compliant per detected draft), 'always' (permissive), or 'never' (strict pre-2019-09 semantics)

'auto'

Returns:

Type Description
dict[str, Any]

an equivalent, $ref/allOf-free JSON Schema document

Raises:

Type Description
DereferencingError

if a $ref is unresolvable, circular, or exceeds max_ref_depth, or if a dialect cannot be determined for the root schema or an external resource

Source code in src/cdm_data_loaders/schemas/jsonschema.py
def dereference_schema(
    schema: dict[str, Any],
    additional_resources: Mapping[str, dict[str, Any]] | None = None,
    *,
    max_ref_depth: int = 50,
    ref_sibling_mode: RefSiblingMode = "auto",
) -> dict[str, Any]:
    """Resolve every ``$ref`` in a schema using ``additional_resources`` where needed.

    Merges any ``allOf`` tags to return a fully self-contained schema.

    :param schema: the JSON Schema document to dereference
    :type schema: dict[str, Any]
    :param additional_resources: mapping of URI to schema dict, used to resolve external ``$ref``s
    :type additional_resources: Mapping[str, dict[str, Any]] | None
    :param max_ref_depth: maximum depth of a ``$ref`` chain before giving up
    :type max_ref_depth: int
    :param ref_sibling_mode: how to handle keywords declared alongside ``$ref``:
        ``'auto'`` (spec-compliant per detected draft), ``'always'`` (permissive), or
        ``'never'`` (strict pre-2019-09 semantics)
    :type ref_sibling_mode: RefSiblingMode
    :return: an equivalent, ``$ref``/``allOf``-free JSON Schema document
    :rtype: dict[str, Any]
    :raises DereferencingError: if a ``$ref`` is unresolvable, circular, or exceeds ``max_ref_depth``,
        or if a dialect cannot be determined for the root schema or an external resource
    """
    additional_resources = additional_resources or {}

    dialect_id = schema.get("$schema")
    spec = specification_with(dialect_id, default=DRAFT202012) if dialect_id else DRAFT202012

    try:
        root_resource = Resource.from_contents(schema, default_specification=spec)
    except CannotDetermineSpecification as exc:
        err_msg = f"Could not determine a JSON Schema dialect for the root schema: {exc}"
        raise DereferencingError(err_msg) from exc

    try:
        resources = [
            (uri, Resource.from_contents(doc, default_specification=spec)) for uri, doc in additional_resources.items()
        ]
    except CannotDetermineSpecification as exc:
        err_msg = f"Could not determine a JSON Schema dialect for an external resource in `additional_resources`: {exc}"
        raise DereferencingError(err_msg) from exc

    registry: Registry = Registry().with_resources(resources)
    resolver: Resolver[Any] = registry.resolver_with_root(root_resource)

    ctx = DereferenceContext(
        resolver=resolver,
        spec=spec,
        max_ref_depth=max_ref_depth,
        ref_sibling_mode=ref_sibling_mode,
    )
    return _dereference(schema, ctx)  # type: ignore[return-value]