Repository: linkml/linkml

Issue:
jsonschema validation caching should be keyed on both schema and entry point
The changes made in https://github.com/linkml/linkml/pull/1363 consider only the _schema_ when implementing the caching logic.

However, the JSON Schema that the instance data needs to be validated against is dependent on two parameters - the schema, as well as the entry point, which is the `target_class`.

Discussion:

Patch:
diff --git a/linkml/validators/jsonschemavalidator.py b/linkml/validators/jsonschemavalidator.py
index 96c585e1..74f74c17 100644
--- a/linkml/validators/jsonschemavalidator.py
+++ b/linkml/validators/jsonschemavalidator.py
@@ -52,7 +52,8 @@ class JsonSchemaDataValidator(DataValidator):
         if self.jsonschema_objs is None:
             self.jsonschema_objs = {}
         schema_id = self.schema.id if isinstance(self.schema, SchemaDefinition) else self.schema
-        if schema_id not in self.jsonschema_objs:
+        cache_params = frozenset([schema_id, target_class.class_name])
+        if cache_params not in self.jsonschema_objs:
             jsonschemastr = JsonSchemaGenerator(
                 self.schema,
                 mergeimports=True,
@@ -60,10 +61,10 @@ class JsonSchemaDataValidator(DataValidator):
                 not_closed=not_closed,
             ).serialize(not_closed=not_closed)
             jsonschema_obj = json.loads(jsonschemastr)
-            self.jsonschema_objs[schema_id] = jsonschema_obj
+            self.jsonschema_objs[cache_params] = jsonschema_obj
         else:
             logging.info(f"Using cached jsonschema for {schema_id}")
-            jsonschema_obj = self.jsonschema_objs[schema_id]
+            jsonschema_obj = self.jsonschema_objs[cache_params]
         return jsonschema.validate(inst_dict, schema=jsonschema_obj, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER)
 
     def validate_dict(
@@ -148,7 +149,7 @@ def cli(
     outargs = {}
     if datautils._is_xsv(input_format):
         if index_slot is None:
-            index_slot = infer_index_slot(sv, target_class)
+            index_slot = datautils.infer_index_slot(sv, target_class)
             if index_slot is None:
                 raise Exception("--index-slot is required for CSV input")
         inargs["index_slot"] = index_slot


Test patch:
diff --git a/tests/test_validation/input/kitchen_sink_inst_02.yaml b/tests/test_validation/input/kitchen_sink_inst_02.yaml
new file mode 100644
index 00000000..cee57ba9
--- /dev/null
+++ b/tests/test_validation/input/kitchen_sink_inst_02.yaml
@@ -0,0 +1,32 @@
+persons:
+  - id: P:003
+    name: hermoine granger
+    age_in_years: 33
+  - id: P:004
+    name: harry potter
+    has_employment_history:
+      - employed_at: ROR:1
+        started_at_time: 2023-01-01
+        is_current: true
+    has_familial_relationships:
+      - related_to: P:001
+        type: SIBLING_OF
+    has_medical_history:
+      - started_at_time: 2023-01-01
+        in_location: GEO:1234
+        diagnosis:
+          id: CODE:P1789
+          name: hypertension
+        procedure:
+          id: CODE:P1846
+          name: valve repair
+    addresses:
+      - street: 4 privet drive
+        city: surrey
+companies:
+  - id: ROR:1
+    name: hogwarts
+activities:
+  - id: A:1
+    started_at_time: 2023-01-01
+    was_associated_with: Agent:987
diff --git a/tests/test_validation/test_jsonschemavalidation.py b/tests/test_validation/test_jsonschemavalidation.py
index 54e18f12..6b2f9c80 100644
--- a/tests/test_validation/test_jsonschemavalidation.py
+++ b/tests/test_validation/test_jsonschemavalidation.py
@@ -1,27 +1,40 @@
 import unittest
 
-from linkml_runtime.loaders import json_loader, yaml_loader
-from linkml_runtime.utils.schemaview import SchemaView
+from linkml_runtime.loaders import yaml_loader
 
 from linkml.generators.pythongen import PythonGenerator
 from linkml.validators import JsonSchemaDataValidator
 from tests.test_validation.environment import env
 
 SCHEMA = env.input_path("kitchen_sink.yaml")
-DATA = env.input_path("kitchen_sink_inst_01.yaml")
+INSTANCE_DATA_1 = env.input_path("kitchen_sink_inst_01.yaml")
+INSTANCE_DATA_2 = env.input_path("kitchen_sink_inst_02.yaml")
 
 
 class JsonSchemaValidatorTestCase(unittest.TestCase):
     def test_jsonschema_validation(self):
         """Validate data against a LinkML module using a json-schema validator"""
-        print(f"TEST: Loading {SCHEMA}")
         mod = PythonGenerator(SCHEMA).compile_module()
-        obj = yaml_loader.load(source=DATA, target_class=mod.Dataset)
-        # schema = SchemaView(SCHEMA).schema
+        obj1 = yaml_loader.load(source=INSTANCE_DATA_1, target_class=mod.Dataset)
         v = JsonSchemaDataValidator(schema=SCHEMA)
-        print(f"Validating: {obj}")
-        results = v.validate_object(obj)
-        print(results)
+        # check that jsonschema_objs dict cache is empty before validate_object()
+        # first call
+        self.assertIsNone(v.jsonschema_objs)
+        v.validate_object(obj1, target_class=mod.Dataset)
+
+        obj2 = yaml_loader.load(source=INSTANCE_DATA_2, target_class=mod.Dataset)
+        v.validate_object(obj2, target_class=mod.Dataset)
+
+        # check that the cache store is a dict
+        self.assertEqual(type(v.jsonschema_objs), dict)
+        # check that the cache store is not empty
+        self.assertGreater(len(v.jsonschema_objs.keys()), 0)
+        for f, j in v.jsonschema_objs.items():
+            # check that cache store keys are of type frozendict()
+            self.assertEqual(type(f), frozenset)
+            # check that cache store values are dicts
+            self.assertEqual(type(j), dict)
+            self.assertGreater(len(j.keys()), 0)
 
 
 if __name__ == "__main__":