Repository: linkml/linkml

Issue:
"exactly_one_of" generates erroneous JSON-schema
**Describe the bug**
The generation of the JSON-schema corresponding a schema with a `exactly_one_of` slot/attribute is wrong.

**To reproduce**
Steps to reproduce the behavior:
1. Create the schema `test.schema.yaml` provided in the section *Additional context*
2. Generate the corresponding JSON schema: `gen-json-schema test.schema.yaml` (see result in the *Additional context* section)
3. See error in "oneOf" keys

**Expected behavior**
A JSON-Schema `oneOf` gets generated.

**Additional context**

```
# test.schema.yaml
id: http://example.org/pattern
name: pattern

imports:
  - https://w3id.org/linkml/types

slots:
  phone:
    exactly_one_of:
      - range: AmericanPhone
      - range: GermanPhone

classes:
  Test:
    tree_root: true
    slots:
      - phone

  AmericanPhone:
    attributes:
      phone_number:
        range: string
        pattern: \d{3} \d{3} \d{4}

  GermanPhone:
    attributes:
      phone_number:
        range: DePhoneNumber

types:
  DePhoneNumber:
    uri: xsd:String
    base: str
    pattern: 0\d{3}-\d{8}
```

Resulting JSON-schema:
```
{
    "$defs": {
        "AmericanPhone": {
            "additionalProperties": false,
            "description": "",
            "properties": {
                "phone_number": {
                    "pattern": "\\d{3} \\d{3} \\d{4}",
                    "type": "string"
                }
            },
            "title": "AmericanPhone",
            "type": "object"
        },
        "GermanPhone": {
            "additionalProperties": false,
            "description": "",
            "properties": {
                "phone_number": {
                    "type": "string"
                }
            },
            "title": "GermanPhone",
            "type": "object"
        },
        "Test": {
            "additionalProperties": false,
            "description": "",
            "properties": {
                "phone": {
                    "oneOf": [
                        {},
                        {}
                    ],
                    "type": "string"
                }
            },
            "title": "Test",
            "type": "object"
        }
    },
    "$id": "http://example.org/pattern",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "additionalProperties": true,
    "metamodel_version": "1.7.0",
    "properties": {
        "phone": {
            "oneOf": [
                {},
                {}
            ],
            "type": "string"
        }
    },
    "title": "pattern",
    "type": "object",
    "version": null
}
```

Discussion:

Patch:
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 90ebf851..29f0200b 100644
--- a/linkml/generators/oocodegen.py
+++ b/linkml/generators/oocodegen.py
@@ -5,7 +5,6 @@ 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
@@ -128,37 +127,6 @@ 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 219db9db..ecc7eef4 100644
--- a/linkml/generators/pydanticgen.py
+++ b/linkml/generators/pydanticgen.py
@@ -70,11 +70,8 @@ class {{ e.name }}(str, Enum):
     {{ e.description }}
     \"\"\"
     {%- endif %}
-    {% for _, pv in e['values'].items() -%}
-    {% if pv.description -%}
-    # {{pv.description}}
-    {%- endif %}
-    {{pv.label}} = "{{pv.value}}"
+    {% for label, value in e['values'].items() -%}
+    {{label}} = "{{value}}"
     {% endfor %}
     {% if not e['values'] -%}
     dummy = "dummy"
@@ -133,7 +130,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: {t.name} // {t}")
+        raise Exception(f"No python type for range: {s.range} // {t}")
     return pyrange
 
 
@@ -174,6 +171,22 @@ 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
@@ -459,6 +472,8 @@ 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())
diff --git a/linkml/generators/typescriptgen.py b/linkml/generators/typescriptgen.py
index 5ff82360..776a4816 100644
--- a/linkml/generators/typescriptgen.py
+++ b/linkml/generators/typescriptgen.py
@@ -9,21 +9,18 @@ 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.generators.oocodegen import OOCodeGenerator
-from linkml.utils.generator import shared_arguments
+from linkml.utils.generator import Generator, shared_arguments
 
 type_map = {
     "str": "string",
@@ -37,29 +34,10 @@ 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 -%}
 /**
@@ -81,7 +59,7 @@ export interface {{gen.name(c)}} {%- if parents %} extends {{parents|join(', ')}
 
 
 @dataclass
-class TypescriptGenerator(OOCodeGenerator):
+class TypescriptGenerator(Generator):
     """
     Generates typescript from a schema
     """
@@ -93,16 +71,13 @@ class TypescriptGenerator(OOCodeGenerator):
     uses_schemaloader = False
 
     def serialize(self) -> str:
-        """Serialize a schema to typescript string"""
-
-        sv: SchemaView = self.schemaview
-        enums = self.generate_enums(sv.all_enums())
+        """
+        Serialize a schema to typescript string
+        :return:
+        """
         template_obj = Template(default_template)
         out_str = template_obj.render(
-            gen=self,
-            schema=self.schemaview.schema,
-            view=self.schemaview,
-            enums=enums,
+            gen=self, schema=self.schemaview.schema, view=self.schemaview
         )
         return out_str
 
@@ -191,8 +166,6 @@ class TypescriptGenerator(OOCodeGenerator):
             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_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 90fa79a9..2d4e0409 100644
--- a/tests/test_generators/test_pydanticgen.py
+++ b/tests/test_generators/test_pydanticgen.py
@@ -1,4 +1,5 @@
 import unittest
+from collections import namedtuple
 
 import yaml
 from linkml_runtime import SchemaView
@@ -95,13 +96,13 @@ enums:
         assert enums
         enum = enums["TestEnum"]
         assert enum
-        assert enum["values"]["number_123"]["value"] == "123"
-        assert enum["values"]["PLUS_SIGN"]["value"] == "+"
+        assert enum["values"]["number_123"] == "123"
+        assert enum["values"]["PLUS_SIGN"] == "+"
         assert (
-            enum["values"]["This_AMPERSAND_that_plus_maybe_a_TOP_HAT"]["value"]
+            enum["values"]["This_AMPERSAND_that_plus_maybe_a_TOP_HAT"]
             == "This & that, plus maybe a 🎩"
         )
-        assert enum["values"]["Ohio"]["value"] == "Ohio"
+        assert enum["values"]["Ohio"] == "Ohio"
 
     def test_pydantic_any_of(self):
         # TODO: convert to SchemaBuilder and parameterize?
diff --git a/tests/test_generators/test_typescriptgen.py b/tests/test_generators/test_typescriptgen.py
index a2dc9845..0740a0b4 100644
--- a/tests/test_generators/test_typescriptgen.py
+++ b/tests/test_generators/test_typescriptgen.py
@@ -1,6 +1,5 @@
 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
@@ -37,6 +36,7 @@ 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,38 +48,5 @@ 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: