Repository: linkml/linkml

Issue:
docgen: Make it easier (or default) to have a hierarchical layout for index page
NMDC does this with a custom index.md.jinja2:

https://github.com/microbiomedata/nmdc-schema/blob/main/src/doc-templates/index.md.jinja2

compare with the default

https://github.com/linkml/linkml/blob/main/linkml/generators/docgen/index.md.jinja2

Hierarchical looks better for many schemas:
https://microbiomedata.github.io/nmdc-schema/

But if other schemas want to adopt then they will need to fork index.md.jinja2 with manual syncs with upstream if anything changes.

No one should need to make their own jinjas unless they have a highly bespoke layout, e.g. mixs


Discussion:

Patch:
diff --git a/linkml/generators/docgen.py b/linkml/generators/docgen.py
index 1307fc23..b8ce4a5b 100644
--- a/linkml/generators/docgen.py
+++ b/linkml/generators/docgen.py
@@ -139,6 +139,7 @@ class DocGenerator(Generator):
     gen_slots: bool = field(default_factory=lambda: True)
     no_types_dir: bool = field(default_factory=lambda: False)
     use_slot_uris: bool = field(default_factory=lambda: False)
+    hierarchical_class_view: bool = field(default_factory=lambda: False)
 
 
     def __post_init__(self):
@@ -882,13 +883,18 @@ class DocGenerator(Generator):
     default=False,
     help="Use IDs from slot_uri instead of names",
 )
+@click.option(
+    "--hierarchical-class-view/--no-hierarchical-class-view",
+    default=True,
+    help="Render class table on index page in a hierarchically indented view",
+)
 @click.option(
     "--example-directory",
     help="Folder in which example files are found. These are used to make inline examples"
 )
 @click.version_option(__version__, "-V", "--version")
 @click.command()
