Source code for linkml.validator.plugins.jsonschema_validation_plugin
import logging
import os
from collections.abc import Iterator
from typing import Any
from jsonschema.exceptions import best_match
from linkml.validator.plugins.validation_plugin import ValidationPlugin
from linkml.validator.report import Severity, ValidationResult
from linkml.validator.validation_context import ValidationContext
logger = logging.getLogger(__name__)
[docs]
class JsonschemaValidationPlugin(ValidationPlugin):
"""A validation plugin which validates instances using a JSON Schema validator.
:param closed: If ``True``, additional properties are not allowed on instances.
Defaults to ``False``.
:param include_range_class_descendants: If True, use an open world assumption and allow the
range of a slot to be any descendant of the declared range. Note that if the range of a
slot has a type designator, descendants will always be included.
:param json_schema_path: If provided, JSON Schema will not be generated from the schema,
instead it will be read from this path. In this case the value of the ``closed`` argument
is disregarded and the open- or closed-ness of the existing JSON Schema is taken as-is.
:param allow_null_for_optional_enums: If ``True``, downgrade enum validation errors
to warnings when the value is null/empty and the slot is not required. Prevents
spurious ``None is not one of [...]`` and ``'' is not one of [...]`` errors for
nullable enum columns. Defaults to ``False``.
"""
def __init__(
self,
*,
closed: bool = False,
include_range_class_descendants: bool = True,
json_schema_path: os.PathLike | None = None,
allow_null_for_optional_enums: bool = False,
) -> None:
self.closed = closed
self.include_range_class_descendants = include_range_class_descendants
self.json_schema_path = json_schema_path
self.allow_null_for_optional_enums = allow_null_for_optional_enums
def _is_null_enum_error(self, value: Any, path: list, context: ValidationContext) -> bool:
"""
Returns True if the error is a null/empty value on an optional enum slot.
Uses the jsonschema error's absolute_path and instance value directly —
no regex parsing of error message strings needed.
:param value: The actual invalid value (e.g. '' or None)
:param path: The absolute_path from the jsonschema error as a list
:param context: The validation context providing schema_view
"""
# Only handle null/empty values
if value is not None and value != "":
return False
# Need at least one path element to identify the slot
if not path:
return False
# The last element of the path is the slot name
slot_name = str(path[-1])
try:
sv = context.schema_view
induced = sv.induced_slot(slot_name, context.target_class)
# Downgrade only if slot is not required AND has an enum range
if not induced.required:
if induced.range and induced.range in sv.all_enums():
return True
except Exception as e:
logger.debug("Could not induce slot %s for null enum check: %s", slot_name, e)
return False
def process(self, instance: Any, context: ValidationContext) -> Iterator[ValidationResult]:
"""Perform JSON Schema validation on the provided instance
:param instance: The instance to validate
:param context: The validation context which provides a JSON Schema artifact
:return: Iterator over validation results
:rtype: Iterator[ValidationResult]
"""
validator = context.json_schema_validator(
closed=self.closed,
include_range_class_descendants=self.include_range_class_descendants,
path_override=self.json_schema_path,
)
for error in validator.iter_errors(instance):
error_context = [ctx.message for ctx in error.context]
best_error = best_match([error])
message = f"{best_error.message} in /{'/'.join(str(p) for p in best_error.absolute_path)}"
severity = Severity.ERROR
if self.allow_null_for_optional_enums:
# Use absolute_path and instance value directly — no regex needed
path = list(best_error.absolute_path)
value = best_error.instance
if self._is_null_enum_error(value, path, context):
severity = Severity.WARN
yield ValidationResult(
type="jsonschema validation",
severity=severity,
instance=instance,
instantiates=context.target_class,
message=message,
context=error_context,
source=best_error,
)