Repository: linkml/linkml

Issue:
Refactor jsonschema generator to use schemaview
Currently jsonschemagen uses the older restricted visitor pattern, which makes it harder to implement things like jsonschema composition, which are inherently recursive.

There are a few other things that it would be good to change, the order I would do this

- [ ] Extend the [compliance cases](https://github.com/linkml/linkml/blob/main/tests/test_generators/input/kitchen_sink_failtest_inst_01.yaml)
- [ ] make the existing tests more modular
- [ ] switch from visitor to schemaview
- [ ] avoid dict manipulation and use a model of json schema; also avoid string manipulation of the form `f"#/$defs/{...}`

The last one may be non-critical. However, I think some lightweight abstracting out of common patterns may be useful.



Discussion:

Patch:
diff --git a/linkml/generators/jsonschemagen.py b/linkml/generators/jsonschemagen.py
index 06314bf0..ec595e62 100644
--- a/linkml/generators/jsonschemagen.py
+++ b/linkml/generators/jsonschemagen.py
@@ -1,17 +1,18 @@
+import json
 import logging
 import os
+from collections import UserDict
 from copy import deepcopy
 from dataclasses import dataclass, field
 from typing import Dict, List, Optional, Tuple, Union
 
 import click
-from jsonasobj2 import JsonObj, as_json
 from linkml_runtime.linkml_model.meta import (ClassDefinition, EnumDefinition,
                                               PermissibleValue,
                                               PermissibleValueText,
-                                              SchemaDefinition, SlotDefinition, ClassDefinitionName)
+                                              SlotDefinition, ClassDefinitionName,
+                                              metamodel_version)
 from linkml_runtime.utils.formatutils import be, camelcase, underscore
-from linkml_runtime.utils.schemaview import SchemaView
 
 from linkml._version import __version__
 from linkml.utils.generator import Generator, shared_arguments
@@ -33,7 +34,81 @@ json_schema_types: Dict[str, Tuple[str, Optional[str]]] = {
     "xsdtime": ("string", "time"),
 }
 
-WITH_OPTIONAL_IDENTIFIER_SUFFIX = "__identifier_optional"
+class JsonSchema(UserDict):
+
+    OPTIONAL_IDENTIFIER_SUFFIX = "__identifier_optional"
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._lax_forward_refs = {}
+
+    def add_def(self, name: str, subschema: "JsonSchema") -> None:
+        canonical_name = camelcase(name)
+
+        if '$defs' not in self:
+            self['$defs'] = {}
+
+        if 'title' not in subschema:
+            subschema['title'] = canonical_name
+
+        self['$defs'][canonical_name] = subschema
+
+        if canonical_name in self._lax_forward_refs:
+            identifier_name = self._lax_forward_refs.pop(canonical_name)
+            self.add_lax_def(canonical_name, identifier_name)
+
+    def add_lax_def(self, name: str, identifier_name: str) -> None:
+        # JSON-Schema does not have inheritance,
+        # so we duplicate slots from inherited parents and mixins
+        # Maps e.g. Person --> Person__identifier_optional
+        # for use when Person is a range of an inlined-as-dict slot
+        canonical_name = camelcase(name)
+
+        if "$defs" not in self or canonical_name not in self["$defs"]:
+            self._lax_forward_refs[canonical_name] = identifier_name
+        else:
+            lax_cls = deepcopy(self["$defs"][canonical_name])
+            lax_cls['required'].remove(identifier_name)
+            self["$defs"][canonical_name + self.OPTIONAL_IDENTIFIER_SUFFIX] = lax_cls
+
+    def add_property(self, name: str, subschema: "JsonSchema", required: bool = False) -> None:
+        canonical_name = underscore(name)
+        
+        if 'properties' not in self:
+            self['properties'] = {}
+        
+        self['properties'][canonical_name] = subschema
+
+        if required:
+            if 'required' not in self:
+                self['required'] = []
+            
+            self['required'].append(canonical_name)
+
+    def __str__(self) -> str:
+        return json.dumps(self.data, sort_keys=True, default=lambda d: d.data)
+
+    @classmethod
+    def ref_for(cls, class_name: Union[str, List[str]], identifier_optional: bool = False):
+        def _ref(class_name):
+            return { 
+                "$ref": f"#/$defs/{camelcase(class_name)}{cls.OPTIONAL_IDENTIFIER_SUFFIX if identifier_optional else ''}"
+            }
+        if isinstance(class_name, list):
+            return {
+                "oneOf": [_ref(name) for name in class_name]
+            }
+        else:
+            return _ref(class_name)
+
+    @classmethod
+    def array_of(cls, subschema: "JsonSchema") -> "JsonSchema":
+        schema = {
+            "type": "array",
+            "items": subschema
+        }
+
+        return JsonSchema(schema)
 
 
 @dataclass
@@ -50,12 +125,11 @@ class JsonSchemaGenerator(Generator):
 
     # ClassVars
     generatorname = os.path.basename(__file__)
-    generatorversion = "0.0.2"
+    generatorversion = "0.0.3"
     valid_formats = ["json"]
     visit_all_class_slots = True
     visit_all_slots = True
-    uses_schemaloader = True
-    #requires_metamodel = False
+    uses_schemaloader = False
 
     #@deprecated("Use top_class")
     topClass: Optional[str] = None
@@ -63,82 +137,59 @@ class JsonSchemaGenerator(Generator):
     not_closed: Optional[bool] = field(default_factory=lambda: True)
     """If not closed, then an open-ended set of attributes can be instantiated for any object"""
 
-    schemaobj: JsonObj = None
-    clsobj: JsonObj = None
     inline: bool = False
     top_class: Optional[ClassDefinitionName] = None  ## JSON object is one instance of this
     """Class instantiated by the root node of the document tree"""
 
-    entryProperties: dict = field(default_factory=lambda: {})
     include_range_class_descendants: bool = field(default_factory=lambda: False)
 
-    # JSON-Schema does not have inheritance,
-    # so we duplicate slots from inherited parents and mixins
-    # Maps e.g. Person --> Person__identifier_optional
-    # for use when Person is a range of an inlined-as-dict slot
-    optional_identifier_class_map: Dict[str, Tuple[str, str]] = field(default_factory=lambda: {})
+    top_level_schema: JsonSchema = None
 
     def __post_init__(self):
         if self.topClass:
-            logging.warning(f"topCls is deprecated - use top_class")
+            logging.warning(f"topClass is deprecated - use top_class")
             self.top_class = self.topClass
-        # TODO: consider moving up a level
-        self.schemaview = SchemaView(self.schema)
+
         super().__post_init__()
 
-    def visit_schema(self, inline: bool = False, **kwargs) -> None:
+    def start_schema(self, inline: bool = False, **kwargs) -> JsonSchema:
         self.inline = inline
-        #logging.error(f"NC: {self.not_closed}")
-        self.schemaobj = JsonObj(
-            title=self.schema.name,
-            type="object",
-            metamodel_version=self.schema.metamodel_version,
-            version=self.schema.version if self.schema.version else None,
-            properties={},
-            additionalProperties=self.not_closed,
-        )
-        for p, c in self.entryProperties.items():
-            self.schemaobj["properties"][p] = {
-                "type": "array",
-                "items": {"$ref": f"#/$defs/{camelcase(c)}"},
-            }
-        self.schemaobj["$schema"] = "http://json-schema.org/draft-07/schema#"
-        self.schemaobj["$id"] = self.schema.id
-        self.schemaobj["$defs"] = JsonObj()
-        self.schemaobj["required"] = []
-
-    def end_schema(self, **_) -> None:
-        # create more lax version of every class that is used as an inlined dict reference;
-        # in this version, the primary key/identifier is optional, since it is used as the key of the dict
-        for cls_name, (
-            id_slot,
-            cls_name_lax,
-        ) in self.optional_identifier_class_map.items():
-            lax_cls = deepcopy(self.schemaobj["$defs"][cls_name])
-            lax_cls.required.remove(id_slot)
-            self.schemaobj["$defs"][cls_name_lax] = lax_cls
-        print(as_json(self.schemaobj, sort_keys=True))
-
-    def visit_class(self, cls: ClassDefinition) -> bool:
+
+        self.top_level_schema = JsonSchema({
+            "$schema": "http://json-schema.org/draft-07/schema#",
+            "$id": self.schema.id,
+            "metamodel_version": metamodel_version,
+            "version": self.schema.version if self.schema.version else None,
+            "title": self.schema.name,
+            "type": "object",
+            "additionalProperties": self.not_closed,
+        })
+
+    def visit_class(self, cls: ClassDefinition) -> None:
         if cls.mixin or cls.abstract:
-            return False
+            return
+        
         additional_properties = False
         if self.is_class_unconstrained(cls):
             additional_properties = True
-        self.clsobj = JsonObj(
-            title=camelcase(cls.name),
-            type="object",
-            properties=JsonObj(),
-            required=[],
-            additionalProperties=additional_properties,
-            description=be(cls.description),
-        )
-        return True
+        
+        class_subschema = JsonSchema({
+            "type": "object",
+            "additionalProperties": additional_properties,
+            "description": be(cls.description),
+        })
+
+        for slot_definition in self.schemaview.class_induced_slots(cls.name):
+            self.visit_class_slot(
+                subschema=class_subschema,
+                cls=cls,
+                aliased_slot_name=self.aliased_slot_name(slot_definition),
+                slot=slot_definition
+            )
 