-def cli(yamlfile, directory, dialect, template_directory, use_slot_uris, **args):
+def cli(yamlfile, directory, dialect, template_directory, use_slot_uris, hierarchical_class_view, **args):
     """Generate documentation folder from a LinkML YAML schema
 
     Currently a default set of templates for markdown is provided (see the folder linkml/generators/docgen/)
@@ -912,6 +918,7 @@ def cli(yamlfile, directory, dialect, template_directory, use_slot_uris, **args)
         dialect=dialect,
         template_directory=template_directory,
         use_slot_uris=use_slot_uris,
+        hierarchical_class_view=hierarchical_class_view,
         **args,
     )
     print(gen.serialize())
diff --git a/linkml/generators/docgen/index.md.jinja2 b/linkml/generators/docgen/index.md.jinja2
index d4f8d0f4..2a78e4c4 100644
--- a/linkml/generators/docgen/index.md.jinja2
+++ b/linkml/generators/docgen/index.md.jinja2
@@ -1,8 +1,9 @@
 # {% if schema.title %}{{ schema.title }}{% else %}{{ schema.name }}{% endif %}
 
-{{ schema.description }}
+{% if schema.description %}{{ schema.description }}{% endif %}
 
 URI: {{ schema.id }}
+
 Name: {{ schema.name }}
 
 {% if include_top_level_diagram %}
@@ -18,9 +19,15 @@ Name: {{ schema.name }}
 
 | Class | Description |
 | --- | --- |
+{% if gen.hierarchical_class_view -%}
+{% for u, v in gen.class_hierarchy_as_tuples() -%}
+| {{ "&nbsp;"|safe*u*8 }}{{ gen.link(schemaview.get_class(v)) }} | {{ schemaview.get_class(v).description }} |
+{% endfor %}
+{% else -%}
 {% for c in gen.all_class_objects()|sort(attribute=sort_by) -%}
 | {{gen.link(c)}} | {{c.description|enshorten}} |
 {% endfor %}
+{% endif %}
 
 ## Slots
 
diff --git a/linkml/generators/jsonschemagen.py b/linkml/generators/jsonschemagen.py
index 4d6158f3..6c24089e 100644
--- a/linkml/generators/jsonschemagen.py
+++ b/linkml/generators/jsonschemagen.py
@@ -317,15 +317,17 @@ class JsonSchemaGenerator(Generator):
             (typ, fmt) = json_schema_types.get(schema_type.base.lower(), ("string", None))
         elif slot.range in self.schemaview.all_enums().keys():
             reference = slot.range
-        elif (slot_is_inlined or parent_slot_is_inlined) and slot.range in self.schemaview.all_classes().keys():
-            descendants = [desc for desc in self.schemaview.class_descendants(slot.range) 
-                if not self.schemaview.get_class(desc).abstract]
-            if descendants and self.include_range_class_descendants:
-                reference = descendants
+        elif slot.range in self.schemaview.all_classes().keys():
+            if slot_is_inlined or parent_slot_is_inlined:
+                descendants = [desc for desc in self.schemaview.class_descendants(slot.range) 
+                    if not self.schemaview.get_class(desc).abstract]
+                if descendants and self.include_range_class_descendants:
+                    reference = descendants
+                else:
+                    reference = slot.range
             else:
-                reference = slot.range
-        else:
-            typ = "string"
+                id_slot = self.schemaview.get_identifier_slot(slot.range)
+                typ = id_slot.range or "string"
 
         return (typ, fmt, reference)
     
@@ -342,84 +344,58 @@ class JsonSchemaGenerator(Generator):
         return constraints
 
     def get_subschema_for_slot(self, slot: SlotDefinition, omit_type: bool = False) -> JsonSchema:
-        slot_has_range_union = slot.any_of is not None and len(slot.any_of) > 0 and all(s.range is not None for s in slot.any_of)
-        if omit_type:
-            prop = JsonSchema()
-        else:
-            slot_is_inlined = self.schemaview.is_inlined(slot)
-
-            if slot_has_range_union:
-                items = []
-                for sub_slot in slot.any_of:
-                    typ, fmt, reference = self.get_type_info_for_slot_subschema(sub_slot, slot_is_inlined)
-                    if reference is not None:
-                        item = JsonSchema.ref_for(reference)
-                    elif fmt is None:
-                        item = JsonSchema({"type": typ})
-                    else:
-                        item = JsonSchema({"type": typ, "format": fmt})
-                    items.append(item)
-                subschema = JsonSchema({
-                    "anyOf": items
-                })
-                if slot.multivalued:
-                    prop = JsonSchema.array_of(subschema)
-                else:
-                    prop = subschema
-            else:
-                typ, fmt, reference = self.get_type_info_for_slot_subschema(slot, slot_is_inlined)
-                if slot_is_inlined:
-                    # If inline we have to include redefined slots
-                    if slot.multivalued:
-                        range_id_slot, range_simple_dict_value_slot, range_required_slots = self._get_range_associated_slots(slot)
-                        # if the range class has an ID and the slot is not inlined as a list, then we need to consider
-                        # various inlined as dict formats
-                        if range_id_slot is not None and not slot.inlined_as_list:
-                            # At a minimum, the inlined dict can have keys (additionalProps) that are IDs
-                            # and the values are the range class but possibly omitting the ID.
-                            additionalProps = [JsonSchema.ref_for(reference, identifier_optional=True)]
-
-                            # If the range can be collected as a simple dict, then we can also accept the value
-                            # of that simple dict directly.
-                            if range_simple_dict_value_slot is not None:
-                                additionalProps.append(self.get_subschema_for_slot(range_simple_dict_value_slot))
-
-                            # If the range has no required slots, then null is acceptable
-                            if len(range_required_slots) == 0:
-                                additionalProps.append(JsonSchema({"type": "null"}))
-
-                            # If through the above logic we identified multiple acceptable forms, then wrap them
-                            # in an "anyOf", otherwise just take the only acceptable form
-                            if len(additionalProps) == 1:
-                                additionalProps = additionalProps[0]
-                            else:
-                                additionalProps = JsonSchema({
-                                    "anyOf": additionalProps
-                                })
-                            prop = JsonSchema({
-                                "type": "object",
-                                "additionalProperties": additionalProps
-                            })
-                            self.top_level_schema.add_lax_def(reference, self.aliased_slot_name(range_id_slot))
+        prop = JsonSchema()
+        slot_is_multivalued = 'multivalued' in slot and slot.multivalued
+        slot_is_inlined = self.schemaview.is_inlined(slot)
+        if not omit_type:
+            typ, fmt, reference = self.get_type_info_for_slot_subschema(slot, slot_is_inlined)
+            if slot_is_inlined:
+                # If inline we have to include redefined slots
+                if slot_is_multivalued:
+                    range_id_slot, range_simple_dict_value_slot, range_required_slots = self._get_range_associated_slots(slot)
+                    # if the range class has an ID and the slot is not inlined as a list, then we need to consider
+                    # various inlined as dict formats
+                    if range_id_slot is not None and not slot.inlined_as_list:
+                        # At a minimum, the inlined dict can have keys (additionalProps) that are IDs
+                        # and the values are the range class but possibly omitting the ID.
+                        additionalProps = [JsonSchema.ref_for(reference, identifier_optional=True)]
+
+                        # If the range can be collected as a simple dict, then we can also accept the value
+                        # of that simple dict directly.
+                        if range_simple_dict_value_slot is not None:
+                            additionalProps.append(self.get_subschema_for_slot(range_simple_dict_value_slot))
+
+                        # If the range has no required slots, then null is acceptable
+                        if len(range_required_slots) == 0:
+                            additionalProps.append(JsonSchema({"type": "null"}))
+
+                        # If through the above logic we identified multiple acceptable forms, then wrap them
+                        # in an "anyOf", otherwise just take the only acceptable form
+                        if len(additionalProps) == 1:
+                            additionalProps = additionalProps[0]
                         else:
-                            prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
+                            additionalProps = JsonSchema({
+                                "anyOf": additionalProps
+                            })
+                        prop = JsonSchema({
+                            "type": "object",
+                            "additionalProperties": additionalProps
+                        })
+                        self.top_level_schema.add_lax_def(reference, self.aliased_slot_name(range_id_slot))
                     else:
-                        prop = JsonSchema.ref_for(reference)
+                        prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
                 else:
-                    if slot.multivalued:
-                        if reference is not None:
-                            prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
-                        elif fmt is None:
-                            prop = JsonSchema.array_of(JsonSchema({"type": typ}))
-                        else:
-                            prop = JsonSchema.array_of(JsonSchema({"type": typ, "format": fmt}))
-                    else:
-                        if reference is not None:
-                            prop = JsonSchema.ref_for(reference)
-                        elif fmt is None:
-                            prop = JsonSchema({"type": typ})
-                        else:
-                            prop = JsonSchema({"type": typ, "format": fmt})
+                    prop = JsonSchema.ref_for(reference)
+            else:
+                if reference is not None:
+                    prop = JsonSchema.ref_for(reference)
+                elif typ and fmt is None:
+                    prop = JsonSchema({"type": typ})
+                elif typ:
+                    prop = JsonSchema({"type": typ, "format": fmt})
+
+                if slot_is_multivalued:
+                    prop = JsonSchema.array_of(prop)
 
         prop.add_keyword('description', slot.description)
 
@@ -441,21 +417,30 @@ class JsonSchemaGenerator(Generator):
             prop.add_keyword('minProperties', slot.minimum_cardinality)
             prop.add_keyword('maxProperties', slot.maximum_cardinality)
 
+        bool_subschema = JsonSchema()
         if slot.any_of is not None and len(slot.any_of) > 0:
-            if not slot_has_range_union:
-                prop['anyOf'] = [self.get_subschema_for_slot(s, omit_type=True) for s in slot.any_of]
+            bool_subschema['anyOf'] = [self.get_subschema_for_slot(s) for s in slot.any_of]
 
         if slot.all_of is not None and len(slot.all_of) > 0:
-            prop['allOf'] = [self.get_subschema_for_slot(s, omit_type=True) for s in slot.all_of]
+            bool_subschema['allOf'] = [self.get_subschema_for_slot(s) for s in slot.all_of]
 
         if slot.exactly_one_of is not None and len(slot.exactly_one_of) > 0:
-            prop['oneOf'] = [self.get_subschema_for_slot(s, omit_type=True) for s in slot.exactly_one_of]
+            bool_subschema['oneOf'] = [self.get_subschema_for_slot(s) for s in slot.exactly_one_of]
 
         if slot.none_of is not None and len(slot.none_of) > 0:
-            prop['not'] = {
-                'anyOf': [self.get_subschema_for_slot(s, omit_type=True) for s in slot.none_of]
+            bool_subschema['not'] = {
+                'anyOf': [self.get_subschema_for_slot(s) for s in slot.none_of]
             }
 
+        if bool_subschema:
+            if prop.is_array:
+                if 'items' not in prop:
+                    prop['items'] = {}
+                prop["type"] = "array"
+                prop['items'].update(bool_subschema)
+            else:
+                prop.update(bool_subschema)
+
         return prop
 
 
diff --git a/linkml/generators/oocodegen.py b/linkml/generators/oocodegen.py
index 29f0200b..90ebf851 100644
--- a/linkml/generators/oocodegen.py
+++ b/linkml/generators/oocodegen.py
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
 from typing import List, Optional, Dict
 
 from linkml_runtime.linkml_model.meta import (ClassDefinition,
+                                              EnumDefinition, EnumDefinitionName,
                                               SchemaDefinition, SlotDefinition,
                                               TypeDefinition)
 from linkml_runtime.utils.formatutils import camelcase, lcamelcase, underscore
@@ -127,6 +128,37 @@ class OOCodeGenerator(Generator):
 
             return safe_label
 
+    def generate_enums(
+        self, all_enums: Dict[EnumDefinitionName, EnumDefinition]
+    ) -> Dict:
+        # TODO: make an explicit class to represent how an enum is passed to the template
+        enums = {}
+        for enum_name, enum_original in all_enums.items():
+            enum = {
+                "name": camelcase(enum_name),
+                "values": {}
+            }
+
+            if hasattr(enum_original, "description"):
+                enum["description"] = enum_original.description
+
+            for pv in enum_original.permissible_values.values():
+                label = self.generate_enum_label(pv.text)
+                val = {
+                    "label": label,
+                    "value": pv.text.replace('"', '\\"')
+                }
+                if hasattr(pv, "description"):
+                    val["description"] = pv.description
+                else :
+                    val["description"] = None
+
+                enum["values"][label] = val
+                
+            enums[enum_name] = enum
+
+        return enums
+
     def create_documents(self) -> List[OODocument]:
         """
         Currently hardcoded for java-style
diff --git a/linkml/generators/pydanticgen.py b/linkml/generators/pydanticgen.py
index ecc7eef4..ed753d9b 100644
--- a/linkml/generators/pydanticgen.py
+++ b/linkml/generators/pydanticgen.py
@@ -70,8 +70,11 @@ class {{ e.name }}(str, Enum):
     {{ e.description }}
     \"\"\"
     {%- endif %}
-    {% for label, value in e['values'].items() -%}
-    {{label}} = "{{value}}"
+    {% for _, pv in e['values'].items() -%}
+    {% if pv.description -%}
+    # {{pv.description}}
+    {%- endif %}
+    {{pv.label}} = "{{pv.value}}"
     {% endfor %}
     {% if not e['values'] -%}
     dummy = "dummy"
@@ -130,7 +133,7 @@ def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
     if pyrange is None and t.typeof is not None:
         pyrange = _get_pyrange(sv.get_type(t.typeof), sv)
     if pyrange is None:
-        raise Exception(f"No python type for range: {s.range} // {t}")
+        raise Exception(f"No python type for range: {t.name} // {t}")
     return pyrange
 
 
@@ -171,22 +174,6 @@ class PydanticGenerator(OOCodeGenerator):
             logging.error(f"Error compiling generated python code: {e}")
             raise e
 