-    def end_class(self, cls: ClassDefinition) -> None:
-        self.schemaobj["$defs"][camelcase(cls.name)] = self.clsobj
+        self.top_level_schema.add_def(cls.name, class_subschema)
 
-    def visit_enum(self, enum: EnumDefinition) -> bool:
+    def visit_enum(self, enum: EnumDefinition) -> None:
         # TODO: this only works with explicitly permitted values. It will need to be extended to
         # support other pv_formula
 
@@ -155,136 +206,110 @@ class JsonSchemaGenerator(Generator):
             map(extract_permissible_text, enum.permissible_values or [])
         )
 
-        self.schemaobj["$defs"][camelcase(enum.name)] = JsonObj(
-            title=camelcase(enum.name),
-            type="string",
-            enum=permissible_values_texts,
-            description=be(enum.description),
-        )
-
-    def get_ref_for_descendants(self, descendants: List):
-        ref_list = []
-        for descendant in descendants:
-            descendant_class = self.schema.classes[descendant]
-            if descendant_class.abstract:
-                continue
-            descendant_ref = JsonObj()
-            descendant_ref_obj = camelcase(descendant)
-            descendant_ref["$ref"] = f"#/$defs/{descendant_ref_obj}"
-            ref_list.append(descendant_ref)
-        return ref_list
+        enum_schema = JsonSchema({
+            "type": "string",
+            "enum": permissible_values_texts,
+            "description": be(enum.description),
+        })
+        self.top_level_schema.add_def(enum.name, enum_schema)
 
     def visit_class_slot(
-        self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition
+        self, subschema: JsonSchema, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition
     ) -> None:
         typ = None  # JSON Schema type (https://json-schema.org/understanding-json-schema/reference/type.html)
         reference = None  # Reference to a JSON schema entity (https://json-schema.org/understanding-json-schema/structuring.html#ref)
         fmt = None  # JSON Schema format (https://json-schema.org/understanding-json-schema/reference/string.html#format)
-        reference_obj = None
         descendants = None
-        if slot.range in self.schema.types:
-            (typ, fmt) = json_schema_types.get(
-                self.schema.types[slot.range].base.lower(), ("string", None)
-            )
-        elif slot.range in self.schema.enums:
-            reference_obj = camelcase(slot.range)
-            reference = f"#/$defs/{reference_obj}"
+
+        class_id_slot = self.schemaview.get_identifier_slot(cls.name, use_key=True)
+        slot_is_required = slot.required or slot == class_id_slot
+        slot_is_inlined = self.schemaview.is_inlined(slot)
+
+        if slot.range in self.schemaview.all_types().keys():
+            schema_type = self.schemaview.induced_type(slot.range)
+            (typ, fmt) = json_schema_types.get(schema_type.base.lower(), ("string", None))
+        elif slot.range in self.schemaview.all_enums().keys():
+            reference = slot.range
             typ = "object"
-        elif slot.range in self.schema.classes and slot.inlined:
-            reference_obj = camelcase(slot.range)
-            descendants = self.schemaview.class_descendants(slot.range)
-            reference = f"#/$defs/{reference_obj}"
+        elif slot_is_inlined and slot.range in self.schemaview.all_classes().keys():
+            reference = slot.range
+            descendants = [desc for desc in self.schemaview.class_descendants(slot.range) 
+                if not self.schemaview.get_class(desc).abstract]
             typ = "object"
         else:
             typ = "string"
-        if slot.inlined:
-            range_cls = self.schema.classes[slot.range]
-            id_slot = None
-            for sn in range_cls.slots:
-                s = self.schema.slots[sn]
-                # TODO: extension_tag should be declared as a key in extensions.yaml in metamodel
-                if (
-                    s.identifier
-                    or s.key
-                    or (
-                        s.alias == "tag"
-                        and (range_cls == "extension" or range_cls == "annotation")
-                    )
-                ):
-                    id_slot = s
-                    break
+        
+        if slot_is_inlined:
             # If inline we have to include redefined slots
-            ref = JsonObj()
-            ref["$ref"] = reference
             if slot.multivalued:
-                if id_slot is not None and not slot.inlined_as_list:
-                    prop = JsonObj(
-                        additionalProperties={
-                            "$ref": f"{reference}{WITH_OPTIONAL_IDENTIFIER_SUFFIX}"
-                        }
-                    )
-                    if id_slot.alias is not None:
-                        id_slot_name = id_slot.alias
-                    else:
-                        id_slot_name = id_slot.name
-                    self.optional_identifier_class_map[reference_obj] = (
-                        id_slot_name,
-                        f"{reference_obj}{WITH_OPTIONAL_IDENTIFIER_SUFFIX}",
-                    )
+                range_id_slot = self.schemaview.get_identifier_slot(slot.range, use_key=True)
+                if range_id_slot is not None and not slot.inlined_as_list:
+                    prop = JsonSchema({
+                        "type": "object",
+                        "additionalProperties": JsonSchema.ref_for(reference, identifier_optional=True)
+                    })
+                    self.top_level_schema.add_lax_def(reference, self.aliased_slot_name(range_id_slot))
                 else:
                     if descendants and self.include_range_class_descendants:
-                        items = JsonObj()
-                        ref_list = self.get_ref_for_descendants(descendants)
-                        items["oneOf"] = ref_list
-                        prop = JsonObj(type="array", items=items)
+                        prop = JsonSchema.array_of(JsonSchema.ref_for(descendants))
                     else:
-                        prop = JsonObj(type="array", items=ref)
+                        prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
             else:
                 if descendants and self.include_range_class_descendants:
-                    prop = JsonObj()
-                    ref_list = self.get_ref_for_descendants(descendants)
-                    prop["oneOf"] = ref_list
+                    prop = JsonSchema.ref_for(descendants)
                 else:
-                    prop = ref
+                    prop = JsonSchema.ref_for(reference)
         else:
             if slot.multivalued:
                 if reference is not None:
-                    prop = JsonObj(type="array", items={"$ref": reference})
+                    prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
                 elif fmt is None:
-                    prop = JsonObj(type="array", items={"type": typ})
+                    prop = JsonSchema.array_of(JsonSchema({"type": typ}))
                 else:
-                    prop = JsonObj(type="array", items={"type": typ, "format": fmt})
+                    prop = JsonSchema.array_of(JsonSchema({"type": typ, "format": fmt}))
             else:
                 if reference is not None:
-                    prop = JsonObj({"$ref": reference})
+                    prop = JsonSchema.ref_for(reference)
                 elif fmt is None:
-                    prop = JsonObj(type=typ)
+                    prop = JsonSchema({"type": typ})
                 else:
-                    prop = JsonObj(type=typ, format=fmt)
+                    prop = JsonSchema({"type": typ, "format": fmt})
 
         if slot.description:
-            prop.description = slot.description
-        if slot.required:
-            self.clsobj.required.append(underscore(aliased_slot_name))
+            prop['description'] = slot.description
+
         if slot.pattern:
             # See https://github.com/linkml/linkml/issues/193
-            prop.pattern = slot.pattern
+            prop['pattern'] = slot.pattern
+
         if slot.minimum_value is not None:
-            prop.minimum = slot.minimum_value
+            prop['minimum'] = slot.minimum_value
+
         if slot.maximum_value is not None:
-            prop.maximum = slot.maximum_value
+            prop['maximum'] = slot.maximum_value
+
         if slot.equals_string is not None:
-            prop.const = slot.equals_string
+            prop['const'] = slot.equals_string
+
         if slot.equals_number is not None:
-            prop.const = slot.equals_number
-        self.clsobj.properties[underscore(aliased_slot_name)] = prop
+            prop['const'] = slot.equals_number
+
+        subschema.add_property(aliased_slot_name, prop, slot_is_required)
+
         if (
             self.top_class is not None and camelcase(self.top_class) == camelcase(cls.name)
         ) or (self.top_class is None and cls.tree_root):
-            self.schemaobj.properties[underscore(aliased_slot_name)] = prop
+            self.top_level_schema.add_property(aliased_slot_name, prop, slot_is_required)
+
+    def serialize(self, **kwargs) -> str:
+        self.start_schema(**kwargs)
+        for enum_definition in self.schemaview.all_enums().values():
+            self.visit_enum(enum_definition)
+
+        for class_definition in self.schemaview.all_classes().values():
+            self.visit_class(class_definition)
 
-            if slot.required:
-                self.schemaobj.required.append(underscore(aliased_slot_name))
+        return str(self.top_level_schema)
 
 
 @shared_arguments(JsonSchemaGenerator)
diff --git a/linkml/utils/validation.py b/linkml/utils/validation.py
index da9bb9e4..3b878e7e 100644
--- a/linkml/utils/validation.py
+++ b/linkml/utils/validation.py
@@ -46,4 +46,4 @@ def validate_object(
         not_closed=not_closed,
     ).serialize(not_closed=not_closed)
     jsonschema_obj = json.loads(jsonschemastr)
-    return jsonschema.validate(inst_dict, schema=jsonschema_obj)
+    return jsonschema.validate(inst_dict, schema=jsonschema_obj, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER)
diff --git a/linkml/validators/jsonschemavalidator.py b/linkml/validators/jsonschemavalidator.py
index 1aa615ab..579919c7 100644
--- a/linkml/validators/jsonschemavalidator.py
+++ b/linkml/validators/jsonschemavalidator.py
@@ -48,7 +48,7 @@ class JsonSchemaDataValidator(DataValidator):
             not_closed=not_closed,
         ).serialize(not_closed=not_closed)
         jsonschema_obj = json.loads(jsonschemastr)
-        return jsonschema.validate(inst_dict, schema=jsonschema_obj)
+        return jsonschema.validate(inst_dict, schema=jsonschema_obj, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER)
 
     def validate_dict(
         self, data: dict, target_class: ClassDefinitionName = None, closed: bool = True
@@ -76,7 +76,7 @@ class JsonSchemaDataValidator(DataValidator):
             not_closed=not_closed,
         ).serialize(not_closed=not_closed)
         jsonschema_obj = json.loads(jsonschemastr)
-        return jsonschema.validate(data, schema=jsonschema_obj)
+        return jsonschema.validate(data, schema=jsonschema_obj, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER)
 
 
 @click.command()
diff --git a/poetry.lock b/poetry.lock
index c0b4a1e8..268c5a02 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -22,6 +22,18 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "arrow"
+version = "1.2.3"
+description = "Better dates & times for Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+python-dateutil = ">=2.7.0"
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
 [[package]]
 name = "attrs"
 version = "22.1.0"
@@ -34,7 +46,7 @@ python-versions = ">=3.5"
 dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
 docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
 tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
 
 [[package]]
 name = "babel"
@@ -86,6 +98,14 @@ webencodings = "*"
 css = ["tinycss2 (>=1.1.0,<1.2)"]
 dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"]
 
+[[package]]
+name = "cached-property"
+version = "1.5.2"
+description = "A decorator for caching properties in classes."
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "certifi"
 version = "2022.9.24"
@@ -133,7 +153,7 @@ optional = false
 python-versions = ">=3.6.0"
 
 [package.extras]
-unicode-backport = ["unicodedata2"]
+unicode_backport = ["unicodedata2"]
 
 [[package]]
 name = "click"
@@ -270,6 +290,17 @@ python-versions = ">=3.7"
 docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
 testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
 
+[[package]]
+name = "fqdn"
+version = "1.5.1"
+description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4"
+
+[package.dependencies]
+cached-property = {version = ">=1.3.0", markers = "python_version < \"3.8\""}
+
 [[package]]
 name = "furo"
 version = "2022.9.29"
@@ -450,6 +481,17 @@ python-versions = "*"
 [package.dependencies]
 six = "*"
 
+[[package]]
+name = "isoduration"
+version = "20.11.0"
+description = "Operations with ISO 8601 durations"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+arrow = ">=0.15.0"
+
 [[package]]
 name = "jedi"
 version = "0.18.1"
@@ -552,11 +594,19 @@ python-versions = ">=3.7"
 
 [package.dependencies]
 attrs = ">=17.4.0"
+fqdn = {version = "*", optional = true, markers = "extra == \"format\""}
+idna = {version = "*", optional = true, markers = "extra == \"format\""}
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""}
+isoduration = {version = "*", optional = true, markers = "extra == \"format\""}
+jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""}
 pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""}
 pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2"
+rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""}
+rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""}
 typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+uri-template = {version = "*", optional = true, markers = "extra == \"format\""}
+webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format\""}
 
 [package.extras]
 format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
@@ -655,7 +705,7 @@ typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
 
 [package.extras]
 benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"]
-code-style = ["pre-commit (==2.6)"]
+code_style = ["pre-commit (==2.6)"]
 compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"]
 linkify = ["linkify-it-py (>=1.0,<2.0)"]
 plugins = ["mdit-py-plugins"]
@@ -694,7 +744,7 @@ python-versions = ">=3.7"
 markdown-it-py = ">=1.0.0,<3.0.0"
 
 [package.extras]
-code-style = ["pre-commit"]
+code_style = ["pre-commit"]
 rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"]
 testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
@@ -732,7 +782,7 @@ sphinx = ">=4,<6"
 typing-extensions = "*"
 
 [package.extras]
-code-style = ["pre-commit (>=2.12,<3.0)"]
+code_style = ["pre-commit (>=2.12,<3.0)"]
 linkify = ["linkify-it-py (>=1.0,<2.0)"]
 rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"]
 testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx (<5.2)", "sphinx-pytest"]
@@ -1232,7 +1282,26 @@ urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+description = "A pure python RFC3339 validator"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+six = "*"
+
+[[package]]
+name = "rfc3987"
+version = "1.3.8"
+description = "Parsing and validation of URIs (RFC 3986) and IRIs (RFC 3987)"
+category = "main"
+optional = false
+python-versions = "*"
 
 [[package]]
 name = "ruamel-yaml"
@@ -1505,19 +1574,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
 aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
 asyncio = ["greenlet (!=0.4.17)"]
 asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
-mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
+mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"]
 mssql = ["pyodbc"]
-mssql-pymssql = ["pymssql"]
-mssql-pyodbc = ["pyodbc"]
+mssql_pymssql = ["pymssql"]
+mssql_pyodbc = ["pyodbc"]
 mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
 mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
-mysql-connector = ["mysql-connector-python"]
+mysql_connector = ["mysql-connector-python"]
 oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
 postgresql = ["psycopg2 (>=2.7)"]
-postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
-postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
-postgresql-psycopg2binary = ["psycopg2-binary"]
-postgresql-psycopg2cffi = ["psycopg2cffi"]
+postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
+postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
+postgresql_psycopg2binary = ["psycopg2-binary"]
+postgresql_psycopg2cffi = ["psycopg2cffi"]
 pymysql = ["pymysql", "pymysql (<1)"]
 sqlcipher = ["sqlcipher3_binary"]
 
@@ -1595,6 +1664,17 @@ category = "main"
 optional = false
 python-versions = ">=3.7"
 
+[[package]]
+name = "uri-template"
+version = "1.2.0"
+description = "RFC 6570 URI Template Processor"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["flake8 (<4.0.0)", "flake8-annotations", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-noqa", "flake8-requirements", "flake8-type-annotations", "flake8-use-fstring", "mypy", "pep8-naming"]
+
 [[package]]
 name = "urllib3"
 version = "1.26.12"
@@ -1645,6 +1725,14 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "webcolors"
+version = "1.12"
+description = "A library for working with color names and color values formats defined by HTML and CSS."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
 [[package]]
 name = "webencodings"
 version = "0.5.1"
@@ -1679,7 +1767,7 @@ docs = ["sphinx", "sphinxcontrib-mermaid", "furo"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7.6"
-content-hash = "4a945774b94528edd68cb228fb6352d6ca0db476e2f33a21eec6f3b4b5e39679"
+content-hash = "d36547db42545cbda530354b2b00f0ffb28a44016f6596988cced9c71b51043f"
 
 [metadata.files]
 alabaster = [
@@ -1693,6 +1781,10 @@ appnope = [
     {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
     {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
 ]
+arrow = [
+    {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"},
+    {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"},
+]
 attrs = [
     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
     {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
@@ -1713,6 +1805,10 @@ bleach = [
     {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"},
     {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"},
 ]
+cached-property = [
+    {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
+    {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
+]
 certifi = [
     {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
     {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
@@ -1914,6 +2010,10 @@ filelock = [
     {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
     {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
 ]
+fqdn = [
+    {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"},
+    {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"},
+]
 furo = [
     {file = "furo-2022.9.29-py3-none-any.whl", hash = "sha256:559ee17999c0f52728481dcf6b1b0cf8c9743e68c5e3a18cb45a7992747869a9"},
     {file = "furo-2022.9.29.tar.gz", hash = "sha256:d4238145629c623609c2deb5384f8d036e2a1ee2a101d64b67b4348112470dbd"},
@@ -2019,6 +2119,10 @@ isodate = [
     {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"},
     {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"},
 ]
+isoduration = [
+    {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"},
+    {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"},
+]
 jedi = [
     {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
     {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
@@ -2510,6 +2614,14 @@ requests = [
     {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
+rfc3339-validator = [
+    {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"},
+    {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"},
+]
+rfc3987 = [
+    {file = "rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53"},
+    {file = "rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733"},
+]
 ruamel-yaml = [
     {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
     {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
@@ -2697,6 +2809,10 @@ typing-extensions = [
     {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
     {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
 ]
+uri-template = [
+    {file = "uri_template-1.2.0-py3-none-any.whl", hash = "sha256:f1699c77b73b925cf4937eae31ab282a86dc885c333f2e942513f08f691fc7db"},
+    {file = "uri_template-1.2.0.tar.gz", hash = "sha256:934e4d09d108b70eb8a24410af8615294d09d279ce0e7cbcdaef1bd21f932b06"},
+]
 urllib3 = [
     {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
     {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
@@ -2736,6 +2852,10 @@ wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
     {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
+webcolors = [
+    {file = "webcolors-1.12-py3-none-any.whl", hash = "sha256:d98743d81d498a2d3eaf165196e65481f0d2ea85281463d856b1e51b09f62dce"},
+    {file = "webcolors-1.12.tar.gz", hash = "sha256:16d043d3a08fd6a1b1b7e3e9e62640d09790dce80d2bdd4792a175b35fe794a9"},
+]
 webencodings = [
     {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
     {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
diff --git a/pyproject.toml b/pyproject.toml
index be5070e3..5da85df8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -101,7 +101,7 @@ hbreader = "*"
 isodate = ">=0.6.0"
 jinja2 = ">= 3.1.0"
 jsonasobj2 = "==1.*,>=1.0.0,>=1.0.3"
-jsonschema = ">=3.0.1"
+jsonschema = {extras = ["format"], version = ">=4.0.0"}
 linkml-dataops = "*"
 linkml-runtime = "~1.3"
 myst-parser = "*"


Test patch:
diff --git a/tests/test_generators/input/kitchen_sink.yaml b/tests/test_generators/input/kitchen_sink.yaml
index 12e7aa00..9a11288d 100644
--- a/tests/test_generators/input/kitchen_sink.yaml
+++ b/tests/test_generators/input/kitchen_sink.yaml
@@ -91,9 +91,15 @@ classes:
       - age in years
       - addresses
       - has birth event
+      - species name
+      - stomach count
     slot_usage:
       name:
-        pattern: "^\\S+ \\S+"  ## do not do this in a real schema, people have all kinds of names
+        pattern: "^\\S+ \\S+$"  ## do not do this in a real schema, people have all kinds of names
+      species name:
+        equals_string: human
+      stomach count:
+        equals_number: 1
     id_prefixes:
       - P
     exact_mappings:
@@ -246,6 +252,8 @@ classes:
         range: CodeSystem
         inlined: true
         multivalued: true
+    slots:
+      - metadata
 
   FakeClass:
     deprecated: this is not a real class, we are using it to test deprecation
@@ -341,6 +349,9 @@ slots:
   metadata:
     range: AnyObject
     description: Example of a slot that has an unconstrained range
+  species name:
+  stomach count:
+    range: integer
   # slots added for inheritance tree purposes
   tree_slot_A:
     slot_uri: ks:A
diff --git a/tests/test_generators/input/kitchen_sink_failtest_inst_01.yaml b/tests/test_generators/input/kitchen_sink_compliance_inst_01.yaml
similarity index 65%
rename from tests/test_generators/input/kitchen_sink_failtest_inst_01.yaml
rename to tests/test_generators/input/kitchen_sink_compliance_inst_01.yaml
index 9b31a57a..e54ecee0 100644
--- a/tests/test_generators/input/kitchen_sink_failtest_inst_01.yaml
+++ b/tests/test_generators/input/kitchen_sink_compliance_inst_01.yaml
@@ -1,29 +1,27 @@
+# By default these cases are expected to *not* validate. Use valid: true
+# for cases that are expected to validate successfully.
+
 - description: name fails regex
-  skip: false
   dataset:
     persons:
       - id: P:001
         name: prince ## our schema is deliberately ultra-restrictive
 - description: missing ID
-  skip: false
   dataset:
     persons:
       - name: prince charming
 - description: wrong datatype for age
-  skip: false
   dataset:
     persons:
       - id: P
         name: prince charming
         age: "33"
 - description: wrong datatype for name
-  skip: false
   dataset:
     companies:
       - id: ROR:1
         name: 5
 - description: missing required type field
-  skip: false
   dataset:
     persons:
       - id: P:001
@@ -31,7 +29,6 @@
         has_familial_relationships:
           - related_to: P:002
 - description: wrong enum
-  skip: true ## https://github.com/linkml/linkml/issues/201
   dataset:
     persons:
       - id: P:001
@@ -40,28 +37,25 @@
           - related_to: P:002
             type: NON_EXISTENT_RELATION
 - description: age lower than threshold
-  skip: false
   dataset:
     persons:
       - id: P
         name: benjamin button
         age_in_years: -5
 - description: age higher than threshold
-  skip: false
   dataset:
     persons:
       - id: P
         name: dr who
         age_in_years: 5000
 - description: additional properties is closed
-  skip: false
   dataset:
     persons:
       - id: P
         name: john smith
         made_up_property: "abc"
 - description: optional open properties
-  skip: true
+  skip: not_closed value does not propagate to nested objects. By design?
   valid: true
   closed: false
   dataset:
@@ -70,7 +64,6 @@
         name: john smith
         made_up_property: "abc"
 - description: additional properties is closed on enclosing object
-  skip: false
   issue: https://github.com/linkml/linkml/issues/106
   dataset:
     persons:
@@ -78,7 +71,6 @@
         name: john smith
     made_up_property: "abc"
 - description: non-multivalued address
-  skip: false
   dataset:
     persons:
       - id: P
@@ -87,7 +79,6 @@
           street: 1 foo street
           city: foo city
 - description: valid cardinality
-  skip: false
   valid: true
   dataset:
     persons:
@@ -99,7 +90,6 @@
               id: CODE:D0001
               name: headache
 - description: multivalued value for singlevalued list
-  skip: false
   dataset:
     persons:
       - id: P
@@ -111,7 +101,6 @@
                 id: CODE:D0001
                 name: headache
 - description: multivalued value for singlevalued list
-  skip: false
   dataset:
     persons:
       - id: P
@@ -124,7 +113,6 @@
                 - headache
                 - cough
 - description: string used in place of object
-  skip: false
   dataset:
     persons:
       - id: P
@@ -132,20 +120,70 @@
         addresses:
           - "1 foo street, foo city"
 - description: inline used where reference expected
-  skip: false
   dataset:
     activities:
       - id: A1
         was_associated_with:
           id: Agent:987
+- description: inline used with reference
+  valid: true
+  dataset:
+    activities:
+      - id: A1
+        was_associated_with: Agent:987
+- description: inline as list used where inline as dictionary expected
+  dataset:
+    code_systems:
+      - id: "CODE:1"
+        name: one
+      - id: "CODE:2"
+        name: two
+- description: inline as dictionary used correctly
+  valid: true
+  dataset:
+    code_systems:
+      "CODE:1":
+        name: one
+      "CODE:2":
+        name: two
+- description: inline as dictionary used where inline as list expected
+  dataset:
+    persons:
+      "PERSON:1":
+        name: person one
+      "PERSON:2":
+        name: person two
+- description: inline as list used correctly
+  valid: true
+  dataset:
+    persons:
+      - id: "PERSON:1"
+        name: person one
+      - id: "PERSON:2"
+        name: person two
 - description: date format
-  skip: true
-  comment: :-
-    as per https://json-schema.org/understanding-json-schema/reference/string.html#format
-    "JSON Schema implementations are not required to implement this part of the specification, and many of them do not."
-    it appears the python json schema implementation does not implement this, so we skip for now
   dataset:
     activities:
       - id: A1
         started_at_time: "not a real date"
-
+- description: instance of linkml:Any class can have additional properties
+  valid: true
+  dataset:
+    metadata:
+      something: 1
+      anything: fine
+- description: value does not match slot pattern
+  dataset:
+    persons:
+      - id: P1
+        name: Toomany    Spaces
+- description: value does not match equals_string specification
+  dataset:
+    persons:
+      - id: P2
+        species_name: cat
+- description: value does not match equals_number specification
+  dataset:
+    persons:
+      - id: P3
+        stomach_count: 2
diff --git a/tests/test_generators/output/kitchen_sink_failtest_log.txt b/tests/test_generators/output/kitchen_sink_failtest_log.txt
deleted file mode 100644
index 2eda961c..00000000
--- a/tests/test_generators/output/kitchen_sink_failtest_log.txt
+++ /dev/null
@@ -1,636 +0,0 @@
---------------------
-name fails regex:
-
-{'persons': [{'id': 'P:001', 'name': 'prince'}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	'prince' does not match '^\\S+ \\S+'
-
-Failed validating 'pattern' in schema['properties']['persons']['items']['properties']['name']:
-    {'pattern': '^\\S+ \\S+', 'type': 'string'}
-
-On instance['persons'][0]['name']:
-    'prince'
-
---------------------
-missing ID:
-
-{'persons': [{'name': 'prince charming'}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	'id' is a required property
-
-Failed validating 'required' in schema['properties']['persons']['items']:
-    {'additionalProperties': False,
-     'description': 'A person, living or dead',
-     'properties': {'addresses': {'items': {'$ref': '#/$defs/Address'},
-                                  'type': 'array'},
-                    'age_in_years': {'description': 'number of years since '
-                                                    'birth',
-                                     'maximum': 999,
-                                     'minimum': 0,
-                                     'type': 'integer'},
-                    'aliases': {'items': {'type': 'string'},
-                                'type': 'array'},
-                    'has_birth_event': {'$ref': '#/$defs/BirthEvent'},
-                    'has_employment_history': {'items': {'$ref': '#/$defs/EmploymentEvent'},
-                                               'type': 'array'},
-                    'has_familial_relationships': {'items': {'$ref': '#/$defs/FamilialRelationship'},
-                                                   'type': 'array'},
-                    'has_medical_history': {'items': {'$ref': '#/$defs/MedicalEvent'},
-                                            'type': 'array'},
-                    'id': {'type': 'string'},
-                    'name': {'pattern': '^\\S+ \\S+', 'type': 'string'}},
-     'required': ['id'],
-     'title': 'Person',
-     'type': 'object'}
-
-On instance['persons'][0]:
-    {'name': 'prince charming'}
-
---------------------
-wrong datatype for age:
-
-{'persons': [{'id': 'P', 'name': 'prince charming', 'age': '33'}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	Additional properties are not allowed ('age' was unexpected)
-
-Failed validating 'additionalProperties' in schema['properties']['persons']['items']:
-    {'additionalProperties': False,
-     'description': 'A person, living or dead',
-     'properties': {'addresses': {'items': {'$ref': '#/$defs/Address'},
-                                  'type': 'array'},
-                    'age_in_years': {'description': 'number of years since '
-                                                    'birth',
-                                     'maximum': 999,
-                                     'minimum': 0,
-                                     'type': 'integer'},
-                    'aliases': {'items': {'type': 'string'},
-                                'type': 'array'},
-                    'has_birth_event': {'$ref': '#/$defs/BirthEvent'},
-                    'has_employment_history': {'items': {'$ref': '#/$defs/EmploymentEvent'},
-                                               'type': 'array'},
-                    'has_familial_relationships': {'items': {'$ref': '#/$defs/FamilialRelationship'},
-                                                   'type': 'array'},
-                    'has_medical_history': {'items': {'$ref': '#/$defs/MedicalEvent'},
-                                            'type': 'array'},
-                    'id': {'type': 'string'},
-                    'name': {'pattern': '^\\S+ \\S+', 'type': 'string'}},
-     'required': ['id'],
-     'title': 'Person',
-     'type': 'object'}
-
-On instance['persons'][0]:
-    {'age': '33', 'id': 'P', 'name': 'prince charming'}
-
---------------------
-wrong datatype for name:
-
-{'companies': [{'id': 'ROR:1', 'name': 5}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	5 is not of type 'string'
-
-Failed validating 'type' in schema['properties']['companies']['items']['properties']['name']:
-    {'type': 'string'}
-
-On instance['companies'][0]['name']:
-    5
-
---------------------
-missing required type field:
-
-{'persons': [{'id': 'P:001', 'name': 'jane smith', 'has_familial_relationships': [{'related_to': 'P:002'}]}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	'type' is a required property
-
-Failed validating 'required' in schema['properties']['persons']['items']['properties']['has_familial_relationships']['items']:
-    {'additionalProperties': False,
-     'description': '',
-     'properties': {'ended_at_time': {'format': 'date', 'type': 'string'},
-                    'related_to': {'type': 'string'},
-                    'started_at_time': {'format': 'date', 'type': 'string'},
-                    'type': {'$ref': '#/$defs/FamilialRelationshipType'}},
-     'required': ['type', 'related_to'],
-     'title': 'FamilialRelationship',
-     'type': 'object'}
-
-On instance['persons'][0]['has_familial_relationships'][0]:
-    {'related_to': 'P:002'}
-
---------------------
-wrong enum:
-
-{'persons': [{'id': 'P:001', 'name': 'jane smith', 'has_familial_relationships': [{'related_to': 'P:002', 'type': 'NON_EXISTENT_RELATION'}]}]}:
- SKIPPED
---------------------
-age lower than threshold:
-
-{'persons': [{'id': 'P', 'name': 'benjamin button', 'age_in_years': -5}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	-5 is less than the minimum of 0
-
-Failed validating 'minimum' in schema['properties']['persons']['items']['properties']['age_in_years']:
-    {'description': 'number of years since birth',
-     'maximum': 999,
-     'minimum': 0,
-     'type': 'integer'}
-
-On instance['persons'][0]['age_in_years']:
-    -5
-
---------------------
-age higher than threshold:
-
-{'persons': [{'id': 'P', 'name': 'dr who', 'age_in_years': 5000}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	5000 is greater than the maximum of 999
-
-Failed validating 'maximum' in schema['properties']['persons']['items']['properties']['age_in_years']:
-    {'description': 'number of years since birth',
-     'maximum': 999,
-     'minimum': 0,
-     'type': 'integer'}
-
-On instance['persons'][0]['age_in_years']:
-    5000
-
---------------------
-additional properties is closed:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'made_up_property': 'abc'}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	Additional properties are not allowed ('made_up_property' was unexpected)
-
-Failed validating 'additionalProperties' in schema['properties']['persons']['items']:
-    {'additionalProperties': False,
-     'description': 'A person, living or dead',
-     'properties': {'addresses': {'items': {'$ref': '#/$defs/Address'},
-                                  'type': 'array'},
-                    'age_in_years': {'description': 'number of years since '
-                                                    'birth',
-                                     'maximum': 999,
-                                     'minimum': 0,
-                                     'type': 'integer'},
-                    'aliases': {'items': {'type': 'string'},
-                                'type': 'array'},
-                    'has_birth_event': {'$ref': '#/$defs/BirthEvent'},
-                    'has_employment_history': {'items': {'$ref': '#/$defs/EmploymentEvent'},
-                                               'type': 'array'},
-                    'has_familial_relationships': {'items': {'$ref': '#/$defs/FamilialRelationship'},
-                                                   'type': 'array'},
-                    'has_medical_history': {'items': {'$ref': '#/$defs/MedicalEvent'},
-                                            'type': 'array'},
-                    'id': {'type': 'string'},
-                    'name': {'pattern': '^\\S+ \\S+', 'type': 'string'}},
-     'required': ['id'],
-     'title': 'Person',
-     'type': 'object'}
-
-On instance['persons'][0]:
-    {'id': 'P', 'made_up_property': 'abc', 'name': 'john smith'}
-
---------------------
-optional open properties:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'made_up_property': 'abc'}]}:
- SKIPPED
---------------------
-additional properties is closed on enclosing object:
-
-{'persons': [{'id': 'P', 'name': 'john smith'}], 'made_up_property': 'abc'}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	Additional properties are not allowed ('made_up_property' was unexpected)
-
-Failed validating 'additionalProperties' in schema:
-    {'$defs': {'Activity': {'additionalProperties': False,
-                            'description': 'a provence-generating activity',
-                            'properties': {'description': {'type': 'string'},
-                                           'ended_at_time': {'format': 'date',
-                                                             'type': 'string'},
-                                           'id': {'type': 'string'},
-                                           'started_at_time': {'format': 'date',
-                                                               'type': 'string'},
-                                           'used': {'type': 'string'},
-                                           'was_associated_with': {'type': 'string'},
-                                           'was_informed_by': {'type': 'string'}},
-                            'required': ['id'],
-                            'title': 'Activity',
-                            'type': 'object'},
-               'Address': {'additionalProperties': False,
-                           'description': '',
-                           'properties': {'city': {'type': 'string'},
-                                          'street': {'type': 'string'}},
-                           'required': [],
-                           'title': 'Address',
-                           'type': 'object'},
-               'Agent': {'additionalProperties': False,
-                         'description': 'a provence-generating agent',
-                         'properties': {'acted_on_behalf_of': {'type': 'string'},
-                                        'id': {'type': 'string'},
-                                        'was_informed_by': {'type': 'string'}},
-                         'required': ['id'],
-                         'title': 'Agent',
-                         'type': 'object'},
-               'AnyObject': {'additionalProperties': True,
-                             'description': 'Example of unconstrained '
-                                            'class',
-                             'properties': {},
-                             'required': [],
-                             'title': 'AnyObject',
-                             'type': 'object'},
-               'BirthEvent': {'additionalProperties': False,
-                              'description': '',
-                              'properties': {'ended_at_time': {'format': 'date',
-                                                               'type': 'string'},
-                                             'in_location': {'type': 'string'},
-                                             'is_current': {'type': 'boolean'},
-                                             'metadata': {'$ref': '#/$defs/AnyObject',
-                                                          'description': 'Example '
-                                                                         'of '
-                                                                         'a '
-                                                                         'slot '
-                                                                         'that '
-                                                                         'has '
-                                                                         'an '
-                                                                         'unconstrained '
-                                                                         'range'},
-                                             'started_at_time': {'format': 'date',
-                                                                 'type': 'string'}},
-                              'required': [],
-                              'title': 'BirthEvent',
-                              'type': 'object'},
-               'ClassWithSpaces': {'additionalProperties': False,
-                                   'description': '',
-                                   'properties': {'slot_with_space_1': {'type': 'string'}},
-                                   'required': [],
-                                   'title': 'ClassWithSpaces',
-                                   'type': 'object'},
-               'CodeSystem': {'additionalProperties': False,
-                              'description': '',
-                              'properties': {'id': {'type': 'string'},
-                                             'name': {'type': 'string'}},
-                              'required': ['id'],
-                              'title': 'CodeSystem',
-                              'type': 'object'},
-               'CodeSystem__identifier_optional': {'additionalProperties': False,
-                                                   'description': '',
-                                                   'properties': {'id': {'type': 'string'},
-                                                                  'name': {'type': 'string'}},
-                                                   'required': [],
-                                                   'title': 'CodeSystem',
-                                                   'type': 'object'},
-               'Company': {'additionalProperties': False,
-                           'description': '',
-                           'properties': {'aliases': {'items': {'type': 'string'},
-                                                      'type': 'array'},
-                                          'ceo': {'type': 'string'},
-                                          'id': {'type': 'string'},
-                                          'name': {'type': 'string'}},
-                           'required': ['id'],
-                           'title': 'Company',
-                           'type': 'object'},
-               'Concept': {'additionalProperties': False,
-                           'description': '',
-                           'properties': {'id': {'type': 'string'},
-                                          'in_code_system': {'type': 'string'},
-                                          'name': {'type': 'string'}},
-                           'required': ['id'],
-                           'title': 'Concept',
-                           'type': 'object'},
-               'Dataset': {'additionalProperties': False,
-                           'description': '',
-                           'properties': {'activities': {'items': {'$ref': '#/$defs/Activity'},
-                                                         'type': 'array'},
-                                          'code_systems': {'additionalProperties': {'$ref': '#/$defs/CodeSystem__identifier_optional'}},
-                                          'companies': {'items': {'$ref': '#/$defs/Company'},
-                                                        'type': 'array'},
-                                          'persons': {'items': {'$ref': '#/$defs/Person'},
-                                                      'type': 'array'}},
-                           'required': [],
-                           'title': 'Dataset',
-                           'type': 'object'},
-               'DiagnosisConcept': {'additionalProperties': False,
-                                    'description': '',
-                                    'properties': {'id': {'type': 'string'},
-                                                   'in_code_system': {'type': 'string'},
-                                                   'name': {'type': 'string'}},
-                                    'required': ['id'],
-                                    'title': 'DiagnosisConcept',
-                                    'type': 'object'},
-               'DiagnosisType': {'description': '',
-                                 'enum': ['TODO'],
-                                 'title': 'DiagnosisType',
-                                 'type': 'string'},
-               'EmploymentEvent': {'additionalProperties': False,
-                                   'description': '',
-                                   'properties': {'employed_at': {'type': 'string'},
-                                                  'ended_at_time': {'format': 'date',
-                                                                    'type': 'string'},
-                                                  'is_current': {'type': 'boolean'},
-                                                  'metadata': {'$ref': '#/$defs/AnyObject',
-                                                               'description': 'Example '
-                                                                              'of '
-                                                                              'a '
-                                                                              'slot '
-                                                                              'that '
-                                                                              'has '
-                                                                              'an '
-                                                                              'unconstrained '
-                                                                              'range'},
-                                                  'started_at_time': {'format': 'date',
-                                                                      'type': 'string'},
-                                                  'type': {'$ref': '#/$defs/EmploymentEventType'}},
-                                   'required': [],
-                                   'title': 'EmploymentEvent',
-                                   'type': 'object'},
-               'EmploymentEventType': {'description': 'codes for different '
-                                                      'kinds of '
-                                                      'employment/HR '
-                                                      'related events',
-                                       'enum': ['HIRE',
-                                                'FIRE',
-                                                'PROMOTION',
-                                                'TRANSFER'],
-                                       'title': 'EmploymentEventType',
-                                       'type': 'string'},
-               'Event': {'additionalProperties': False,
-                         'description': '',
-                         'properties': {'ended_at_time': {'format': 'date',
-                                                          'type': 'string'},
-                                        'is_current': {'type': 'boolean'},
-                                        'metadata': {'$ref': '#/$defs/AnyObject',
-                                                     'description': 'Example '
-                                                                    'of a '
-                                                                    'slot '
-                                                                    'that '
-                                                                    'has '
-                                                                    'an '
-                                                                    'unconstrained '
-                                                                    'range'},
-                                        'started_at_time': {'format': 'date',
-                                                            'type': 'string'}},
-                         'required': [],
-                         'title': 'Event',
-                         'type': 'object'},
-               'FakeClass': {'additionalProperties': False,
-                             'description': '',
-                             'properties': {'test_attribute': {'type': 'string'}},
-                             'required': [],
-                             'title': 'FakeClass',
-                             'type': 'object'},
-               'FamilialRelationship': {'additionalProperties': False,
-                                        'description': '',
-                                        'properties': {'ended_at_time': {'format': 'date',
-                                                                         'type': 'string'},
-                                                       'related_to': {'type': 'string'},
-                                                       'started_at_time': {'format': 'date',
-                                                                           'type': 'string'},
-                                                       'type': {'$ref': '#/$defs/FamilialRelationshipType'}},
-                                        'required': ['type', 'related_to'],
-                                        'title': 'FamilialRelationship',
-                                        'type': 'object'},
-               'FamilialRelationshipType': {'description': '',
-                                            'enum': ['SIBLING_OF',
-                                                     'PARENT_OF',
-                                                     'CHILD_OF'],
-                                            'title': 'FamilialRelationshipType',
-                                            'type': 'string'},
-               'MarriageEvent': {'additionalProperties': False,
-                                 'description': '',
-                                 'properties': {'ended_at_time': {'format': 'date',
-                                                                  'type': 'string'},
-                                                'in_location': {'type': 'string'},
-                                                'is_current': {'type': 'boolean'},
-                                                'married_to': {'type': 'string'},
-                                                'metadata': {'$ref': '#/$defs/AnyObject',
-                                                             'description': 'Example '
-                                                                            'of '
-                                                                            'a '
-                                                                            'slot '
-                                                                            'that '
-                                                                            'has '
-                                                                            'an '
-                                                                            'unconstrained '
-                                                                            'range'},
-                                                'started_at_time': {'format': 'date',
-                                                                    'type': 'string'}},
-                                 'required': [],
-                                 'title': 'MarriageEvent',
-                                 'type': 'object'},
-               'MedicalEvent': {'additionalProperties': False,
-                                'description': '',
-                                'properties': {'diagnosis': {'$ref': '#/$defs/DiagnosisConcept'},
-                                               'ended_at_time': {'format': 'date',
-                                                                 'type': 'string'},
-                                               'in_location': {'type': 'string'},
-                                               'is_current': {'type': 'boolean'},
-                                               'metadata': {'$ref': '#/$defs/AnyObject',
-                                                            'description': 'Example '
-                                                                           'of '
-                                                                           'a '
-                                                                           'slot '
-                                                                           'that '
-                                                                           'has '
-                                                                           'an '
-                                                                           'unconstrained '
-                                                                           'range'},
-                                               'procedure': {'$ref': '#/$defs/ProcedureConcept'},
-                                               'started_at_time': {'format': 'date',
-                                                                   'type': 'string'}},
-                                'required': [],
-                                'title': 'MedicalEvent',
-                                'type': 'object'},
-               'Organization': {'additionalProperties': False,
-                                'description': '',
-                                'properties': {'aliases': {'items': {'type': 'string'},
-                                                           'type': 'array'},
-                                               'id': {'type': 'string'},
-                                               'name': {'type': 'string'}},
-                                'required': ['id'],
-                                'title': 'Organization',
-                                'type': 'object'},
-               'OtherCodes': {'description': '',
-                              'enum': ['a b'],
-                              'title': 'OtherCodes',
-                              'type': 'string'},
-               'Person': {'additionalProperties': False,
-                          'description': 'A person, living or dead',
-                          'properties': {'addresses': {'items': {'$ref': '#/$defs/Address'},
-                                                       'type': 'array'},
-                                         'age_in_years': {'description': 'number '
-                                                                         'of '
-                                                                         'years '
-                                                                         'since '
-                                                                         'birth',
-                                                          'maximum': 999,
-                                                          'minimum': 0,
-                                                          'type': 'integer'},
-                                         'aliases': {'items': {'type': 'string'},
-                                                     'type': 'array'},
-                                         'has_birth_event': {'$ref': '#/$defs/BirthEvent'},
-                                         'has_employment_history': {'items': {'$ref': '#/$defs/EmploymentEvent'},
-                                                                    'type': 'array'},
-                                         'has_familial_relationships': {'items': {'$ref': '#/$defs/FamilialRelationship'},
-                                                                        'type': 'array'},
-                                         'has_medical_history': {'items': {'$ref': '#/$defs/MedicalEvent'},
-                                                                 'type': 'array'},
-                                         'id': {'type': 'string'},
-                                         'name': {'pattern': '^\\S+ \\S+',
-                                                  'type': 'string'}},
-                          'required': ['id'],
-                          'title': 'Person',
-                          'type': 'object'},
-               'Place': {'additionalProperties': False,
-                         'description': '',
-                         'properties': {'aliases': {'items': {'type': 'string'},
-                                                    'type': 'array'},
-                                        'id': {'type': 'string'},
-                                        'name': {'type': 'string'}},
-                         'required': ['id'],
-                         'title': 'Place',
-                         'type': 'object'},
-               'ProcedureConcept': {'additionalProperties': False,
-                                    'description': '',
-                                    'properties': {'id': {'type': 'string'},
-                                                   'in_code_system': {'type': 'string'},
-                                                   'name': {'type': 'string'}},
-                                    'required': ['id'],
-                                    'title': 'ProcedureConcept',
-                                    'type': 'object'},
-               'Relationship': {'additionalProperties': False,
-                                'description': '',
-                                'properties': {'ended_at_time': {'format': 'date',
-                                                                 'type': 'string'},
-                                               'related_to': {'type': 'string'},
-                                               'started_at_time': {'format': 'date',
-                                                                   'type': 'string'},
-                                               'type': {'type': 'string'}},
-                                'required': [],
-                                'title': 'Relationship',
-                                'type': 'object'},
-               'SubclassTest': {'additionalProperties': False,
-                                'description': '',
-                                'properties': {'slot_with_space_1': {'type': 'string'},
-                                               'slot_with_space_2': {'$ref': '#/$defs/ClassWithSpaces'}},
-                                'required': [],
-                                'title': 'SubclassTest',
-                                'type': 'object'}},
-     '$id': 'https://w3id.org/linkml/tests/kitchen_sink',
-     '$schema': 'http://json-schema.org/draft-07/schema#',
-     'additionalProperties': False,
-     'metamodel_version': '1.7.0',
-     'properties': {'activities': {'items': {'$ref': '#/$defs/Activity'},
-                                   'type': 'array'},
-                    'code_systems': {'additionalProperties': {'$ref': '#/$defs/CodeSystem__identifier_optional'}},
-                    'companies': {'items': {'$ref': '#/$defs/Company'},
-                                  'type': 'array'},
-                    'persons': {'items': {'$ref': '#/$defs/Person'},
-                                'type': 'array'}},
-     'required': [],
-     'title': 'kitchen_sink',
-     'type': 'object',
-     'version': None}
-
-On instance:
-    {'made_up_property': 'abc',
-     'persons': [{'id': 'P', 'name': 'john smith'}]}
-
---------------------
-non-multivalued address:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'addresses': {'street': '1 foo street', 'city': 'foo city'}}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	{'street': '1 foo street', 'city': 'foo city'} is not of type 'array'
-
-Failed validating 'type' in schema['properties']['persons']['items']['properties']['addresses']:
-    {'items': {'$ref': '#/$defs/Address'}, 'type': 'array'}
-
-On instance['persons'][0]['addresses']:
-    {'city': 'foo city', 'street': '1 foo street'}
-
---------------------
-valid cardinality:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'has_medical_history': [{'in_location': 'GEO:1234', 'diagnosis': {'id': 'CODE:D0001', 'name': 'headache'}}]}]}:
-No Exception (expected=True: {'description': 'valid cardinality', 'skip': False, 'valid': True, 'dataset': {'persons': [{'id': 'P', 'name': 'john smith', 'has_medical_history': [{'in_location': 'GEO:1234', 'diagnosis': {'id': 'CODE:D0001', 'name': 'headache'}}]}]}}--------------------
-multivalued value for singlevalued list:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'has_medical_history': [{'in_location': 'GEO:1234', 'diagnosis': [{'id': 'CODE:D0001', 'name': 'headache'}]}]}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	[{'id': 'CODE:D0001', 'name': 'headache'}] is not of type 'object'
-
-Failed validating 'type' in schema['properties']['persons']['items']['properties']['has_medical_history']['items']['properties']['diagnosis']:
-    {'additionalProperties': False,
-     'description': '',
-     'properties': {'id': {'type': 'string'},
-                    'in_code_system': {'type': 'string'},
-                    'name': {'type': 'string'}},
-     'required': ['id'],
-     'title': 'DiagnosisConcept',
-     'type': 'object'}
-
-On instance['persons'][0]['has_medical_history'][0]['diagnosis']:
-    [{'id': 'CODE:D0001', 'name': 'headache'}]
-
---------------------
-multivalued value for singlevalued list:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'has_medical_history': [{'in_location': 'GEO:1234', 'diagnosis': {'id': 'CODE:D0001', 'name': ['headache', 'cough']}}]}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	['headache', 'cough'] is not of type 'string'
-
-Failed validating 'type' in schema['properties']['persons']['items']['properties']['has_medical_history']['items']['properties']['diagnosis']['properties']['name']:
-    {'type': 'string'}
-
-On instance['persons'][0]['has_medical_history'][0]['diagnosis']['name']:
-    ['headache', 'cough']
-
---------------------
-string used in place of object:
-
-{'persons': [{'id': 'P', 'name': 'john smith', 'addresses': ['1 foo street, foo city']}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	'1 foo street, foo city' is not of type 'object'
-
-Failed validating 'type' in schema['properties']['persons']['items']['properties']['addresses']['items']:
-    {'additionalProperties': False,
-     'description': '',
-     'properties': {'city': {'type': 'string'},
-                    'street': {'type': 'string'}},
-     'required': [],
-     'title': 'Address',
-     'type': 'object'}
-
-On instance['persons'][0]['addresses'][0]:
-    '1 foo street, foo city'
-
---------------------
-inline used where reference expected:
-
-{'activities': [{'id': 'A1', 'was_associated_with': {'id': 'Agent:987'}}]}:
-
-vException (expected=False: <class 'jsonschema.exceptions.ValidationError'>:
-	{'id': 'Agent:987'} is not of type 'string'
-
-Failed validating 'type' in schema['properties']['activities']['items']['properties']['was_associated_with']:
-    {'type': 'string'}
-
-On instance['activities'][0]['was_associated_with']:
-    {'id': 'Agent:987'}
-
---------------------
-date format:
-
-{'activities': [{'id': 'A1', 'started_at_time': 'not a real date'}]}:
- SKIPPED
diff --git a/tests/test_generators/output/kitchen_sink_inst_01.json b/tests/test_generators/output/kitchen_sink_inst_01.json
deleted file mode 100644
index d151bb0f..00000000
--- a/tests/test_generators/output/kitchen_sink_inst_01.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
-  "persons": [
-    {
-      "id": "P:001",
-      "name": "fred bloggs",
-      "age_in_years": 33
-    },
-    {
-      "id": "P:002",
-      "name": "joe schmoe",
-      "has_employment_history": [
-        {
-          "started_at_time": "2019-01-01",
-          "is_current": true,
-          "employed_at": "ROR:1"
-        }
-      ],
-      "has_familial_relationships": [
-        {
-          "related_to": "P:001",
-          "type": "SIBLING_OF"
-        }
-      ],
-      "has_medical_history": [
-        {
-          "started_at_time": "2019-01-01",
-          "in_location": "GEO:1234",
-          "diagnosis": {
-            "id": "CODE:D0001",
-            "name": "headache",
-            "in_code_system": "DiseaseCodes"
-          },
-          "procedure": {
-            "id": "CODE:P0001",
-            "name": "trepanation",
-            "in_code_system": "ProcedureCodes"
-          }
-        }
-      ],
-      "addresses": [
-        {
-          "street": "1 foo street",
-          "city": "foo city"
-        }
-      ]
-    }
-  ],
-  "companies": [
-    {
-      "id": "ROR:1",
-      "name": "foo"
-    }
-  ],
-  "activities": [
-    {
-      "id": "A:1",
-      "started_at_time": "2019-01-01",
-      "was_associated_with": "Agent:987"
-    }
-  ],
-  "code_systems": {
-    "DiseaseCodes": {
-      "id": "DiseaseCodes",
-      "name": "disease codes"
-    },
-    "ProcedureCodes": {
-      "id": "ProcedureCodes",
-      "name": "disease codes"
-    }
-  },
-  "@type": "Dataset"
-}
\ No newline at end of file
diff --git a/tests/test_generators/test_jsonschemagen.py b/tests/test_generators/test_jsonschemagen.py
index 891f9c36..2e2cd4ef 100644
--- a/tests/test_generators/test_jsonschemagen.py
+++ b/tests/test_generators/test_jsonschemagen.py
@@ -1,23 +1,19 @@
 import json
+import logging
 import unittest
 
+import jsonschema
 import yaml
-from linkml_runtime.dumpers import json_dumper, rdf_dumper
+from linkml_runtime.dumpers import json_dumper
 from linkml_runtime.loaders import yaml_loader
 
 from linkml.generators.jsonschemagen import JsonSchemaGenerator
-from linkml.utils.validation import validate_object
 from tests.test_generators.environment import env
 from tests.test_generators.test_pythongen import make_python
 
 SCHEMA = env.input_path("kitchen_sink.yaml")
-JSONSCHEMA_OUT = env.expected_path("kitchen_sink.schema.json")
-PYTHON = env.expected_path("kitchen_sink.py")
 DATA = env.input_path("kitchen_sink_inst_01.yaml")
-FAILDATA = env.input_path("kitchen_sink_failtest_inst_01.yaml")
-DATA_JSON = env.expected_path("kitchen_sink_inst_01.json")
-DATA_RDF = env.expected_path("kitchen_sink_inst_01.rdf")
-FAILLOG = env.expected_path("kitchen_sink_failtest_log.txt")
+COMPLIANCE_CASES = env.input_path("kitchen_sink_compliance_inst_01.yaml")
 
 
 class JsonSchemaTestCase(unittest.TestCase):
@@ -25,8 +21,29 @@ class JsonSchemaTestCase(unittest.TestCase):
     Tests generation of JSON-Schema
     """
 
-    def test_jsonschema(self):
-        """json schema"""
+    def setUp(self):
+        generator = JsonSchemaGenerator(
+            SCHEMA, 
+            mergeimports=True, 
+            top_class="Dataset", 
+            not_closed=False
+        )
+        self.kitchen_sink_json_schema = json.loads(generator.serialize())
+
+        generator.not_closed = True
+        self.kitchen_sink_json_schema_not_closed = json.loads(generator.serialize())
+
+
+    def test_jsonschema_integration(self):
+        """Integration test for JsonSchemaGenerator.
+        
+        This test loads an instance object adhering to the kitchen sink schema from
+        a YAML file (input/kitchen_sink_inst_01.yaml), performs sanity checks on the
+        instance data, constructs a JsonSchemaGenerator from the kitchen sink schema, 
+        and uses the jsonschema library to verify that the generated JSON Schema is 
+        able to validate the instance data.
+        """
+
         kitchen_module = make_python(False)
         inst: Dataset
         inst = yaml_loader.load(DATA, target_class=kitchen_module.Dataset)
@@ -36,11 +53,11 @@ class JsonSchemaTestCase(unittest.TestCase):
         ok_metadata = True
         for p in inst.persons:
             for a in p.addresses:
-                print(f"{p.id} address = {a.street}")
+                logging.debug(f"{p.id} address = {a.street}")
                 if a.street.startswith("1 foo"):
                     ok_address = True
             for h in p.has_medical_history:
-                print(f"{p.id} history = {h}")
+                logging.debug(f"{p.id} history = {h}")
                 if h.in_location == "GEO:1234" and h.diagnosis.name == "headache":
                     ok_history = True
                 # test the metadata slot, which has an unconstrained range
@@ -49,61 +66,109 @@ class JsonSchemaTestCase(unittest.TestCase):
                         ok_metadata = True
         assert ok_address
         assert ok_history
-        json_dumper.dump(element=inst, to_file=DATA_JSON)
-        gen = JsonSchemaGenerator(SCHEMA, mergeimports=True, top_class="Dataset")
-        jsonschemastr = gen.serialize()
-        with open(JSONSCHEMA_OUT, "w") as io:
-            io.write(jsonschemastr)
-        validate_object(inst, SCHEMA, closed=True)
-
-        # test for https://github.com/linkml/linkml/issues/579
-        jso = json.loads(jsonschemastr)
-        assert "$defs" in jso
-        any_obj = jso["$defs"]["AnyObject"]
-        assert any_obj["additionalProperties"]
-
-        # test for expected failures
-        # FAILDATA contains a set of json objects each of which
-        # violates the schema in some way
-        with open(FAILDATA, "r") as io:
-            failobjs = yaml.load(io, Loader=yaml.loader.SafeLoader)
-
-        with open(FAILLOG, "w") as log:
-            for failobj in failobjs:
-                dataset = failobj["dataset"]
-                is_skip = failobj.get("skip", False)
-                expected_to_be_valid = failobj.get("valid", False)
-                closed = failobj.get("closed", True)
-                log.write("-" * 20)
-                log.write(f"\n{failobj['description']}:\n")
-                log.write(f"\n{failobj['dataset']}:\n")
-                if not is_skip:
-                    # with self.assertRaises(Exception) as e:
-                    #    jsonschema.validate(dataset, schema=jsonschema_obj)
-                    validated = False
-                    try:
-                        validate_object(
-                            dataset,
-                            SCHEMA,
-                            target_class=kitchen_module.Dataset,
-                            closed=closed,
-                        )
-                        validated = True
-                        log.write(
-                            f"No Exception (expected={expected_to_be_valid}: {failobj}"
-                        )
-                    except Exception as e:
-                        validated = False
-                        log.write(
-                            f"\nvException (expected={expected_to_be_valid}: {type(e)}:\n\t{e}\n\n"
-                        )
-                    if expected_to_be_valid:
-                        assert validated
-                    else:
-                        assert not validated
+
+        json_instance = json.loads(json_dumper.dumps(inst))
+        del json_instance['@type']
+
+        jsonschema.validate(json_instance, self.kitchen_sink_json_schema)
+        
+
+    def test_class_uri_any(self):
+        """Test that class_ur: linkml:Any results in a JSON Schema with 
+        "additionalProperties": true.
+        
+        See also https://github.com/linkml/linkml/issues/579
+        """
+
+        self.assertIn("$defs", self.kitchen_sink_json_schema)
+        self.assertTrue(self.kitchen_sink_json_schema["$defs"]["AnyObject"]["additionalProperties"])
+
+
+    def test_compliance_cases(self):
+        """Tests various validation compliance cases.
+
+        The file input/kitchen_sink_compliance_inst_01.yaml has multiple documents describing various
+        compliance test cases. Minimally each document contains a `dataset` object which will be
+        validated against the kitchen sink schema. By default each instance is expected to *fail*
+        validation, but cases can also be marked with valid: true if validation should pass.
+        """
+        with open(COMPLIANCE_CASES, "r") as io:
+            cases = yaml.load(io, Loader=yaml.loader.SafeLoader)
+
+        for case in cases:
+            with self.subTest(msg=case['description']):
+                skip_reason = case.get("skip", None)
+                if skip_reason is not None:
+                    self.skipTest(skip_reason)
+
+                dataset = case["dataset"]
+                expected_valid = case.get("valid", False)
+                
+                if case.get("closed", True):
+                    schema = self.kitchen_sink_json_schema
+                else:
+                    schema = self.kitchen_sink_json_schema_not_closed
+                
+                do_validate = lambda: jsonschema.validate(dataset, schema, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER)
+                if expected_valid:
+                    # this will raise an exception and fail the test if the 
+                    # instance does *not* validate
+                    do_validate()
                 else:
-                    log.write(" SKIPPED\n")
+                    self.assertRaises(jsonschema.ValidationError, do_validate)
+
+    def test_type_inheritance(self):
+        # TODO: can this be a compliance case too?
+        schema = """
+id: http://example.org/test_type_inheritance
+name: test_type_inheritance
+
+types:
+  alpha:
+    base: double 
+  beta:
+    typeof: alpha
+
+slots:
+  alpha_slot:
+    range: alpha
+  beta_slot:
+    range: beta
+
+classes:
+  Test:
+    slots:
+      - alpha_slot
+      - beta_slot
+"""
+        generator = JsonSchemaGenerator(schema, top_class="Test")
+        json_schema = json.loads(generator.serialize())
+        test_def_properties = json_schema['$defs']['Test']['properties']
+        self.assertEqual(test_def_properties['alpha_slot']['type'], 'number')
+        self.assertEqual(test_def_properties['beta_slot']['type'], 'number')
+
+    def test_top_class_identifier(self):
+        """Test that an identifier slot on the top_class becomes a required
+        property in the JSON Schema."""
+
+        schema = """
+id: http://example.org/test_top_class_identifier
+name: test_top_class_identifier
+
+slots:
+  id:
+    identifier: true
+
+classes:
+  Test:
+    slots:
+      - id
+"""
+        generator = JsonSchemaGenerator(schema, top_class="Test")
+        json_schema = json.loads(generator.serialize())
 
+        self.assertIn("id", json_schema["$defs"]["Test"]["required"])
+        self.assertIn("id", json_schema["required"])
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_generators/test_pythongen.py b/tests/test_generators/test_pythongen.py
index 8068e83f..20bc597d 100644
--- a/tests/test_generators/test_pythongen.py
+++ b/tests/test_generators/test_pythongen.py
@@ -59,7 +59,7 @@ class PythonGenTestCase(unittest.TestCase):
         self.assertEqual(
             "Person(id='P:1', name=None, has_employment_history=[EmploymentEvent(started_at_time=None, "
             "ended_at_time=None, is_current=None, metadata=None, employed_at='ROR:1', type=None)], has_familial_relationships=[], "
-            "has_medical_history=[], age_in_years=None, addresses=[], has_birth_event=None, aliases=[])",
+            "has_medical_history=[], age_in_years=None, addresses=[], has_birth_event=None, species_name=None, stomach_count=None, aliases=[])",
             str(p),
         )
 
diff --git a/tests/test_utils/input/schema_with_inference.yaml b/tests/test_utils/input/schema_with_inference.yaml
index c2751ba4..c87f730d 100644
--- a/tests/test_utils/input/schema_with_inference.yaml
+++ b/tests/test_utils/input/schema_with_inference.yaml
@@ -85,6 +85,7 @@ classes:
       - description
       - is_juvenile
       - age_category
+      - summary
     slot_usage:
       description:
         string_serialization: |-