-    def generate_enums(
-        self, all_enums: Dict[EnumDefinitionName, EnumDefinition]
-    ) -> Dict[str, dict]:
-        # TODO: make an explicit class to represent how an enum is passed to the template
-        enums = {}
-        for enum_name, enum_original in all_enums.items():
-            enum = {"name": camelcase(enum_name), "values": {}}
-
-            for pv in enum_original.permissible_values.values():
-                label = self.generate_enum_label(pv.text)
-                enum["values"][label] = pv.text.replace('"', '\\"')
-
-            enums[enum_name] = enum
-
-        return enums
-
     def sort_classes(self, clist: List[ClassDefinition]) -> List[ClassDefinition]:
         """
         sort classes such that if C is a child of P then C appears after P in the list
@@ -472,8 +459,6 @@ class PydanticGenerator(OOCodeGenerator):
         return None
 
     def serialize(self) -> str:
-        sv = self.schemaview
-
         if self.template_file is not None:
             with open(self.template_file) as template_file:
                 template_obj = Template(template_file.read())
@@ -523,7 +508,9 @@ class PydanticGenerator(OOCodeGenerator):
 
                 slot_ranges: List[str] = []
 
-                if len(s.any_of) > 0 and s.range is not None:
+                # Confirm that the original slot range (ignoring the default that comes in from
+                # induced_slot) isn't in addition to setting any_of
+                if len(s.any_of) > 0 and sv.get_slot(sn).range is not None:
                     raise ValueError("Slot cannot have both range and any_of defined")
 
                 if s.any_of is not None and len(s.any_of) > 0:
diff --git a/linkml/generators/typescriptgen.py b/linkml/generators/typescriptgen.py
index 776a4816..5ff82360 100644
--- a/linkml/generators/typescriptgen.py
+++ b/linkml/generators/typescriptgen.py
@@ -9,18 +9,21 @@ from typing import (Callable, Dict, Iterator, List, Optional, Set, TextIO,
 import click
 from jinja2 import Environment, FileSystemLoader, Template
 from linkml_runtime.dumpers import yaml_dumper
-from linkml_runtime.linkml_model.meta import (Annotation, ClassDefinition,
-                                              ClassDefinitionName, Definition,
-                                              DefinitionName, Element,
-                                              EnumDefinition, SchemaDefinition,
-                                              SlotDefinition,
-                                              SlotDefinitionName,
-                                              TypeDefinition)
+from linkml_runtime.linkml_model.meta import (
+    Annotation, ClassDefinition,
+    ClassDefinitionName, Definition,
+    DefinitionName, Element,
+    EnumDefinition, SchemaDefinition,
+    SlotDefinition,
+    SlotDefinitionName,
+    TypeDefinition
+)
 from linkml_runtime.utils.formatutils import camelcase, underscore
 from linkml_runtime.utils.schemaview import SchemaView
 
 from linkml._version import __version__
-from linkml.utils.generator import Generator, shared_arguments
+from linkml.generators.oocodegen import OOCodeGenerator
+from linkml.utils.generator import shared_arguments
 
 type_map = {
     "str": "string",
@@ -34,10 +37,29 @@ default_template = """
 {%- for c in view.all_classes().values() -%}
 {%- set cref = gen.classref(c) -%}
 {% if cref -%}
-export type {{cref}} = string
+export type {{cref}} = string;
 {% endif -%}
 {%- endfor -%}
 
+{% for e in enums.values() %}
+{%- if e.description -%}
+/**
+* {{e.description}}
+*/
+{%- endif %}
+export enum {{e.name}} {
+    {% if not e.values -%}
+    dummy = "dummy"
+    {%- endif %}
+    {%- for _, pv in e['values'].items() %}
+    {% if pv.description -%}
+    /** {{pv.description}} */
+    {% endif -%}
+    {{pv.label}} = "{{pv.value}}",    
+    {%- endfor %}
+};
+{% endfor %}
+
 {% for c in view.all_classes().values() -%}
 {%- if c.description -%}
 /**
@@ -59,7 +81,7 @@ export interface {{gen.name(c)}} {%- if parents %} extends {{parents|join(', ')}
 
 
 @dataclass
-class TypescriptGenerator(Generator):
+class TypescriptGenerator(OOCodeGenerator):
     """
     Generates typescript from a schema
     """
@@ -71,13 +93,16 @@ class TypescriptGenerator(Generator):
     uses_schemaloader = False
 
     def serialize(self) -> str:
-        """
-        Serialize a schema to typescript string
-        :return:
-        """
+        """Serialize a schema to typescript string"""
+
+        sv: SchemaView = self.schemaview
+        enums = self.generate_enums(sv.all_enums())
         template_obj = Template(default_template)
         out_str = template_obj.render(
-            gen=self, schema=self.schemaview.schema, view=self.schemaview
+            gen=self,
+            schema=self.schemaview.schema,
+            view=self.schemaview,
+            enums=enums,
         )
         return out_str
 
@@ -166,6 +191,8 @@ class TypescriptGenerator(Generator):
             parents = []
         return [ClassDefinitionName(camelcase(p)) for p in parents + cls.mixins]
 
+    def default_value_for_type(self, typ: str) -> str:
+        pass
 
 @shared_arguments(TypescriptGenerator)
 @click.version_option(__version__, "-V", "--version")


Test patch:
diff --git a/tests/test_generators/input/jsonschema_class_inheritance.yaml b/tests/test_generators/input/jsonschema_class_inheritance.yaml
new file mode 100644
index 00000000..258fa896
--- /dev/null
+++ b/tests/test_generators/input/jsonschema_class_inheritance.yaml
@@ -0,0 +1,74 @@
+schema:
+  id: http://example.org/test_class_inheritance
+  name: test_class_inheritance
+
+  slots:
+    alpha_value:
+      range: string
+      required: true
+    gamma_value:
+      range: string
+      required: true
+    alpha_obj:
+      range: Alpha
+    beta_obj:
+      range: Beta
+    gamma_obj:
+      range: Gamma
+
+  classes:
+    Alpha:
+      slots:
+        - alpha_value
+    Beta:
+      is_a: Alpha
+    Gamma:
+      is_a: Beta
+      slots:
+        - gamma_value
+    Test:
+      tree_root: true
+      slots:
+        - alpha_obj
+        - beta_obj
+        - gamma_obj
+
+json_schema:
+  properties:
+    alpha_obj:
+      anyOf: 
+        - "$ref": "#/$defs/Alpha"
+        - "$ref": "#/$defs/Beta"
+        - "$ref": "#/$defs/Gamma"
+    beta_obj:
+      anyOf: 
+        - "$ref": "#/$defs/Beta"
+        - "$ref": "#/$defs/Gamma"
+    gamma_obj:
+      anyOf: 
+        - "$ref": "#/$defs/Gamma"
+
+data_cases:
+  - data:
+      alpha_obj:
+        alpha_value: v1
+  - data:
+      alpha_obj:
+        alpha_value: v1
+        gamma_value: v2
+  - data:
+      beta_obj:
+        alpha_value: v1
+  - data:
+      beta_obj:
+        alpha_value: v1
+        gamma_value: v2
+  - data:
+      gamma_obj:
+        alpha_value: v1
+    error_message: "'gamma_value' is a required property"
+  - data:
+      gamma_obj:
+        alpha_value: v1
+        gamma_value: v2
+  
\ No newline at end of file
diff --git a/tests/test_generators/input/jsonschema_collection_forms.yaml b/tests/test_generators/input/jsonschema_collection_forms.yaml
index c2ad0872..056fd2e6 100644
--- a/tests/test_generators/input/jsonschema_collection_forms.yaml
+++ b/tests/test_generators/input/jsonschema_collection_forms.yaml
@@ -6,8 +6,11 @@ schema:
   default_range: string
 
   slots:
+    int_key:
+      identifier: true
+      range: integer
     key:
-      key: true
+      identifier: true
     value:
     value2:
     key_value_pairs:
@@ -18,6 +21,22 @@ schema:
       range: MoreThanOneNonKeySlot
       multivalued: true
       inlined: true
+    not_inlined_key_value_pairs:
+      range: KeyValuePair
+      multivalued: true
+      inlined: false
+    not_inlined_int_key_value_pairs:
+      range: IntKeyValuePair
+      multivalued: true
+      inlined: false
+    inlined_as_list_key_value_pair:
+      range: KeyValuePair
+      multivalued: true
+      inlined_as_list: true
+    inlined_as_list_int_key_value_pair:
+      range: IntKeyValuePair
+      multivalued: true
+      inlined_as_list: true
 
   classes:
     Test:
@@ -25,15 +44,28 @@ schema:
       slots:
         - key_value_pairs
         - more_than_one_non_key_slots
+        - not_inlined_key_value_pairs
+        - not_inlined_int_key_value_pairs
+        - inlined_as_list_key_value_pair
+        - inlined_as_list_int_key_value_pair
     KeyValuePair:
       slots:
         - key
         - value
+    IntKeyValuePair:
+      slots:
+        - int_key
+        - value
     MoreThanOneNonKeySlot:
       slots:
         - key
         - value
         - value2
+    IntMoreThanOneNonKeySlot:
+      slots:
+        - int_key
+        - value
+        - value2
 
 data_cases:
   - data:
@@ -77,3 +109,50 @@ data_cases:
         k1: v1
         k2: v2  
     error_message: "not valid under any of the given schemas"
+  - data:
+      not_inlined_key_value_pairs:
+        - k1
+        - k2
+  - data:
+      not_inlined_key_value_pairs:
+        k1: v1
+        k2: v2
+    error_message: "not of type 'array'"
+  - data:
+      not_inlined_int_key_value_pairs:
+        - 1
+        - 2
+  - data:
+      not_inlined_int_key_value_pairs:
+        - k1
+        - k2
+    error_message: "not of type 'integer'"
+  - data:
+      not_inlined_int_key_value_pairs:
+        1: v1
+        2: v2
+    error_message: "not of type 'array'"
+  - data:
+      inlined_as_list_key_value_pair:
+        - key: k1
+          value: v1
+        - key: k2
+          value: k2
+  - data:
+      inlined_as_list_key_value_pair:
+        k1: v1
+        k2: v2
+    error_message: "not of type 'array'"
+  - data:
+      inlined_as_list_int_key_value_pair:
+        - int_key: 1
+          value: v1
+        - int_key: 2
+          value: k2
+  - data:
+      inlined_as_list_int_key_value_pair:
+        1: 
+          value: v1
+        2: 
+          value: v2
+    error_message: "not of type 'array'"
diff --git a/tests/test_generators/input/jsonschema_range_union_cases.yaml b/tests/test_generators/input/jsonschema_range_union_cases.yaml
index 8df55dc8..fe815b38 100644
--- a/tests/test_generators/input/jsonschema_range_union_cases.yaml
+++ b/tests/test_generators/input/jsonschema_range_union_cases.yaml
@@ -54,6 +54,18 @@ schema:
       any_of:
         - range: C1
         - range: C2
+    c1_xor_c2_single_value:
+      multivalued: false
+      inlined: true
+      exactly_one_of:
+        - range: C1
+        - range: C2
+    c1_xor_c2_multi_value:
+      multivalued: true
+      inlined_as_list: true
+      exactly_one_of:
+        - range: C1
+        - range: C2
     grab_bag:
       multivalued: true
       inlined_as_list: true
@@ -102,6 +114,8 @@ schema:
         - c1_mutli_value
         - c1_or_c2_single_value
         - c1_or_c2_multi_value
+        - c1_xor_c2_single_value
+        - c1_xor_c2_multi_value
         - grab_bag
 
 data_cases:
@@ -135,6 +149,13 @@ data_cases:
       c1_or_c2_multi_value:
         - s1: hi
         - s2: hello
+  - data:
+      c1_xor_c2_single_value:
+        s2: hi
+  - data:
+      c1_xor_c2_multi_value:
+        - s1: hi
+        - s2: hello
   - data:
       grab_bag:
         - s1: two
@@ -222,6 +243,24 @@ data_cases:
         - s2: 'hello'
         - s3: 'hola'
     error_message: Failed validating 'anyOf' in schema
+  - data:
+      c1_xor_c2_single_value:
+        - s1: 'hi'
+    error_message: Failed validating 'oneOf' in schema
+  - data:
+      c1_xor_c2_single_value:
+        s3: 'hi'
+    error_message: Failed validating 'oneOf' in schema
+  - data:
+      c1_xor_c2_multi_value:
+        s1: 'hi'
+    error_message: is not of type 'array'
+  - data:
+      c1_xor_c2_multi_value:
+        - s1: 'hi'
+        - s2: 'hello'
+        - s3: 'hola'
+    error_message: Failed validating 'oneOf' in schema
   - data:
       grab_bag:
         - s2: 'two'
diff --git a/tests/test_generators/input/jsonschema_value_constraints.yaml b/tests/test_generators/input/jsonschema_value_constraints.yaml
index 50806f45..16bcd3fd 100644
--- a/tests/test_generators/input/jsonschema_value_constraints.yaml
+++ b/tests/test_generators/input/jsonschema_value_constraints.yaml
@@ -46,6 +46,12 @@ schema:
       none_of:
         - equals_string: purple
         - equals_string: green
+    integer_over_ten_or_string_na:
+      exactly_one_of:
+        - range: integer
+          minimum_value: 10
+        - range: string
+          pattern: "[nN]/?[aA]"
 
   classes:
     Test:
@@ -61,6 +67,7 @@ schema:
         - string_all_patterns
         - string_exactly_one_pattern
         - string_none_of
+        - integer_over_ten_or_string_na
 
 data_cases:
   - data:
@@ -130,3 +137,16 @@ data_cases:
   - data: 
       string_none_of: green
     error_message: should not be valid under
+  - data:
+      integer_over_ten_or_string_na: 100
+  - data:
+      integer_over_ten_or_string_na: 5
+    error_message: 5 is less than the minimum
+  - data:
+      integer_over_ten_or_string_na: n/a
+  - data:
+      integer_over_ten_or_string_na: wrong
+    error_message: "'wrong' does not match"
+  - data:
+      integer_over_ten_or_string_na: false
+    error_message: False is not valid under any of the given schemas
diff --git a/tests/test_generators/test_docgen.py b/tests/test_generators/test_docgen.py
index 3f44c629..a291a06d 100644
--- a/tests/test_generators/test_docgen.py
+++ b/tests/test_generators/test_docgen.py
@@ -625,6 +625,21 @@ class DocGeneratorTestCase(unittest.TestCase):
                 
         shutil.rmtree(md_temp_dir)
 
+    def test_hierarchical_class_view(self):
+        """Test to check if class table view on index page follows hierarchical view"""
+        gen = DocGenerator(SCHEMA, mergeimports=True, hierarchical_class_view=True)
+
+        md_temp_dir = tempfile.mkdtemp()
+
+        md = gen.serialize(directory=md_temp_dir)
+
+        assert_mdfile_contains("index.md", "Event", after="Dataset")
+        
+        assert_mdfile_contains("index.md", "BirthEvent", after="Event")
+
+        assert_mdfile_contains("index.md", "EmploymentEvent", after="BirthEvent")
+        
+        assert_mdfile_contains("index.md", "MarriageEvent", after="EmploymentEvent")
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_generators/test_jsonschemagen.py b/tests/test_generators/test_jsonschemagen.py
index 62e93b75..3678ff61 100644
--- a/tests/test_generators/test_jsonschemagen.py
+++ b/tests/test_generators/test_jsonschemagen.py
@@ -131,6 +131,11 @@ class JsonSchemaTestCase(unittest.TestCase):
 
         self.externalFileTest("jsonschema_type_inheritance.yaml")
 
+    def test_class_inheritance(self):
+        """Tests that a class definition's is_a slot is correctly accounted for."""
+
+        self.externalFileTest("jsonschema_class_inheritance.yaml", {'not_closed': False, 'include_range_class_descendants': True})
+
     def test_top_class_identifier(self):
         """Test that an identifier slot on the top_class becomes a required
         property in the JSON Schema."""
@@ -138,25 +143,13 @@ class JsonSchemaTestCase(unittest.TestCase):
         self.externalFileTest("jsonschema_top_class_identifier.yaml")
 
     def test_value_constraints(self):
-        with open(env.input_path("jsonschema_value_constraints.yaml")) as f:
-            test_def = yaml.safe_load(f)
-
-        generator = JsonSchemaGenerator(
-            yaml.dump(test_def["schema"]), stacktrace=True, not_closed=False
-        )
-        json_schema = json.loads(generator.serialize())
-
-        for data_case in test_def.get("data_cases", []):
-            data = data_case["data"]
-            with self.subTest(data=data):
-                if "error_message" in data_case:
-                    self.assertRaisesRegex(
-                        jsonschema.ValidationError,
-                        data_case["error_message"],
-                        lambda: jsonschema.validate(data, json_schema),
-                    )
-                else:
-                    jsonschema.validate(data, json_schema)
+        """Test the translation of metaslots that constrain values.
+        
+        Tests the translation of equals_string, equals_number, pattern, maximum_value,
+        and minimum_value metaslots. Additionally contains one test case that verifies
+        these work with multiple ranges as well.
+        """
+        self.externalFileTest("jsonschema_value_constraints.yaml")
 
     def test_rules(self):
         """Tests translation of various types of class rules.
@@ -246,7 +239,7 @@ class JsonSchemaTestCase(unittest.TestCase):
     #
     # **********************************************************
 
-    def externalFileTest(self, file: str) -> None:
+    def externalFileTest(self, file: str, generator_args={'not_closed': False}) -> None:
         with open(env.input_path(file)) as f:
             test_definition = yaml.safe_load(f)
 
@@ -254,6 +247,7 @@ class JsonSchemaTestCase(unittest.TestCase):
             yaml.dump(test_definition["schema"]),
             test_definition.get("json_schema", {}),
             test_definition.get("data_cases", []),
+            generator_args=generator_args
         )
 
     def assertSchemaValidates(
@@ -261,9 +255,11 @@ class JsonSchemaTestCase(unittest.TestCase):
         schema: Union[str, SchemaDefinition],
         expected_json_schema_subset={},
         data_cases=[],
+        generator_args={},
     ):
-        generator = JsonSchemaGenerator(schema)
+        generator = JsonSchemaGenerator(schema, **generator_args)
         json_schema = json.loads(generator.serialize())
+        print(generator.serialize())
 
         self.assertDictSubset(expected_json_schema_subset, json_schema)
 
diff --git a/tests/test_generators/test_pydanticgen.py b/tests/test_generators/test_pydanticgen.py
index 2d4e0409..1842703c 100644
--- a/tests/test_generators/test_pydanticgen.py
+++ b/tests/test_generators/test_pydanticgen.py
@@ -1,5 +1,4 @@
 import unittest
-from collections import namedtuple
 
 import yaml
 from linkml_runtime import SchemaView
@@ -96,13 +95,13 @@ enums:
         assert enums
         enum = enums["TestEnum"]
         assert enum
-        assert enum["values"]["number_123"] == "123"
-        assert enum["values"]["PLUS_SIGN"] == "+"
+        assert enum["values"]["number_123"]["value"] == "123"
+        assert enum["values"]["PLUS_SIGN"]["value"] == "+"
         assert (
-            enum["values"]["This_AMPERSAND_that_plus_maybe_a_TOP_HAT"]
+            enum["values"]["This_AMPERSAND_that_plus_maybe_a_TOP_HAT"]["value"]
             == "This & that, plus maybe a 🎩"
         )
-        assert enum["values"]["Ohio"] == "Ohio"
+        assert enum["values"]["Ohio"]["value"] == "Ohio"
 
     def test_pydantic_any_of(self):
         # TODO: convert to SchemaBuilder and parameterize?
@@ -110,7 +109,7 @@ enums:
 id: test_schema
 name: test_info
 description: just testing
-
+default_range: string
 prefixes:
   linkml: https://w3id.org/linkml/
   schema: http://schema.org/
diff --git a/tests/test_generators/test_typescriptgen.py b/tests/test_generators/test_typescriptgen.py
index 0740a0b4..a2dc9845 100644
--- a/tests/test_generators/test_typescriptgen.py
+++ b/tests/test_generators/test_typescriptgen.py
@@ -1,5 +1,6 @@
 import unittest
 
+from linkml_runtime import SchemaView
 from linkml_runtime.linkml_model import SlotDefinition
 from linkml.generators.typescriptgen import TypescriptGenerator
 from linkml.utils.schema_builder import SchemaBuilder
@@ -36,7 +37,6 @@ class TypescriptGeneratorTestCase(unittest.TestCase):
         assert("id: string" in tss)
         assert("description?: string" in tss)
 
-
     def test_mutlivalued_string(self):
         """ Test that multivalued string slots are generated as string arrays """
 
@@ -48,5 +48,38 @@ class TypescriptGeneratorTestCase(unittest.TestCase):
         tss = TypescriptGenerator(schema, mergeimports=True).serialize()
         assert("aliases?: string[]" in tss)
 
+    def test_enums(self):
+        unit_test_schema = """
+id: unit_test
+name: unit_test
+
+prefixes:
+  ex: https://example.org/
+default_prefix: ex
+
+enums:
+  TestEnum:
+    permissible_values:
+      123:
+      +:
+      This & that, plus maybe a 🎩:
+      Ohio:
+"""
+
+        sv = SchemaView(unit_test_schema)
+        gen = TypescriptGenerator(schema=unit_test_schema)
+        enums = gen.generate_enums(sv.all_enums())
+        assert enums
+        enum = enums["TestEnum"]
+        assert enum
+        assert enum["values"]["number_123"]["value"] == "123"
+        assert enum["values"]["PLUS_SIGN"]["value"] == "+"
+        assert (
+            enum["values"]["This_AMPERSAND_that_plus_maybe_a_TOP_HAT"]["value"]
+            == "This & that, plus maybe a 🎩"
+        )
+        assert enum["values"]["Ohio"]["value"] == "Ohio"
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/test_issues/input/issue_177.yaml b/tests/test_issues/input/issue_177.yaml
index a8389975..03dac7db 100644
--- a/tests/test_issues/input/issue_177.yaml
+++ b/tests/test_issues/input/issue_177.yaml
@@ -8,6 +8,7 @@ types:
     base: int
     uri: xsd:int
 
+default_range: string
 
 classes:
   c1: