Repository: linkml/linkml

Issue:
Date slots are rendered as String fields in Java
**Describe the bug**
When producing Java code from a LinkML schema with `gen-java`, slots with a `date` range are rendered as fields with a `String` type, instead of a more appropriate type representing a date (such as [LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html)).

**To reproduce**
Steps to reproduce the behavior:
1. Create a minimal schema such as the following:

```yaml
id: http://example.com/person
name: person
prefixes:
  linkml: https://w3id.org/linkml/
imports:
  - linkml:types

classes:
  Person:
    attributes:
      dob:
        range: date
```

2. Generate Java code for this schema:

```sh
$ gen-java person.yaml
```

3. Observe that in the generated `Person.java` file, the `dob` private variable in the `Person` class has type `String`:

```java
package None;

import java.util.List;
import lombok.*;

@Data
@EqualsAndHashCode(callSuper=false)
public class Person  {

  private String dob;

}
``` 

**Expected behavior**
I expect slots with a `date` range to be translated into Java with a type suitable to represent dates.

**Screenshots**
N/A

**About your computer (if applicable, please complete the following information):**
 - OS: Tested with macOS 13.4.1 and Slackware Linux 15.0
 - Browser: irrelevant
 - Version: Tested with linkml-1.4.4

**Additional context**
I believe the problem lies in [lines 48–50](https://github.com/linkml/linkml/blob/fa359e617889291dc7c6ed26e6e823a42f298c29/linkml/generators/javagen.py#L48) of `linkml/generators/javagen.py`:


```python
TYPEMAP = {
    "xsd:string": "String",
    "xsd:integer": "Integer",
    "xsd:float": "Float",
    "xsd:double": "Double",
    "xsd:boolean": "boolean",
    "xds:dateTime": "ZonedDateTime",
    "xds:date": "LocalDateTime",
    "xds:time": "Instant",
    "xsd:anyURI": "String",
    "xsd:decimal": "BigDecimal",
}
```

Note the typos: `xds:` instead of `xsd:` for the `dateTime`, `date`, and `time` types.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/linkml/linkml/pull/1710?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`598376c`)](https://app.codecov.io/gh/linkml/linkml/commit/598376ce7f8c11bd3cf31f0ca7e3d5c34770021a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml) 79.86% compared to head [(`40c0202`)](https://app.codecov.io/gh/linkml/linkml/pull/1710?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml) 79.63%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1710      +/-   ##
==========================================
- Coverage   79.86%   79.63%   -0.24%     
==========================================
  Files          98       98              
  Lines       11185    11101      -84     
  Branches     2865     2811      -54     
==========================================
- Hits         8933     8840      -93     
- Misses       1716     1721       +5     
- Partials      536      540       +4     
```


| [Files](https://app.codecov.io/gh/linkml/linkml/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml) | Coverage Δ | |
|---|---|---|
| [linkml/generators/javagen.py](https://app.codecov.io/gh/linkml/linkml/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml#diff-bGlua21sL2dlbmVyYXRvcnMvamF2YWdlbi5weQ==) | `80.59% <ø> (-0.29%)` | :arrow_down: |

... and [19 files with indirect coverage changes](https://app.codecov.io/gh/linkml/linkml/pull/1710/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/linkml/linkml/pull/1710?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkml).


Patch:
diff --git a/docs/howtos/collaborative-development.md b/docs/howtos/collaborative-development.md
new file mode 100644
index 00000000..ace2f538
--- /dev/null
+++ b/docs/howtos/collaborative-development.md
@@ -0,0 +1,185 @@
+# How to run a collaborative data modeling project
+
+This document describes how to run a collaborative data modeling project using the [LinkML](https://linkml.io) modeling language.
+
+This guide is intended to serve as a practical how-to for running a collaborative data modeling project,
+adaptable to the diverse needs of different projects.
+Whether you're tackling a small, focused endeavor or a large-scale, multi-organizational initiative,
+the principles and practices outlined here can be tailored to your project's unique requirements.
+
+Our aim is to provide a straightforward, actionable set of guidelines that balance the technical aspects
+of data modeling with the nuances of collaborative work.
+You'll find insights on initiating and planning your project, managing contributions,
+and maintaining effective communication and governance.
+
+Remember, the success of a data modeling project hinges on a well-coordinated blend of technical
+proficiency and collaborative synergy.
+This guide is designed to equip you with the essentials for navigating these complexities efficiently.
+
+This guide is a work in progress, and in the spirit of the guide itself, we welcome comments ([issue 1744](https://github.com/linkml/linkml/issues/1744))
+and pull requests.
+
+## Sources
+
+In this guide we draw from:
+
+- The O3 principles: [Open code, open data, and open infrastructure to promote the longevity of curated scientific resources](https://osf.io/vuzt3/)
+- The [OBook](https://oboacademy.github.io/obook), and in particular [How to be an Open Science Engineer - maximising impact for a better world](https://oboacademy.github.io/obook/howto/open-science-engineer/)
+- The Tislab collaboration guide (link TBD)
+
+## Principles
+
+### Version Control Your Schema
+
+(adapted from O3 principles)
+
+Version control systems like git track changes in files and enable multiple users to collaborate. They are widely used for maintaining code,
+but can (and should) also be used to maintain and manage data, metadata, ontologies, schemas, and other semantic artifacts.
+
+Some projects may opt to manage their data alongside the schema in the same repo. This may be a good pattern
+for "registry" style projects, but in other cases the data may be best managed separately in a dedicated
+store or a repository like Zenodo or Figshare.
+
+### Permissively License Your Code and Data
+
+(adapted from O3 principles)
+
+Using recognizable, permissive licenses (e.g., CC0, CC BY) encourages contribution and ensures content longevity.
+Non-permissive licenses or custom terms can hinder reuse and engagement.
+Permissive licensing doesn't typically lead to a lack of credit for the original resource.
+
+### Use Technical Workflows (Automation) and Social Workflows
+
+(adapted from O3 principles)
+
+Automating quality control, generation of artifacts, releases, and deployment helps in maintaining and contributing to projects.
+This includes using continuous integration for quality checks, automating the generation of data views, and packaging data and code for easy deployment.
+
+The [LinkML Cookiecutter](https://github.com/linkml/linkml-project-cookiecutter/) provides a template for a project that
+includes a number of automation features using GitHub actions.
+
+Implementing social workflows through tools like GitHub enhances community engagement.
+Transparent discussion forums, giving credit to contributors, and using issue trackers and discussion boards are crucial for maintaining a dynamic and active contributor base.
+
+Document or reference best practices for contributing to the schema. This includes things such 
+as [keeping issues and pull requests small and atomic](https://codeinthehole.com/tips/advanced-pull-request-crafting/).
+
+### Establish Project Governance
+
+(adapted from O3 principles and TisLab guide)
+
+Clear governance defines roles, responsibilities, and behavior expectations in a project.
+Establishing codes of conduct, standard operating procedures, and guidelines for administration and contribution roles is vital. Governance should evolve over time to meet the project's needs.
+
+You may wish to use an existing governance model or community documents as a starting point for your project:
+
+- Bioschemas [community](https://bioschemas.org/community/) and [governance](https://github.com/Bioschemas/governance/blob/master/governance.md) docs
+- [Biolink](https://biolink.github.io/biolink-model/) 
+- schema.org [how we work](https://schema.org/docs/howwework.html)
+- [GA4GH Constitution](https://www.ga4gh.org/constitution/)
+- [Bioregistry](https://github.com/biopragmatics/bioregistry/blob/main/docs/GOVERNANCE.md)
+
+### Attract and Engage Contributors
+
+Detailed contribution guidelines, offering various ways to contribute, and hosting projects in neutral spaces increase contributor engagement and recruitment. Projects should be accessible and welcoming to new contributors to ensure longevity and development.
+
+The [LinkML Cookiecutter](https://github.com/linkml/linkml-project-cookiecutter/) provides a template 
+for project documents such as a `CONTRIBUTING.md` file and a `CODE_OF_CONDUCT.md` file that can be
+adapted for your project.
+
+The core schema developers should be familiar with existing best practice for contribution.
+
+## Open Science Principles
+
+Many schemas developed in LinkML are not intended to be used in isolation, but rather as part of a larger
+open ecosystem. The [OBook](https://oboacademy.github.io/obook) has a chapter [How to be an Open Science Engineer - maximising impact for a better world](https://oboacademy.github.io/obook/howto/open-science-engineer/) outlines principles and practices for effective collaboration and impact in the field of open science engineering.
+
+Here's a summary:
+
+### Principle of Collaboration
+
+Emphasizes the importance of social collaborative workflows in open science. It advises on effective online communication, upvoting helpful answers on platforms like Stack Overflow and GitHub, answering questions even outside one's specific project, conducting basic research before posting queries, and continuously improving open science documentation.
+
+### Principle of Upstream Fixing
+
+Encourages fixing issues at the earliest possible stage in the dependency chain, maximizing the impact of changes and benefiting a wider community. It includes a case study highlighting the importance of quality control and community contributions in ontology development.
+
+### Principle of No-ownership
+
+Advocates for a mindset of shared ownership and collaborative development in open science projects, particularly in the context of publicly funded work like ontologies. It suggests embracing community-driven development without specific owners or decision-makers and emphasizes the importance of proactive involvement and decision-making in a decentralized environment.
+
+The document also includes a TL;DR summary with key takeaways:
+
+- Upvote and get involved in issue trackers.
+- Always conduct a basic search before asking questions.
+- Continuously improve documentation.
+- Be generous with likes and gratitude.
+- Promote open communication and push fixes upstream.
+- See issues and pull requests through to the end.
+- Encourage reviewing each other's work and reducing fear of making mistakes or having pull requests rejected.
+
+These principles and practices are aimed at fostering a more collaborative, efficient, and impactful open science community.
+
+## Example Governance and Community Documents
+
+### Bioschemas governance
+
+__Source__: Bioschemas [community](https://bioschemas.org/community/) and [governance](https://github.com/Bioschemas/governance/blob/master/governance.md) docs
+
+The document titled "Bioschemas Governance" outlines the governance structure and guidelines for the Bioschemas community, a project aimed at improving data interoperability in life sciences. Here's a summary:
+
+- **Overview**: Bioschemas adheres to five core principles of OpenStand: Respectful cooperation, adherence to standards development parameters, collective empowerment, availability, and voluntary adoption. Community members must follow the Code of Conduct based on FORCE11 guidelines.
+
+- **Steering Council**: Responsible for strategic and organizational planning, oversight of community activities, and promoting Bioschemas activities. The council meets every two months and communicates regularly via email and online messaging platforms.
+
+- **Community and Working Groups**: Day-to-day activities are conducted by the community, focusing on profile and type development and adoption. Working groups, each led by two individuals, develop markup practices for specific concepts. The Steering Council approves releases of profiles and types.
+
+- **Role Holder Appointment and Removal Processes**: Describes the election of Steering Council members and Working Group Leads, with a 2-year term of service. Inactive role holders may be removed following a defined process.
+
+- **Specification Development and Versioning**: Explains how specifications (profiles or types) are developed, including collaborative community engagement, version numbering, and authorship acknowledgment.
+
+- **Profile and Type Development**: Details steps for developing profiles and types, including identifying base types, property cardinality, and use cases. Processes for proposing new profiles or types and renaming or deprecating them are also covered.
+
+- **Changing Governance Documents**: Future changes to governance documents are to be submitted via a GitHub pull request, with public comment and Steering Council approval.
+
+- **Sources**: Lists references used in the document, including links to governing principles and codes of conduct from other organizations like FORCE11, Jupyter, and W3C.
+
+This document provides a comprehensive guide to the governance structure, roles, processes, and best practices within the Bioschemas community, emphasizing open collaboration, transparency, and adherence to established standards.
+
+### schema.org
+
+__SOURCE__: schema.org [how we work](https://schema.org/docs/howwework.html)
+
+The document titled "How We Work - Schema.org" provides an overview of the processes and practices employed by Schema.org in developing and updating its schemas. Here's a summary:
+
+- **Overview and Process**:
+  - Schema.org updates materials through official named releases every few weeks.
+  - Simple improvements and bug fixes can be fast-tracked as "Early Access Fixes".
+  - A development version of the site, "webschemas.org", reflects the latest work-in-progress based on community discussions and proposals.
+  - A "pending" extension at pending.webschemas.org showcases new vocabulary proposals, which may not yet reflect wider consensus.
+  - Steering group reviews and approves release candidates; if no concerns are raised within 10 business days, the official site is updated.
+
+- **Versioning and Change Control**:
+  - Schema.org is developed incrementally, with several updates a year, each documented as a release.
+  - Content for each release is based on public discussions and unanimous agreement of the Steering Group.
+  - Two types of extension vocabulary are introduced: hosted (part of Schema.org but tagged within a subdomain) and external (published elsewhere and managed by other organizations).
+
+- **Schema Structure**:
+  - Schema.org contains term definitions (types, properties, enumerated values), machine-readable files, and a JSON-LD context file.
+  - The approach to schema definitions is based on W3C RDFS with customizations.
+
+- **Extensibility Mechanisms**:
+  - Schema.org allows for extensibility through mechanisms independent of its vocabulary definitions and release planning.
+  - This includes publishing Schema.org data alongside other structured data types and using PropertyValue and Role mechanisms for additional annotations.
+
+- **Early Access Fixes and Pending Releases**:
+  - Early Access Fixes allow for rapid updates between official releases.
+  - The "pending schemas" extension is a staging area for work-in-progress terms, subject to change and community review.
+
+- **Workflow FAQ**:
+  - The document addresses common questions regarding public-vocabs, W3C WebSchemas group, the role of the Schema.org webmaster, and how to get involved or propose new schemas.
+
+- **Related Links and Further Reading**:
+  - The document provides links to additional resources for more in-depth understanding of Schema.org's work and processes.
+
+This document serves as a comprehensive guide to the operational framework, versioning system, and collaborative nature of Schema.org's efforts in structuring and standardizing web data.
\ No newline at end of file
diff --git a/linkml/generators/docgen.py b/linkml/generators/docgen.py
index c7d04a89..0dee9da8 100644
--- a/linkml/generators/docgen.py
+++ b/linkml/generators/docgen.py
@@ -130,6 +130,9 @@ class DocGenerator(Generator):
     directory: str = None
     """directory in which to write documents"""
 
+    index_name: str = "index"
+    """name of the index document"""
+
     template_directory: str = None
     """directory for custom templates"""
 
@@ -188,7 +191,7 @@ class DocGenerator(Generator):
         }
         template = self._get_template("index")
         out_str = template.render(gen=self, schema=sv.schema, schemaview=sv, **template_vars)
-        self._write(out_str, directory, "index")  # TODO: make configurable
+        self._write(out_str, directory, self.index_name)
         if self._is_single_file_format(self.format):
             logging.info(f"{self.format} is a single-page format, skipping non-index elements")
             return
@@ -844,6 +847,7 @@ class DocGenerator(Generator):
     required=True,
     help="Folder to which document files are written",
 )
+@click.option("--index-name", default="index", show_default=True, help="Name of the index document.")
 @click.option("--dialect", help="Dialect or 'flavor' of Markdown used.")
 @click.option(
     "--diagram-type",
@@ -885,7 +889,7 @@ class DocGenerator(Generator):
 )
 @click.version_option(__version__, "-V", "--version")
 @click.command()
-def cli(yamlfile, directory, dialect, template_directory, use_slot_uris, hierarchical_class_view, **args):
+def cli(yamlfile, directory, index_name, 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
@@ -912,6 +916,7 @@ def cli(yamlfile, directory, dialect, template_directory, use_slot_uris, hierarc
         template_directory=template_directory,
         use_slot_uris=use_slot_uris,
         hierarchical_class_view=hierarchical_class_view,
+        index_name=index_name,
         **args,
     )
     print(gen.serialize())
diff --git a/linkml/generators/docgen/class.md.jinja2 b/linkml/generators/docgen/class.md.jinja2
index 390f5923..52a33d49 100644
--- a/linkml/generators/docgen/class.md.jinja2
+++ b/linkml/generators/docgen/class.md.jinja2
@@ -1,4 +1,10 @@
-# Class: {{ gen.name(element) }}
+{%- if element.title -%}
+    {%- set title = element.title ~ ' (' ~ gen.name(element) ~ ')' -%}
+{%- else -%}
+    {%- set title = gen.name(element) -%}
+{%- endif -%}
+
+# Class: {{ title }}
 
 {%- if header -%}
 {{header}}
diff --git a/linkml/generators/docgen/slot.md.jinja2 b/linkml/generators/docgen/slot.md.jinja2
index 8bd9890d..fb4731e6 100644
--- a/linkml/generators/docgen/slot.md.jinja2
+++ b/linkml/generators/docgen/slot.md.jinja2
@@ -1,4 +1,10 @@
-# Slot: {{ gen.name(element) }}
+{%- if element.title %}
+    {%- set title = element.title ~ ' (' ~ element.name ~ ')' -%}
+{%- else %}
+    {%- set title = gen.name(element) -%}
+{%- endif -%}
+
+# Slot: {{ title }}
 
 {%- if header -%}
 {{header}}
diff --git a/linkml/generators/javagen.py b/linkml/generators/javagen.py
index 85ec4349..3043ae1d 100644
--- a/linkml/generators/javagen.py
+++ b/linkml/generators/javagen.py
@@ -45,9 +45,9 @@ TYPEMAP = {
     "xsd:float": "Float",
     "xsd:double": "Double",
     "xsd:boolean": "boolean",
-    "xds:dateTime": "ZonedDateTime",
-    "xds:date": "LocalDateTime",
-    "xds:time": "Instant",
+    "xsd:dateTime": "ZonedDateTime",
+    "xsd:date": "LocalDate",
+    "xsd:time": "Instant",
     "xsd:anyURI": "String",
     "xsd:decimal": "BigDecimal",
 }
diff --git a/linkml/generators/javagen/java_record_template.jinja2 b/linkml/generators/javagen/java_record_template.jinja2
index e63a81c0..aee9b036 100644
--- a/linkml/generators/javagen/java_record_template.jinja2
+++ b/linkml/generators/javagen/java_record_template.jinja2
@@ -5,8 +5,8 @@ package {{ doc.package }};
 import java.math.BigDecimal;{% endif -%}
 {% if f.range.startswith('Instant') %}
 import java.time.Instant;{% endif -%}
-{% if f.range.startswith('LocalDateTime') %}
-import java.time.LocalDateTime;{% endif -%}
+{% if f.range.startswith('LocalDate') %}
+import java.time.LocalDate;{% endif -%}
 {% if f.range.startswith('ZonedDateTime') %}
 import java.time.ZonedDateTime;{% endif -%}
 {% if f.range.startswith('List') %}
diff --git a/linkml/generators/owlgen.py b/linkml/generators/owlgen.py
index 21da64e2..cd8a5ca8 100644
--- a/linkml/generators/owlgen.py
+++ b/linkml/generators/owlgen.py
@@ -4,7 +4,7 @@ import os
 from collections import defaultdict
 from dataclasses import dataclass, field
 from enum import Enum, unique
-from typing import List, Mapping, Optional, Set, Tuple, Union
+from typing import Any, List, Mapping, Optional, Set, Tuple, Union
 
 import click
 import rdflib
@@ -51,7 +51,11 @@ class MetadataProfile(Enum):
     """
 
     linkml = "linkml"
+    """Default, uses the slot URIs from the LinkML metamodel"""
+
     rdfs = "rdfs"
+    """RDFS conventions, using core RDFS properties preferentially"""
+
     ols = "ols"
 
     @staticmethod
@@ -62,21 +66,20 @@ class MetadataProfile(Enum):
 @unique
 class OWLProfile(Enum):
     """
-    An enumeration of OWL Profiles
+    An enumeration of OWL Profiles.
     """
 
     dl = "dl"
+    """Here this means strict OWL DL, with no punning."""
+
     full = "full"
+    """May include punning (metaclasses)."""
 
     @staticmethod
     def list():
         return list(map(lambda c: c.value, MetadataProfile))
 
 
-class ElementDefinition(object):
-    pass
-
-
 @dataclass
 class OwlSchemaGenerator(Generator):
     """
@@ -162,7 +165,7 @@ class OwlSchemaGenerator(Generator):
 
     def as_graph(self) -> Graph:
         """
-        Generate an rdflib Graph from the schema
+        Generate an rdflib Graph from the LinkML schema.
 
         :return:
         """
@@ -173,6 +176,7 @@ class OwlSchemaGenerator(Generator):
             owl_id = f"{owl_id}{self.ontology_uri_suffix}"
         mergeimports = self.mergeimports
         base = URIRef(owl_id)
+        # initialize the rdflib Graph where all axiom triples will be added
         graph = Graph(identifier=base)
         self.graph = graph
         for prefix in self.metamodel.schema.emit_prefixes:
@@ -181,6 +185,7 @@ class OwlSchemaGenerator(Generator):
             self.graph.namespace_manager.bind(pfx.prefix_prefix, URIRef(pfx.prefix_reference))
         graph.add((base, RDF.type, OWL.Ontology))
 
+        # Add main schema elements
         for cls in sv.all_classes(imports=mergeimports).values():
             self.add_class(cls)
             for a in cls.attributes.values():
@@ -192,17 +197,26 @@ class OwlSchemaGenerator(Generator):
         for enm in sv.all_enums(imports=mergeimports).values():
             self.add_enum(enm)
 
+        # Add metadata as annotation properties
         self.add_metadata(schema, base)
         return graph
 
     def serialize(self, **kwargs) -> str:
+        """
+        Serialize the OWL triple graph to a standard RDF serialization format.
+
+        :param kwargs:
+        :return:
+        """
         self.as_graph()
         data = self.graph.serialize(format="turtle" if self.format in ["owl", "ttl"] else self.format)
         return data
 
     def add_metadata(self, e: Definition, uri: URIRef) -> None:
         """
-        Add generic annotation properties.
+        Add annotation properties.
+
+        Set the profile attribute to the appropriate OWL profile.
 
         :param e: schema element
         :param uri: URI representation of schema element
@@ -213,8 +227,10 @@ class OwlSchemaGenerator(Generator):
         this_sv = self.schemaview
         sn_mappings = msv.slot_name_mappings()
 
+        # iterate through all the assigned metamodel slots
         for metaslot_name, metaslot_value in vars(e).items():
             if not metaslot_value:
+                # ignore if unset or an empty list
                 continue
             metaslot_name = sn_mappings.get(metaslot_name).name
             metaslot = msv.induced_slot(metaslot_name, e.class_name)
@@ -262,18 +278,21 @@ class OwlSchemaGenerator(Generator):
             if k_uri:
                 self.graph.add((uri, URIRef(k_uri), Literal(v.value)))
 
-    def add_class(self, cls: ClassDefinition) -> bool:
+    def add_class(self, cls: ClassDefinition) -> None:
         """
-        Each ClassDefinition is represented as an OWL class
+        Each ClassDefinition is represented as an OWL class.
 
         * the OWL Class will instantiate ClassDefinition, if schema.metaclasses is true
         * the OWL Class will be annotated using the same properties as the source ClassDefinition
         * induced slots and their ranges added as OWL restrictions; note this will be under the Open World Assumption
+
+        This method works by generating an OWL ontology via populating triples in a graph.
+        To understand how the RDF-level operations here related to the OWL
+        representation, consult https://www.w3.org/TR/owl2-mapping-to-rdf/
+
         :param cls:
         :return:
         """
-        # To understand how the RDF-level operations here related to the OWL
-        # representation, consult https://www.w3.org/TR/owl2-mapping-to-rdf/
         sv = self.schemaview
         cls_uri = self._class_uri(cls.name)
         self.add_metadata(cls, cls_uri)
@@ -288,9 +307,8 @@ class OwlSchemaGenerator(Generator):
                     ClassDefinition.class_class_uri,
                 )
             )
-        # self._add_element_properties(cls_uri, cls)
 
-        # Parent classes
+        # Parent classes: is_a and mixins
         has_parent = False
         if cls.is_a:
             self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(cls.is_a)))
@@ -303,6 +321,7 @@ class OwlSchemaGenerator(Generator):
                 has_parent = True
             self.graph.add((cls_uri, RDFS.subClassOf, parent))
         if not has_parent and self.add_root_classes:
+            # If user selects add_root_classes, then all classes will be subclasses of LinkML:ClassDefinition
             if cls.mixin and self.mixins_as_expressions:
                 self.graph.add((cls_uri, RDFS.subClassOf, self._mixin_grouping_class_uri()))
             else:
@@ -319,8 +338,9 @@ class OwlSchemaGenerator(Generator):
                             cls_uri,
                         )
                     )
-
         if cls.class_uri:
+            # If a class_ur is assigned, and it is different from model class_uri, then
+            # Add an assertion that links the two
             mapped_uri = sv.get_uri(cls, expand=True, native=not self.use_native_uris)
             if cls_uri != mapped_uri:
                 p = OWL.equivalentClass if self.assert_equivalent_classes else SKOS.exactMatch
@@ -328,11 +348,15 @@ class OwlSchemaGenerator(Generator):
         subject_expr = URIRef(cls_uri)
         if self.mixins_as_expressions and cls.mixin:
             subject_expr = self._some_values_from(self._metaslot_uri("mixins"), subject_expr)
+        # type designator yield GCI rules
+        # E.g. if C has a type designator slot t,
+        # then create an axiom: (t some C) subClassOf C.
         type_designator = sv.get_type_designator_slot(cls.name)
         if type_designator:
             td_prop = self._prop_uri(type_designator.name)
             restr = self._some_values_from(td_prop, subject_expr)
             self.graph.add((restr, RDFS.subClassOf, subject_expr))
+        # unique key constraints are mapped to OWL hasKey
         if cls.unique_keys:
             for uk in cls.unique_keys.values():
                 uk_props = [self._prop_uri(slot) for slot in uk.unique_key_slots]
@@ -341,6 +365,7 @@ class OwlSchemaGenerator(Generator):
                 self.graph.add((subject_expr, OWL.hasKey, uk_props_listnode))
 
         def condition_to_bnode(expr: AnonymousClassExpression) -> Optional[BNode]:
+            # inner function: translate a LinkML class expression to an OWL class expression.
             ixn_listnode = self.transform_class_expression(expr, quantifier_predicate=OWL.someValuesFrom)
             if not ixn_listnode:
                 return None
@@ -348,11 +373,13 @@ class OwlSchemaGenerator(Generator):
                 ixn_listnode = self._intersection_of([ixn_listnode, self._class_uri(expr.is_a)])
             return ixn_listnode
 
+        # rules yield OWL GCI subClassOf axioms
         for rule in cls.rules:
             pre_node = condition_to_bnode(rule.preconditions)
             pre_node = self._intersection_of([pre_node, subject_expr])
             post_node = condition_to_bnode(rule.postconditions)
             self.graph.add((pre_node, RDFS.subClassOf, post_node))
+        # classification rules yield OWL GCI subClassOf axioms
         for expr in cls.classification_rules:
             ixn_listnode = condition_to_bnode(expr)
             self.graph.add((ixn_listnode, RDFS.subClassOf, subject_expr))
@@ -366,9 +393,12 @@ class OwlSchemaGenerator(Generator):
                 # simplify
                 if len(ixn_listnodes) > 1:
                     raise AssertionError
-                for x in Collection(self.graph, ixn_listnodes[0]):
+                ixn_listnode = ixn_listnodes[0]
+                if not isinstance(ixn_listnode, BNode):
+                    raise AssertionError
+                for x in Collection(self.graph, ixn_listnode):
                     self.graph.add((subject_expr, RDFS.subClassOf, x))
-                self._remove_list(ixn_listnodes[0])
+                self._remove_list(ixn_listnode)
                 self.graph.remove((superclass_expr, OWL.intersectionOf, ixn_listnodes[0]))
             else:
                 self.graph.add((subject_expr, RDFS.subClassOf, superclass_expr))
@@ -382,9 +412,10 @@ class OwlSchemaGenerator(Generator):
         Transform a LinkML class expression into an OWL expression.
 
         If the class includes boolean expressions, then these
-        are recursively transformed.
+        are recursively transformed (each such inner expression is
+        an anonymous expression)
 
-        :param cls: LinkML class expression
+        :param cls: LinkML class expression (anonymous if called recursively)
         :return: blank node representing the OWL expression
         """
         graph = self.graph
@@ -423,12 +454,16 @@ class OwlSchemaGenerator(Generator):
         if cls.all_of:
             owl_exprs.append(self._intersection_of([self.transform_class_expression(x) for x in cls.all_of]))
         if cls.none_of:
-            owl_exprs.append(self._complement_of_union_of([self.transform_class_expression(x) for x in cls.any_of]))
+            owl_exprs.append(self._complement_of_union_of([self.transform_class_expression(x) for x in cls.none_of]))
         for slot in own_slots:
-            x = self.transform_class_slot_expression(cls, slot, slot)
+            if slot.name:
+                owltypes = self.slot_node_owltypes(sv.get_slot(slot.name))
+            else:
+                owltypes = self.slot_node_owltypes(slot)
+            x = self.transform_class_slot_expression(cls, slot, slot, owltypes)
             if not x:
                 range = sv.schema.default_range
-                if range:
+                if range and OWL.Thing not in owltypes:
                     if range in sv.all_types():
                         x = self._type_uri(range)
                     elif range in sv.all_enums():
@@ -467,11 +502,28 @@ class OwlSchemaGenerator(Generator):
                 graph.add((max_card_expr, OWL.onProperty, slot_uri))
         return self._intersection_of(owl_exprs)
 
+    def slot_node_owltypes(self, slot: Union[SlotDefinition, AnonymousSlotExpression]) -> Set[URIRef]:
+        sv = self.schemaview
+        node_types = set()
+        if isinstance(slot, SlotDefinition):
+            if slot.range in sv.all_classes():
+                range_class = sv.get_class(slot.range)
+                if not (range_class and range_class.class_uri == "linkml:Any"):
+                    node_types.add(OWL.Thing)
+            if slot.range in sv.all_types():
+                node_types.add(RDFS.Datatype)
+        for k in ["any_of", "all_of", "exactly_one_of", "none_of"]:
+            subslot = getattr(slot, k, None)
+            if subslot:
+                node_types.update(self.slot_node_owltypes(subslot))
+        return node_types
+
     def transform_class_slot_expression(
         self,
         cls: Optional[Union[ClassDefinition, AnonymousClassExpression]],
         slot: Union[SlotDefinition, AnonymousSlotExpression],
         main_slot: SlotDefinition = None,
+        owl_types: Set[OWL_TYPE] = None,
     ) -> Union[BNode, URIRef]:
         """
         Take a ClassExpression and SlotExpression combination and transform to a node.
@@ -489,34 +541,51 @@ class OwlSchemaGenerator(Generator):
 
         owl_exprs = []
 
+        if slot.has_member:
+            owl_exprs.append(
+                self._union_of(
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.has_member]
+                )
+            )
+        if slot.all_members:
+            owl_exprs.append(
+                self._union_of(
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.all_members]
+                )
+            )
+
         if slot.any_of:
             owl_exprs.append(
-                self._union_of([self.transform_class_slot_expression(cls, x, main_slot) for x in slot.any_of])
+                self._union_of(
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.any_of]
+                )
             )
         if slot.all_of:
             owl_exprs.append(
-                self._intersection_of([self.transform_class_slot_expression(cls, x, main_slot) for x in slot.all_of])
+                self._intersection_of(
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.all_of]
+                )
             )
         if slot.none_of:
             owl_exprs.append(
                 self._complement_of_union_of(
-                    [self.transform_class_slot_expression(cls, x, main_slot) for x in slot.none_of]
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.none_of]
                 )
             )
         if slot.exactly_one_of:
             owl_exprs.append(
                 self._exactly_one_of(
-                    [self.transform_class_slot_expression(cls, x, main_slot) for x in slot.exactly_one_of]
+                    [self.transform_class_slot_expression(cls, x, main_slot, owl_types) for x in slot.exactly_one_of]
                 )
             )
         range = slot.range
         # if not range and not owl_exprs:
         #    range = sv.schema.default_range
-        owl_types = set()
+        this_owl_types = set()
         if range:
             if range in sv.all_types(imports=True):
                 self.slot_is_literal_map[main_slot.name].add(True)
-                owl_types.add(RDFS.Literal)
+                this_owl_types.add(RDFS.Literal)
                 typ = sv.get_type(range)
                 if self.type_objects:
                     # TODO
@@ -527,21 +596,25 @@ class OwlSchemaGenerator(Generator):
                 # TODO: enums fill this in
                 owl_exprs.append(self._enum_uri(EnumDefinitionName(range)))
             elif range in sv.all_classes(imports=True):
-                owl_types.add(OWL.Thing)
+                this_owl_types.add(OWL.Thing)
                 self.slot_is_literal_map[main_slot.name].add(False)
                 owl_exprs.append(self._class_uri(ClassDefinitionName(range)))
             else:
                 raise ValueError(f"Unknown range {range}")
-        constraints_exprs, constraints_owltypes = self.add_constraints(slot)
-        owl_types.update(constraints_owltypes)
+        is_literal = None
+        if owl_types:
+            is_literal = RDFS.Datatype in owl_types
+        constraints_exprs, constraints_owltypes = self.add_constraints(slot, is_literal=is_literal)
+        this_owl_types.update(constraints_owltypes)
         owl_exprs.extend(constraints_exprs)
-        this_expr = self._intersection_of(owl_exprs, owl_types=owl_types)
-        self.node_owltypes[this_expr].update(self._get_owltypes(owl_types, owl_exprs))
+        this_expr = self._intersection_of(owl_exprs, owl_types=this_owl_types)
+        self.node_owltypes[this_expr].update(self._get_owltypes(this_owl_types, owl_exprs))
         return this_expr
 
     def add_constraints(
         self,
         element: Union[SlotDefinition, AnonymousSlotExpression, TypeDefinition, AnonymousTypeExpression],
+        is_literal: Optional[bool] = None,
     ) -> Tuple[List[BNode], Set[OWL_TYPE]]:
         owl_types = set()
         owl_exprs = []
@@ -554,6 +627,29 @@ class OwlSchemaGenerator(Generator):
         if element.equals_number is not None:
             constraints[XSD.minInclusive] = element.equals_number
             constraints[XSD.maxInclusive] = element.equals_number
+        if element.equals_string is not None:
+            equals_string = element.equals_string
+            if is_literal is None:
+                logging.warning(f"ignoring equals_string={equals_string} as unable to tell if literal")
+            elif is_literal:
+                constraints[XSD.pattern] = equals_string
+            else:
+                eq_uri = URIRef(self.schemaview.expand_curie(equals_string))
+                owl_exprs.append(eq_uri)
+        if element.equals_string_in:
+            equals_string_in = element.equals_string_in
+            if is_literal is None:
+                logging.warning(f"ignoring equals_string={equals_string_in} as unable to tell if literal")
+            elif is_literal:
+                dt_exprs = [
+                    self._datatype_restriction(XSD.string, [self._facet(XSD.pattern, s)]) for s in equals_string_in
+                ]
+                union_expr = self._union_of(dt_exprs, owl_types={RDFS.Literal})
+                owl_exprs.append(union_expr)
+                owl_types.add(RDFS.Literal)
+            else:
+                eq_uris = [URIRef(self.schemaview.expand_curie(s)) for s in equals_string_in]
+                owl_exprs.append(self._union_of(eq_uris))
         for constraint_prop, constraint_val in constraints.items():
             if constraint_val is not None:
                 owl_types.add(RDFS.Literal)
@@ -673,7 +769,7 @@ class OwlSchemaGenerator(Generator):
                 # self.graph.add(
                 #    (type_uri, OWL.equivalentClass, URIRef(self.schemaview.expand_curie(typ.uri)))
                 # )
-            constraints_exprs, _ = self.add_constraints(typ)
+            constraints_exprs, _ = self.add_constraints(typ, is_literal=True)
             eq_conjunctions.extend(constraints_exprs)
             ixn = self._intersection_of(eq_conjunctions)
             if ixn:
@@ -882,6 +978,23 @@ class OwlSchemaGenerator(Generator):
             sub_exprs.append(self._intersection_of([x, neg_expr]))
         return self._union_of(sub_exprs)
 
+    def _datatype_restriction(self, datatype: URIRef, facets: List[Union[BNode, URIRef]]) -> BNode:
+        node = BNode()
+        graph = self.graph
+        graph.add((node, RDF.type, RDFS.Datatype))
+        graph.add((node, OWL.onDatatype, datatype))
+        listnode = BNode()
+        Collection(graph, listnode, facets)
+        graph.add((node, OWL.withRestrictions, listnode))
+        return node
+
+    def _facet(self, typ: URIRef, val: Union[Literal, Any]):
+        if not isinstance(val, Literal):
+            val = Literal(val)
+        node = BNode()
+        self.graph.add((node, typ, val))
+        return node
+
     def _boolean_expression(
         self,
         exprs: List[Union[BNode, URIRef]],
@@ -891,7 +1004,8 @@ class OwlSchemaGenerator(Generator):
     ) -> Optional[Union[BNode, URIRef]]:
         graph = self.graph
         if [x for x in exprs if x is None]:
-            raise ValueError(f"None in exprs: {exprs}")
+            logging.warning(f"Null expr in: {exprs} for {predicate} {node}")
+            exprs = [x for x in exprs if x is not None]
         if len(exprs) == 0:
             return None
         elif len(exprs) == 1:
diff --git a/linkml/generators/pydanticgen.py b/linkml/generators/pydanticgen.py
index b78b0b88..66a5f5d4 100644
--- a/linkml/generators/pydanticgen.py
+++ b/linkml/generators/pydanticgen.py
@@ -499,18 +499,10 @@ class PydanticGenerator(OOCodeGenerator):
 
                 # 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:
-                    base_range_subsumes_any_of = False
-                    base_range = sv.get_slot(sn).range
-                    base_range_cls = sv.get_class(base_range, strict=False)
-                    if base_range_cls is not None and base_range_cls.class_uri == "linkml:Any":
-                        base_range_subsumes_any_of = True
-                    if not base_range_subsumes_any_of:
-                        raise ValueError("Slot cannot have both range and any_of defined")
-
-                if s.any_of is not None and len(s.any_of) > 0:
+                any_of_ranges = [a.range if a.range else s.range for a in s.any_of]
+                if any_of_ranges:
                     # list comprehension here is pulling ranges from within AnonymousSlotExpression
-                    slot_ranges.extend([r.range for r in s.any_of])
+                    slot_ranges.extend(any_of_ranges)
                 else:
                     slot_ranges.append(s.range)
 
diff --git a/linkml/generators/rdfgen.py b/linkml/generators/rdfgen.py
index acf2f592..06423ca9 100644
--- a/linkml/generators/rdfgen.py
+++ b/linkml/generators/rdfgen.py
@@ -36,7 +36,7 @@ class RDFGenerator(Generator):
     context: List[str] = None
 
     def _data(self, g: Graph) -> str:
-        return g.serialize(format="turtle" if self.format == "ttl" else self.format).decode()
+        return g.serialize(format="turtle" if self.format == "ttl" else self.format)
 
     def end_schema(self, output: Optional[str] = None, context: str = None, **_) -> None:
         gen = JSONLDGenerator(
diff --git a/linkml/generators/shaclgen.py b/linkml/generators/shaclgen.py
index 90dcf2ba..928e2f59 100644
--- a/linkml/generators/shaclgen.py
+++ b/linkml/generators/shaclgen.py
@@ -53,7 +53,7 @@ class ShaclGenerator(Generator):
 
     def serialize(self, **args) -> None:
         g = self.as_graph()
-        data = g.serialize(format="turtle" if self.format in ["owl", "ttl"] else self.format).decode()
+        data = g.serialize(format="turtle" if self.format in ["owl", "ttl"] else self.format)
         return data
 
     def as_graph(self) -> None:
diff --git a/linkml/generators/shexgen.py b/linkml/generators/shexgen.py
index 796926fd..fb41cc93 100644
--- a/linkml/generators/shexgen.py
+++ b/linkml/generators/shexgen.py
@@ -148,7 +148,7 @@ class ShExGenerator(Generator):
             g = Graph()
             g.parse(data=shex, format="json-ld")
             g.bind("owl", OWL)
-            shex = g.serialize(format="turtle").decode()
+            shex = g.serialize(format="turtle")
         elif self.format == "shex":
             g = Graph()
             self.namespaces.load_graph(g)
diff --git a/poetry.lock b/poetry.lock
index fab50573..4231056b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
 version = "0.7.13"
 description = "A configurable sidebar-enabled Sphinx theme"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -16,7 +15,6 @@ files = [
 name = "antlr4-python3-runtime"
 version = "4.9.3"
 description = "ANTLR 4.9.3 runtime for Python 3.7"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -27,7 +25,6 @@ files = [
 name = "appnope"
 version = "0.1.3"
 description = "Disable App Nap on macOS >= 10.9"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -39,7 +36,6 @@ files = [
 name = "arrow"
 version = "1.2.3"
 description = "Better dates & times for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -54,7 +50,6 @@ python-dateutil = ">=2.7.0"
 name = "attrs"
 version = "23.1.0"
 description = "Classes Without Boilerplate"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -73,7 +68,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
 name = "babel"
 version = "2.12.1"
 description = "Internationalization utilities"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -88,7 +82,6 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
 name = "backcall"
 version = "0.2.0"
 description = "Specifications for callback functions passed in to an API"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -100,7 +93,6 @@ files = [
 name = "beautifulsoup4"
 version = "4.12.2"
 description = "Screen-scraping library"
-category = "dev"
 optional = false
 python-versions = ">=3.6.0"
 files = [
@@ -119,7 +111,6 @@ lxml = ["lxml"]
 name = "bleach"
 version = "6.0.0"
 description = "An easy safelist-based HTML-sanitizing tool."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -138,7 +129,6 @@ css = ["tinycss2 (>=1.1.0,<1.2)"]
 name = "cachetools"
 version = "5.3.1"
 description = "Extensible memoizing collections and decorators"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -150,7 +140,6 @@ files = [
 name = "certifi"
 version = "2023.7.22"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -162,7 +151,6 @@ files = [
 name = "cffi"
 version = "1.15.1"
 description = "Foreign Function Interface for Python calling C code."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -239,7 +227,6 @@ pycparser = "*"
 name = "cfgraph"
 version = "0.2.1"
 description = "rdflib collections flattening graph"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -253,7 +240,6 @@ rdflib = ">=0.4.2"
 name = "chardet"
 version = "5.2.0"
 description = "Universal encoding detector for Python 3"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -265,7 +251,6 @@ files = [
 name = "charset-normalizer"
 version = "3.2.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -350,7 +335,6 @@ files = [
 name = "click"
 version = "8.1.7"
 description = "Composable command line interface toolkit"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -365,7 +349,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -377,7 +360,6 @@ files = [
 name = "coverage"
 version = "6.5.0"
 description = "Code coverage measurement for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -440,7 +422,6 @@ toml = ["tomli"]
 name = "curies"
 version = "0.5.5"
 description = "Idiomatic conversion between URIs and compact URIs (CURIEs)."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -466,7 +447,6 @@ tests = ["coverage", "pytest"]
 name = "debugpy"
 version = "1.7.0"
 description = "An implementation of the Debug Adapter Protocol for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -498,7 +478,6 @@ files = [
 name = "decorator"
 version = "5.1.1"
 description = "Decorators for Humans"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -510,7 +489,6 @@ files = [
 name = "defusedxml"
 version = "0.7.1"
 description = "XML bomb protection for Python stdlib modules"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 files = [
@@ -522,7 +500,6 @@ files = [
 name = "deprecated"
 version = "1.2.14"
 description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -540,7 +517,6 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
 name = "distlib"
 version = "0.3.7"
 description = "Distribution utilities"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -552,7 +528,6 @@ files = [
 name = "docutils"
 version = "0.19"
 description = "Docutils -- Python Documentation Utilities"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -564,7 +539,6 @@ files = [
 name = "entrypoints"
 version = "0.4"
 description = "Discover and load entry points from installed packages."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -576,7 +550,6 @@ files = [
 name = "et-xmlfile"
 version = "1.1.0"
 description = "An implementation of lxml.xmlfile for the standard library"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -588,7 +561,6 @@ files = [
 name = "exceptiongroup"
 version = "1.1.3"
 description = "Backport of PEP 654 (exception groups)"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -603,7 +575,6 @@ test = ["pytest (>=6)"]
 name = "fastjsonschema"
 version = "2.18.0"
 description = "Fastest Python implementation of JSON schema"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -618,7 +589,6 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc
 name = "filelock"
 version = "3.12.2"
 description = "A platform independent file lock."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -634,7 +604,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p
 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"
 files = [
@@ -646,7 +615,6 @@ files = [
 name = "furo"
 version = "2023.3.27"
 description = "A clean customisable Sphinx documentation theme."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -664,7 +632,6 @@ sphinx-basic-ng = "*"
 name = "graphviz"
 version = "0.20.1"
 description = "Simple Python interface for Graphviz"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -681,7 +648,6 @@ test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>=
 name = "greenlet"
 version = "2.0.1"
 description = "Lightweight in-process concurrent programming"
-category = "main"
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
 files = [
@@ -755,7 +721,6 @@ test = ["faulthandler", "objgraph", "psutil"]
 name = "hbreader"
 version = "0.9.1"
 description = "Honey Badger reader - a generic file/url/string open and read tool"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -767,7 +732,6 @@ files = [
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -779,7 +743,6 @@ files = [
 name = "imagesize"
 version = "1.4.1"
 description = "Getting image size from png/jpeg/jpeg2000/gif file"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -791,7 +754,6 @@ files = [
 name = "importlib-metadata"
 version = "6.7.0"
 description = "Read metadata from Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -811,7 +773,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
 name = "importlib-resources"
 version = "5.12.0"
 description = "Read resources from Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -830,7 +791,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
 name = "iniconfig"
 version = "2.0.0"
 description = "brain-dead simple config-ini parsing"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -842,7 +802,6 @@ files = [
 name = "ipykernel"
 version = "6.16.2"
 description = "IPython Kernel for Jupyter"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -871,7 +830,6 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-cov", "p
 name = "ipython"
 version = "7.34.0"
 description = "IPython: Productive Interactive Computing"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -908,7 +866,6 @@ test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments"
 name = "ipython-genutils"
 version = "0.2.0"
 description = "Vestigial utilities from IPython"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -920,7 +877,6 @@ files = [
 name = "isodate"
 version = "0.6.1"
 description = "An ISO 8601 date/time/duration parser and formatter"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -935,7 +891,6 @@ six = "*"
 name = "isoduration"
 version = "20.11.0"
 description = "Operations with ISO 8601 durations"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -950,7 +905,6 @@ arrow = ">=0.15.0"
 name = "jedi"
 version = "0.19.0"
 description = "An autocompletion tool for Python that can be used for text editors."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -970,7 +924,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
 name = "jinja2"
 version = "3.1.2"
 description = "A very fast and expressive template engine."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -988,7 +941,6 @@ i18n = ["Babel (>=2.7)"]
 name = "json-flattener"
 version = "0.1.9"
 description = "Python library for denormalizing nested dicts or json objects to tables and back"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -1004,7 +956,6 @@ pyyaml = "*"
 name = "jsonasobj"
 version = "1.3.1"
 description = "JSON as python objects"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1016,7 +967,6 @@ files = [
 name = "jsonasobj2"
 version = "1.0.4"
 description = "JSON as python objects - version 2"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1031,12 +981,10 @@ hbreader = "*"
 name = "jsonpatch"
 version = "1.33"
 description = "Apply JSON-Patches (RFC 6902)"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
 files = [
     {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
-    {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
 ]
 
 [package.dependencies]
@@ -1046,7 +994,6 @@ jsonpointer = ">=1.9"
 name = "jsonpath-ng"
 version = "1.6.0"
 description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1061,19 +1008,16 @@ ply = "*"
 name = "jsonpointer"
 version = "2.4"
 description = "Identify specific nodes in a JSON document (RFC 6901)"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
 files = [
     {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
-    {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
 ]
 
 [[package]]
 name = "jsonschema"
 version = "4.17.3"
 description = "An implementation of JSON Schema validation for Python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1103,7 +1047,6 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-
 name = "jupyter-client"
 version = "7.4.9"
 description = "Jupyter protocol implementation and client libraries"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1128,7 +1071,6 @@ test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-com
 name = "jupyter-core"
 version = "4.12.0"
 description = "Jupyter core package. A base package on which Jupyter projects rely."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1147,7 +1089,6 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
 name = "jupyterlab-pygments"
 version = "0.2.2"
 description = "Pygments theme using JupyterLab CSS variables"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1159,7 +1100,6 @@ files = [
 name = "linkml-dataops"
 version = "0.1.0"
 description = "LinkML Data Operations API"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1178,7 +1118,6 @@ linkml-runtime = ">=1.1.6"
 name = "linkml-runtime"
 version = "1.6.0"
 description = "Runtime environment for LinkML, the Linked open data modeling language"
-category = "main"
 optional = false
 python-versions = ">=3.7.6,<4.0.0"
 files = [
@@ -1205,7 +1144,6 @@ requests = "*"
 name = "markdown-it-py"
 version = "2.2.0"
 description = "Python port of markdown-it. Markdown parsing, done right!"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1230,7 +1168,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 name = "markupsafe"
 version = "2.1.3"
 description = "Safely add untrusted strings to HTML/XML markup."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1290,7 +1227,6 @@ files = [
 name = "matplotlib-inline"
 version = "0.1.6"
 description = "Inline Matplotlib backend for Jupyter"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1305,7 +1241,6 @@ traitlets = "*"
 name = "mdit-py-plugins"
 version = "0.3.5"
 description = "Collection of plugins for markdown-it-py"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1325,7 +1260,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 name = "mdurl"
 version = "0.1.2"
 description = "Markdown URL utilities"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1337,7 +1271,6 @@ files = [
 name = "mistune"
 version = "3.0.1"
 description = "A sane and fast Markdown parser with useful plugins and renderers"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1349,7 +1282,6 @@ files = [
 name = "myst-parser"
 version = "1.0.0"
 description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser,"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1376,7 +1308,6 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,
 name = "nbclient"
 version = "0.7.4"
 description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
-category = "dev"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -1386,7 +1317,7 @@ files = [
 
 [package.dependencies]
 jupyter-client = ">=6.1.12"
-jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
 nbformat = ">=5.1"
 traitlets = ">=5.3"
 
@@ -1399,7 +1330,6 @@ test = ["flaky", "ipykernel", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "p
 name = "nbconvert"
 version = "7.6.0"
 description = "Converting Jupyter Notebooks"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1438,7 +1368,6 @@ webpdf = ["pyppeteer (>=1,<1.1)"]
 name = "nbformat"
 version = "5.8.0"
 description = "The Jupyter Notebook format"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1460,7 +1389,6 @@ test = ["pep440", "pre-commit", "pytest", "testpath"]
 name = "nest-asyncio"
 version = "1.5.8"
 description = "Patch asyncio to allow nested event loops"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1472,7 +1400,6 @@ files = [
 name = "openpyxl"
 version = "3.1.2"
 description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1487,7 +1414,6 @@ et-xmlfile = "*"
 name = "packaging"
 version = "23.1"
 description = "Core utilities for Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1499,7 +1425,6 @@ files = [
 name = "pandocfilters"
 version = "1.5.0"
 description = "Utilities for writing pandoc filters in python"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -1511,7 +1436,6 @@ files = [
 name = "parse"
 version = "1.19.1"
 description = "parse() is the opposite of format()"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1523,7 +1447,6 @@ files = [
 name = "parso"
 version = "0.8.3"
 description = "A Python Parser"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1539,7 +1462,6 @@ testing = ["docopt", "pytest (<6.0.0)"]
 name = "pexpect"
 version = "4.8.0"
 description = "Pexpect allows easy control of interactive console applications."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1554,7 +1476,6 @@ ptyprocess = ">=0.5"
 name = "pickleshare"
 version = "0.7.5"
 description = "Tiny 'shelve'-like database with concurrency support"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1566,7 +1487,6 @@ files = [
 name = "pkgutil-resolve-name"
 version = "1.3.10"
 description = "Resolve a name to an object."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1578,7 +1498,6 @@ files = [
 name = "platformdirs"
 version = "3.10.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1594,7 +1513,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
 name = "pluggy"
 version = "1.2.0"
 description = "plugin and hook calling mechanisms for python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1610,7 +1528,6 @@ testing = ["pytest", "pytest-benchmark"]
 name = "ply"
 version = "3.11"
 description = "Python Lex & Yacc"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1622,7 +1539,6 @@ files = [
 name = "prefixcommons"
 version = "0.1.12"
 description = "A python API for working with ID prefixes"
-category = "main"
 optional = false
 python-versions = ">=3.7,<4.0"
 files = [
@@ -1640,7 +1556,6 @@ requests = ">=2.28.1,<3.0.0"
 name = "prefixmaps"
 version = "0.1.5"
 description = "A python library for retrieving semantic prefix maps"
-category = "main"
 optional = false
 python-versions = ">=3.7.6,<4.0.0"
 files = [
@@ -1663,7 +1578,6 @@ refresh = ["bioregistry[refresh] (>=0.8.0,<0.9.0)", "rdflib[refresh] (>=6.2.0,<7
 name = "prompt-toolkit"
 version = "3.0.39"
 description = "Library for building powerful interactive command lines in Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -1678,7 +1592,6 @@ wcwidth = "*"
 name = "psutil"
 version = "5.9.5"
 description = "Cross-platform lib for process and system monitoring in Python."
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -1705,7 +1618,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 name = "ptyprocess"
 version = "0.7.0"
 description = "Run a subprocess in a pseudo terminal"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1717,7 +1629,6 @@ files = [
 name = "pycparser"
 version = "2.21"
 description = "C parser in Python"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -1729,7 +1640,6 @@ files = [
 name = "pydantic"
 version = "1.10.13"
 description = "Data validation and settings management using python type hints"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1782,7 +1692,6 @@ email = ["email-validator (>=1.0.3)"]
 name = "pygments"
 version = "2.16.1"
 description = "Pygments is a syntax highlighting package written in Python."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1797,7 +1706,6 @@ plugins = ["importlib-metadata"]
 name = "pyjsg"
 version = "0.11.10"
 description = "Python JSON Schema Grammar interpreter"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1813,7 +1721,6 @@ jsonasobj = ">=1.2.1"
 name = "pyparsing"
 version = "3.1.1"
 description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "main"
 optional = false
 python-versions = ">=3.6.8"
 files = [
@@ -1828,7 +1735,6 @@ diagrams = ["jinja2", "railroad-diagrams"]
 name = "pyproject-api"
 version = "1.5.3"
 description = "API to interact with the python pyproject.toml based projects"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1848,7 +1754,6 @@ testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1
 name = "pyrsistent"
 version = "0.19.3"
 description = "Persistent/Functional/Immutable data structures"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1885,7 +1790,6 @@ files = [
 name = "pyshex"
 version = "0.8.1"
 description = "Python ShEx Implementation"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1908,7 +1812,6 @@ urllib3 = "*"
 name = "pyshexc"
 version = "0.9.1"
 description = "PyShExC - Python ShEx compiler"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1928,7 +1831,6 @@ shexjsg = ">=0.8.1"
 name = "pytest"
 version = "7.4.2"
 description = "pytest: simple powerful testing with Python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1951,7 +1853,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
 name = "pytest-logging"
 version = "2015.11.4"
 description = "Configures logging and allows tweaking the log level with a py.test flag"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1965,7 +1866,6 @@ pytest = ">=2.8.1"
 name = "pytest-subtests"
 version = "0.11.0"
 description = "unittest subTest() support and subtests fixture"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1981,7 +1881,6 @@ pytest = ">=7.0"
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -1996,7 +1895,6 @@ six = ">=1.5"
 name = "pytrie"
 version = "0.4.0"
 description = "A pure Python implementation of the trie data structure."
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2010,7 +1908,6 @@ sortedcontainers = "*"
 name = "pytz"
 version = "2023.3.post1"
 description = "World timezone definitions, modern and historical"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2022,7 +1919,6 @@ files = [
 name = "pywin32"
 version = "306"
 description = "Python for Window Extensions"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2046,7 +1942,6 @@ files = [
 name = "pyyaml"
 version = "6.0.1"
 description = "YAML parser and emitter for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2096,7 +1991,6 @@ files = [
 name = "pyzmq"
 version = "25.1.1"
 description = "Python bindings for 0MQ"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2200,34 +2094,29 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
 
 [[package]]
 name = "rdflib"
-version = "6.2.0"
+version = "7.0.0"
 description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information."
-category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8.1,<4.0.0"
 files = [
-    {file = "rdflib-6.2.0-py3-none-any.whl", hash = "sha256:85c34a86dfc517a41e5f2425a41a0aceacc23983462b32e68610b9fad1383bca"},
-    {file = "rdflib-6.2.0.tar.gz", hash = "sha256:62dc3c86d1712db0f55785baf8047f63731fa59b2682be03219cb89262065942"},
+    {file = "rdflib-7.0.0-py3-none-any.whl", hash = "sha256:0438920912a642c866a513de6fe8a0001bd86ef975057d6962c79ce4771687cd"},
+    {file = "rdflib-7.0.0.tar.gz", hash = "sha256:9995eb8569428059b8c1affd26b25eac510d64f5043d9ce8c84e0d0036e995ae"},
 ]
 
 [package.dependencies]
-isodate = "*"
-pyparsing = "*"
-setuptools = "*"
+isodate = ">=0.6.0,<0.7.0"
+pyparsing = ">=2.1.0,<4"
 
 [package.extras]
-berkeleydb = ["berkeleydb"]
-dev = ["black (==22.6.0)", "flake8", "flakeheaven", "isort", "mypy", "pep8-naming", "types-setuptools"]
-docs = ["myst-parser", "sphinx (<6)", "sphinx-autodoc-typehints", "sphinxcontrib-apidoc", "sphinxcontrib-kroki"]
-html = ["html5lib"]
-networkx = ["networkx"]
-tests = ["html5lib", "pytest", "pytest-cov"]
+berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"]
+html = ["html5lib (>=1.0,<2.0)"]
+lxml = ["lxml (>=4.3.0,<5.0.0)"]
+networkx = ["networkx (>=2.0.0,<3.0.0)"]
 
 [[package]]
 name = "rdflib-jsonld"
 version = "0.6.1"
 description = "rdflib extension adding JSON-LD parser and serializer"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2242,7 +2131,6 @@ rdflib = ">=5.0.0"
 name = "rdflib-shim"
 version = "1.0.3"
 description = "Shim for rdflib 5 and 6 incompatibilities"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2258,7 +2146,6 @@ rdflib-jsonld = "0.6.1"
 name = "requests"
 version = "2.31.0"
 description = "Python HTTP for Humans."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2280,7 +2167,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 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.*"
 files = [
@@ -2295,7 +2181,6 @@ six = "*"
 name = "rfc3987"
 version = "1.3.8"
 description = "Parsing and validation of URIs (RFC 3986) and IRIs (RFC 3987)"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2307,7 +2192,6 @@ files = [
 name = "ruamel-yaml"
 version = "0.17.33"
 description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
-category = "main"
 optional = false
 python-versions = ">=3"
 files = [
@@ -2326,7 +2210,6 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
 name = "ruamel-yaml-clib"
 version = "0.2.7"
 description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2337,8 +2220,7 @@ files = [
     {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"},
     {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"},
     {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"},
-    {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"},
-    {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"},
+    {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"},
     {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"},
     {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"},
     {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"},
@@ -2373,7 +2255,6 @@ files = [
 name = "setuptools"
 version = "68.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2390,7 +2271,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
 name = "shexjsg"
 version = "0.8.2"
 description = "ShExJSG - Astract Syntax Tree for the ShEx 2.0 language"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2405,7 +2285,6 @@ pyjsg = ">=0.11.10"
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -2417,7 +2296,6 @@ files = [
 name = "snowballstemmer"
 version = "2.2.0"
 description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2429,7 +2307,6 @@ files = [
 name = "sortedcontainers"
 version = "2.4.0"
 description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2441,7 +2318,6 @@ files = [
 name = "soupsieve"
 version = "2.4.1"
 description = "A modern CSS selector implementation for Beautiful Soup."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2453,7 +2329,6 @@ files = [
 name = "sparqlslurper"
 version = "0.5.1"
 description = "SPARQL Slurper for rdflib"
-category = "main"
 optional = false
 python-versions = ">=3.7.4"
 files = [
@@ -2470,7 +2345,6 @@ sparqlwrapper = ">=1.8.2"
 name = "sparqlwrapper"
 version = "2.0.0"
 description = "SPARQL Endpoint interface to Python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2491,7 +2365,6 @@ pandas = ["pandas (>=1.3.5)"]
 name = "sphinx"
 version = "5.3.0"
 description = "Python documentation generator"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2527,7 +2400,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
 name = "sphinx-basic-ng"
 version = "1.0.0b2"
 description = "A modern skeleton for Sphinx themes."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2545,7 +2417,6 @@ docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-ta
 name = "sphinx-click"
 version = "4.4.0"
 description = "Sphinx extension that automatically documents click applications"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2562,7 +2433,6 @@ sphinx = ">=2.0"
 name = "sphinx-rtd-theme"
 version = "0.5.1"
 description = "Read the Docs theme for Sphinx"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2580,7 +2450,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"]
 name = "sphinxcontrib-applehelp"
 version = "1.0.2"
 description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2596,7 +2465,6 @@ test = ["pytest"]
 name = "sphinxcontrib-devhelp"
 version = "1.0.2"
 description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2612,7 +2480,6 @@ test = ["pytest"]
 name = "sphinxcontrib-htmlhelp"
 version = "2.0.0"
 description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2628,7 +2495,6 @@ test = ["html5lib", "pytest"]
 name = "sphinxcontrib-jsmath"
 version = "1.0.1"
 description = "A sphinx extension which renders display math in HTML via JavaScript"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2643,7 +2509,6 @@ test = ["flake8", "mypy", "pytest"]
 name = "sphinxcontrib-mermaid"
 version = "0.7.1"
 description = "Mermaid diagrams in yours Sphinx powered docs"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2655,7 +2520,6 @@ files = [
 name = "sphinxcontrib-qthelp"
 version = "1.0.3"
 description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2671,7 +2535,6 @@ test = ["pytest"]
 name = "sphinxcontrib-serializinghtml"
 version = "1.1.5"
 description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -2687,7 +2550,6 @@ test = ["pytest"]
 name = "sqlalchemy"
 version = "2.0.21"
 description = "Database Abstraction Library"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2707,14 +2569,6 @@ files = [
     {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"},
     {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"},
     {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:56628ca27aa17b5890391ded4e385bf0480209726f198799b7e980c6bd473bd7"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db726be58837fe5ac39859e0fa40baafe54c6d54c02aba1d47d25536170b690f"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7421c1bfdbb7214313919472307be650bd45c4dc2fcb317d64d078993de045b"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:632784f7a6f12cfa0e84bf2a5003b07660addccf5563c132cd23b7cc1d7371a9"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7276cf26145a888f2182a98f204541b519d9ea358a65d82095d9c9e22f917"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2a1f7ffac934bc0ea717fa1596f938483fb8c402233f9b26679b4f7b38d6ab6e"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-win32.whl", hash = "sha256:bfece2f7cec502ec5f759bbc09ce711445372deeac3628f6fa1c16b7fb45b682"},
-    {file = "SQLAlchemy-2.0.21-cp312-cp312-win_amd64.whl", hash = "sha256:526b869a0f4f000d8d8ee3409d0becca30ae73f494cbb48801da0129601f72c6"},
     {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"},
     {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"},
     {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"},
@@ -2743,7 +2597,7 @@ files = [
 ]
 
 [package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
+greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""}
 typing-extensions = ">=4.2.0"
 
 [package.extras]
@@ -2774,7 +2628,6 @@ sqlcipher = ["sqlcipher3-binary"]
 name = "tinycss2"
 version = "1.2.1"
 description = "A tiny CSS parser"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2793,7 +2646,6 @@ test = ["flake8", "isort", "pytest"]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2805,7 +2657,6 @@ files = [
 name = "tornado"
 version = "6.2"
 description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
-category = "dev"
 optional = false
 python-versions = ">= 3.7"
 files = [
@@ -2826,7 +2677,6 @@ files = [
 name = "tox"
 version = "4.8.0"
 description = "tox is a generic virtualenv management and test command line tool"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2854,7 +2704,6 @@ testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pol
 name = "traitlets"
 version = "5.9.0"
 description = "Traitlets Python configuration system"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2870,7 +2719,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
 name = "typing-extensions"
 version = "4.7.1"
 description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2882,7 +2730,6 @@ files = [
 name = "uri-template"
 version = "1.3.0"
 description = "RFC 6570 URI Template Processor"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2897,7 +2744,6 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
 name = "urllib3"
 version = "2.0.5"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2915,7 +2761,6 @@ zstd = ["zstandard (>=0.18.0)"]
 name = "virtualenv"
 version = "20.24.5"
 description = "Virtual Python Environment builder"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2936,7 +2781,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
 name = "watchdog"
 version = "3.0.0"
 description = "Filesystem events monitoring"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2976,7 +2820,6 @@ watchmedo = ["PyYAML (>=3.10)"]
 name = "wcwidth"
 version = "0.2.7"
 description = "Measures the displayed width of unicode strings in a terminal"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2988,7 +2831,6 @@ files = [
 name = "webcolors"
 version = "1.13"
 description = "A library for working with the color formats defined by HTML and CSS."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -3004,7 +2846,6 @@ tests = ["pytest", "pytest-cov"]
 name = "webencodings"
 version = "0.5.1"
 description = "Character encoding aliases for legacy web content"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -3016,7 +2857,6 @@ files = [
 name = "wrapt"
 version = "1.15.0"
 description = "Module for decorators, wrappers and monkey patching."
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 files = [
@@ -3101,7 +2941,6 @@ files = [
 name = "zipp"
 version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -3118,5 +2957,5 @@ docs = []
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.8"
-content-hash = "d56d10ab69335c3a5439d1fe631b193c78094ef5527d2558b2e2a86f884bd41a"
+python-versions = "^3.8.1"
+content-hash = "7d0f8f7c25b9fd8aeb9fb565a92b7246b373d0f46aceadfbec6652f441d800bb"
diff --git a/pyproject.toml b/pyproject.toml
index e43f5e75..6b8891bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -97,7 +97,7 @@ gen-linkml = "linkml.generators.linkmlgen:cli"
 linkml-schema-fixer = "linkml.utils.schema_fixer:main"
 
 [tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.8.1"
 antlr4-python3-runtime = "<4.10,==4.*,>=4.9.0"
 click = ">=7.0"
 graphviz = ">=0.10.1"


Test patch:
diff --git a/tests/test_compliance/test_boolean_slot_compliance.py b/tests/test_compliance/test_boolean_slot_compliance.py
index 737c21a4..5975fccf 100644
--- a/tests/test_compliance/test_boolean_slot_compliance.py
+++ b/tests/test_compliance/test_boolean_slot_compliance.py
@@ -20,8 +20,10 @@ from tests.test_compliance.test_compliance import (
     CLASS_U1,
     CLASS_U2,
     CORE_FRAMEWORKS,
+    SLOT_ID,
     SLOT_S1,
     SLOT_S2,
+    SLOT_S3,
     CLASS_C1a,
     CLASS_C1b,
     SLOT_S1a,
@@ -30,6 +32,7 @@ from tests.test_compliance.test_compliance import (
 
 
 @pytest.mark.parametrize("use_default_range", [False, True])
+@pytest.mark.parametrize("use_any_type", [False, True])
 @pytest.mark.parametrize(
     "data_name,value,is_valid",
     [
@@ -37,10 +40,11 @@ from tests.test_compliance.test_compliance import (
         ("int", 1, True),
         ("str", "abc", False),
         ("obj", {SLOT_S2: "abc"}, True),
+        ("bad_obj", {SLOT_S1: "abc"}, False),
     ],
 )
 @pytest.mark.parametrize("framework", CORE_FRAMEWORKS)
-def test_any_of(framework, data_name, value, is_valid, use_default_range):
+def test_any_of(framework, data_name, value, is_valid, use_any_type, use_default_range):
     """
     Tests behavior of any_of.
 
@@ -51,21 +55,23 @@ def test_any_of(framework, data_name, value, is_valid, use_default_range):
     as it cleanly maps to many language constructs (e.g. Union in Python),
     and it is often possible to normalize complex combinations to any_of form.
 
+    TODO: resolve issues around combining any_of with default ranges or
+    asserted ranges, see https://github.com/linkml/linkml/issues/1483
+
     :param framework: generator to test
     :param data_name: unique identifier for the test data instance
     :param value: data value to use in the instance
     :param is_valid: whether the test instance is expected to be valid
+    :param use_any_type: if True, assert additional range using linkml:Any
     :param use_default_range: if True, the default range will be included in addition to any_of.
     :return:
     """
-    expected_json_schema = {
-        "s1": {
-            # "$ref": "#/$defs/Any",
-            "anyOf": [{"$ref": "#/$defs/D"}, {"type": "integer"}]
-        }
-    }
-    if use_default_range:
-        # Note: this is redundant and may be removed in the future
+    expected_json_schema = {"s1": {"anyOf": [{"$ref": "#/$defs/D"}, {"type": "integer"}]}}
+    if use_default_range and not use_any_type:
+        # TODO: undesired behavior, see https://github.com/linkml/linkml/issues/1483
+        expected_json_schema["s1"]["type"] = "string"
+    if use_any_type:
+        # TODO: undesired behavior, see https://github.com/linkml/linkml/issues/1483
         expected_json_schema["s1"]["$ref"] = "#/$defs/Any"
     classes = {
         CLASS_D: {
@@ -94,15 +100,17 @@ def test_any_of(framework, data_name, value, is_valid, use_default_range):
     }
     if use_default_range:
         default_range = "string"
+    else:
+        default_range = None
+    if use_any_type:
         classes[CLASS_ANY] = {
             "class_uri": "linkml:Any",
         }
         classes[CLASS_C]["attributes"][SLOT_S1]["range"] = CLASS_ANY
-    else:
-        default_range = None
+
     schema = validated_schema(
         test_any_of,
-        f"DefaultRangeEQ_{default_range}",
+        f"DefaultRangeEQ_{default_range}_AnyTypeEQ_{use_any_type}",
         framework,
         classes=classes,
         default_range=default_range,
@@ -111,9 +119,14 @@ def test_any_of(framework, data_name, value, is_valid, use_default_range):
     expected_behavior = ValidationBehavior.IMPLEMENTS
     if framework in [PYTHON_DATACLASSES, SQL_DDL_SQLITE]:
         expected_behavior = ValidationBehavior.INCOMPLETE
-    if framework == JSON_SCHEMA and use_default_range:
-        expected_behavior = ValidationBehavior.INCOMPLETE
-    # TODO: rdflib transformer has issues around ranges
+    if framework == JSON_SCHEMA:
+        if use_default_range and not is_valid:
+            # https://github.com/linkml/linkml/issues/1483
+            expected_behavior = ValidationBehavior.INCOMPLETE
+        if data_name == "bad_obj":
+            expected_behavior = ValidationBehavior.INCOMPLETE
+        if is_valid and (use_any_type or use_default_range):
+            expected_behavior = ValidationBehavior.FALSE_POSITIVE
     check_data(
         schema,
         data_name,
@@ -420,9 +433,9 @@ def test_class_any_of(framework, data_name, s1value, s2value, is_valid):
 
     :param framework: generator to test
     :param data_name: unique identifier for the test data instance
-    :param value: data value to use in the instance
+    :param s1value: value for slot S1
+    :param s2value: value for slot S2
     :param is_valid: whether the test instance is expected to be valid
-    :param use_default_range: if True, the default range will be included in addition to any_of.
     :return:
     """
     slots = {
@@ -479,6 +492,783 @@ def test_class_any_of(framework, data_name, s1value, s2value, is_valid):
     )
 
 
+@pytest.mark.parametrize(
+    "schema_name,s1_range,s2_range,op,s1_expression,s2_expression,data_name,s1value,s2value,is_valid",
+    [
+        # strings
+        (
+            "any_of_streq",
+            "string",
+            "string",
+            "any_of",
+            {"equals_string": "x"},
+            {"equals_string": "y"},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_streq",
+            "string",
+            "string",
+            "any_of",
+            {"equals_string": "x"},
+            {"equals_string": "y"},
+            "both",
+            "x",
+            "y",
+            True,
+        ),
+        (
+            "any_of_streq",
+            "string",
+            "string",
+            "any_of",
+            {"equals_string": "x"},
+            {"equals_string": "y"},
+            "first",
+            "x",
+            "z",
+            True,
+        ),
+        (
+            "any_of_streq",
+            "string",
+            "string",
+            "any_of",
+            {"equals_string": "x"},
+            {"equals_string": "y"},
+            "second",
+            "z",
+            "y",
+            True,
+        ),
+        (
+            "any_of_streq",
+            "string",
+            "string",
+            "any_of",
+            {"equals_string": "x"},
+            {"equals_string": "y"},
+            "neither",
+            "z",
+            "z",
+            False,
+        ),
+        # list of strings
+        (
+            "any_of_streq_MV",
+            "string*",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x"]},
+            {"equals_string_in": ["y"]},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "both",
+            ["x"],
+            ["y"],
+            True,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "first",
+            ["x"],
+            ["z"],
+            True,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "second",
+            ["z"],
+            ["y"],
+            True,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "neither",
+            ["x", "z"],
+            ["y", "z"],
+            False,
+        ),
+        # strings, object reference
+        (
+            "any_of_streq_ref",
+            "D",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "both",
+            "TEST:x",
+            "TEST:y",
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "first",
+            "TEST:x",
+            "TEST:z",
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "second",
+            "TEST:z",
+            "TEST:y",
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "neither",
+            "TEST:z",
+            "TEST:z",
+            False,
+        ),
+        # ints, all_of
+        (
+            "all_of_min_min_INT",
+            "integer",
+            "integer",
+            "all_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "all_of_min_min_INT",
+            "integer",
+            "integer",
+            "all_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "both",
+            20,
+            20,
+            True,
+        ),
+        (
+            "all_of_min_min_INT",
+            "integer",
+            "integer",
+            "all_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "first",
+            20,
+            0,
+            False,
+        ),
+        (
+            "all_of_min_min_INT",
+            "integer",
+            "integer",
+            "all_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "second",
+            0,
+            20,
+            False,
+        ),
+        (
+            "all_of_min_min_INT",
+            "integer",
+            "integer",
+            "all_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "neither",
+            0,
+            0,
+            False,
+        ),
+        # ints, none_of
+        (
+            "none_of_min_min_INT",
+            "integer",
+            "integer",
+            "none_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "none_of_min_min_INT",
+            "integer",
+            "integer",
+            "none_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "both",
+            20,
+            20,
+            False,
+        ),
+        (
+            "none_of_min_min_INT",
+            "integer",
+            "integer",
+            "none_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "first",
+            20,
+            0,
+            False,
+        ),
+        (
+            "none_of_min_min_INT",
+            "integer",
+            "integer",
+            "none_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "second",
+            0,
+            20,
+            False,
+        ),
+        (
+            "none_of_min_min_INT",
+            "integer",
+            "integer",
+            "none_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "neither",
+            0,
+            0,
+            True,
+        ),
+        # ints, any_of
+        (
+            "any_of_min_min_INT",
+            "integer",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_min_min_INT",
+            "integer",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "both",
+            20,
+            20,
+            True,
+        ),
+        (
+            "any_of_min_min_INT",
+            "integer",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "first",
+            20,
+            0,
+            True,
+        ),
+        (
+            "any_of_min_min_INT",
+            "integer",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "second",
+            0,
+            20,
+            True,
+        ),
+        (
+            "any_of_min_min_INT",
+            "integer",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "neither",
+            0,
+            0,
+            False,
+        ),
+        # floats, any_of
+        (
+            "any_of_min_min_FLOAT",
+            "float",
+            "float",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_min_min_FLOAT",
+            "float",
+            "float",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "both",
+            20.0,
+            20.0,
+            True,
+        ),
+        (
+            "any_of_min_min_FLOAT",
+            "float",
+            "float",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "first",
+            20.0,
+            0.0,
+            True,
+        ),
+        (
+            "any_of_min_min_FLOAT",
+            "float",
+            "float",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "second",
+            0.0,
+            20.0,
+            True,
+        ),
+        (
+            "any_of_min_min_FLOAT",
+            "float",
+            "float",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "neither",
+            0.0,
+            0.0,
+            False,
+        ),
+        # Any type, any_of
+        (
+            "any_of_min_min_ANY",
+            "Any",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "none",
+            None,
+            None,
+            True,
+        ),
+        (
+            "any_of_min_min_ANY",
+            "Any",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "both",
+            20,
+            20,
+            True,
+        ),
+        (
+            "any_of_min_min_ANY",
+            "Any",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "first",
+            20,
+            0,
+            True,
+        ),
+        (
+            "any_of_min_min_ANY",
+            "Any",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "second",
+            0,
+            20,
+            True,
+        ),
+        (
+            "any_of_min_min_ANY",
+            "Any",
+            "integer",
+            "any_of",
+            {"minimum_value": 10},
+            {"minimum_value": 20},
+            "neither",
+            0,
+            0,
+            False,
+        ),
+    ],
+)
+@pytest.mark.parametrize("framework", CORE_FRAMEWORKS)
+def test_class_boolean_with_expressions(
+    framework, schema_name, s1_range, s2_range, op, s1_expression, s2_expression, data_name, s1value, s2value, is_valid
+):
+    """
+    Tests behavior of any_of for class expressions.
+
+    :param framework: generator to test
+    :param schema_name: unique name of generated schema
+    :param s1_range: range of slot 1
+    :param s2_range: range of slot 2
+    :param op: linkml boolean operation to use
+    :param s1_expression: expression for slot 1
+    :param s2_expression: expression for slot 2
+    :param data_name: name of data to generate
+    :param s1value: value for slot 1
+    :param s2value: value for slot 2
+    :param is_valid: whether the data should be valid
+    :return:
+    """
+    if (s1_range == CLASS_ANY or s2_range == CLASS_ANY) and framework not in [PYDANTIC]:
+        pytest.skip("Class Any not supported in this test")
+    if s1_range.endswith("*"):
+        s1_multivalued = True
+        s1_range = s1_range[:-1]
+    else:
+        s1_multivalued = False
+    if s2_range.endswith("*"):
+        s2_multivalued = True
+        s2_range = s2_range[:-1]
+    else:
+        s2_multivalued = False
+    slots = {
+        SLOT_S1: {
+            "range": s1_range,
+            "multivalued": s1_multivalued,
+        },
+        SLOT_S2: {
+            "range": s2_range,
+            "multivalued": s2_multivalued,
+        },
+        SLOT_S3: {
+            "range": "string",
+        },
+        SLOT_ID: {
+            "range": "string",
+            "identifier": True,
+        },
+    }
+    classes = {
+        CLASS_ANY: {
+            "class_uri": "linkml:Any",
+        },
+        CLASS_D: {
+            "slots": [SLOT_ID, SLOT_S3],
+        },
+        CLASS_C: {
+            "slots": [SLOT_S1, SLOT_S2],
+            op: [
+                {
+                    "slot_conditions": {
+                        SLOT_S1: s1_expression,
+                    },
+                },
+                {
+                    "slot_conditions": {
+                        SLOT_S2: s2_expression,
+                    },
+                },
+            ],
+        },
+    }
+    schema = validated_schema(
+        test_class_boolean_with_expressions,
+        schema_name,
+        framework,
+        prefixes={
+            "TEST": "https://example.org/TEST/",
+        },
+        classes=classes,
+        slots=slots,
+        core_elements=["any_of", "ClassDefinition"],
+    )
+    expected_behavior = ValidationBehavior.IMPLEMENTS
+    if not is_valid and framework not in [OWL]:
+        expected_behavior = ValidationBehavior.INCOMPLETE
+    if framework == OWL:
+        if s1_range == "float" or s2_range == "float":
+            # TODO: investigate this
+            expected_behavior = ValidationBehavior.FALSE_POSITIVE
+        if s1_range == "D" or s2_range == "D":
+            # expected: OWL is open world
+            expected_behavior = ValidationBehavior.INCOMPLETE
+
+    check_data(
+        schema,
+        f"{schema_name}-{data_name}",
+        framework,
+        {SLOT_S1: s1value, SLOT_S2: s2value},
+        is_valid,
+        target_class=CLASS_C,
+        expected_behavior=expected_behavior,
+        description=f"validity {is_valid} check for value {s1value}, {s2value}",
+        # exclude_rdf=True,
+    )
+
+
+@pytest.mark.parametrize(
+    "schema_name,range,op,expression1,expression2,data_name,value,is_valid",
+    [
+        # strings
+        ("any_of_streq", "string", "any_of", {"equals_string": "x"}, {"equals_string": "y"}, "none", None, True),
+        ("any_of_streq", "string", "any_of", {"equals_string": "x"}, {"equals_string": "y"}, "matches", "x", True),
+        ("any_of_streq", "string", "any_of", {"equals_string": "x"}, {"equals_string": "y"}, "no_matches", "z", False),
+        # list of strings
+        (
+            "any_of_streq_MV",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x"]},
+            {"equals_string_in": ["y"]},
+            "none",
+            None,
+            True,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "one",
+            ["y", "z"],
+            False,
+        ),
+        (
+            "any_of_streq_MV",
+            "string*",
+            "any_of",
+            {"equals_string_in": ["x", "a"]},
+            {"equals_string_in": ["y", "a"]},
+            "neither",
+            ["z"],
+            False,
+        ),
+        # strings, object reference
+        (
+            "any_of_streq_ref",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "none",
+            None,
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "match",
+            "TEST:x",
+            True,
+        ),
+        (
+            "any_of_streq_ref",
+            "D",
+            "any_of",
+            {"equals_string": "TEST:x"},
+            {"equals_string": "TEST:y"},
+            "neither",
+            "TEST:z",
+            False,
+        ),
+        # ints, all_of
+        ("all_of_min_min_INT", "integer", "all_of", {"minimum_value": 10}, {"minimum_value": 20}, "none", None, True),
+        ("all_of_min_min_INT", "integer", "all_of", {"minimum_value": 10}, {"minimum_value": 20}, "both", 20, True),
+        ("all_of_min_min_INT", "integer", "all_of", {"minimum_value": 10}, {"minimum_value": 20}, "one", 10, False),
+        ("all_of_min_min_INT", "integer", "all_of", {"minimum_value": 10}, {"minimum_value": 20}, "neither", 0, False),
+        # ints, none_of
+        ("none_of_min_min_INT", "integer", "none_of", {"minimum_value": 10}, {"minimum_value": 20}, "none", None, True),
+        ("none_of_min_min_INT", "integer", "none_of", {"minimum_value": 10}, {"minimum_value": 20}, "both", 20, False),
+        ("none_of_min_min_INT", "integer", "none_of", {"minimum_value": 10}, {"minimum_value": 20}, "first", 10, False),
+        ("none_of_min_min_INT", "integer", "none_of", {"minimum_value": 10}, {"minimum_value": 20}, "neither", 0, True),
+        # ints, any_of
+        ("any_of_min_min_INT", "integer", "any_of", {"minimum_value": 10}, {"minimum_value": 20}, "none", None, True),
+        ("any_of_min_min_INT", "integer", "any_of", {"minimum_value": 10}, {"minimum_value": 20}, "both", 20, True),
+        ("any_of_min_min_INT", "integer", "any_of", {"minimum_value": 10}, {"minimum_value": 20}, "first", 10, True),
+        ("any_of_min_min_INT", "integer", "any_of", {"minimum_value": 10}, {"minimum_value": 20}, "neither", 0, False),
+    ],
+)
+@pytest.mark.parametrize("framework", CORE_FRAMEWORKS)
+def test_slot_boolean_with_expressions(
+    framework, schema_name, range, op, expression1, expression2, data_name, value, is_valid
+):
+    """
+    Tests behavior of any_of for slot expressions.
+
+    :param framework: generator to test
+    :param schema_name: unique name of generated schema
+    :param range: range of slot
+    :param op: linkml boolean operation to use (combines expression1 and expression2)
+    :param expression1: first expression
+    :param expression2: second expression
+    :param data_name: unique name of generated data
+    :param value: value to test
+    :param is_valid: expected validity of value
+    :return:
+    """
+    if range.endswith("*"):
+        multivalued = True
+        range = range[:-1]
+    else:
+        multivalued = False
+    slots = {
+        SLOT_S1: {
+            "range": range,
+            "multivalued": multivalued,
+        },
+        SLOT_S3: {
+            "range": "string",
+        },
+        SLOT_ID: {
+            "range": "string",
+            "identifier": True,
+        },
+    }
+    classes = {
+        CLASS_ANY: {
+            "class_uri": "linkml:Any",
+        },
+        CLASS_D: {
+            "slots": [SLOT_ID, SLOT_S3],
+        },
+        CLASS_C: {
+            "slots": [SLOT_S1],
+            "slot_usage": {
+                SLOT_S1: {
+                    op: [expression1, expression2],
+                },
+            },
+        },
+    }
+    schema = validated_schema(
+        test_slot_boolean_with_expressions,
+        schema_name,
+        framework,
+        prefixes={
+            "TEST": "https://example.org/TEST/",
+        },
+        classes=classes,
+        slots=slots,
+        core_elements=["any_of", "ClassDefinition"],
+    )
+    expected_behavior = ValidationBehavior.IMPLEMENTS
+    if not is_valid and framework not in [OWL, JSON_SCHEMA]:
+        expected_behavior = ValidationBehavior.INCOMPLETE
+    if framework == JSON_SCHEMA:
+        if multivalued and not is_valid:
+            # https://github.com/linkml/linkml/issues/1677
+            expected_behavior = ValidationBehavior.INCOMPLETE
+    if framework == OWL:
+        if range == "float":
+            # TODO: investigate this
+            expected_behavior = ValidationBehavior.FALSE_POSITIVE
+        if range == "D":
+            # expected: OWL is open world
+            expected_behavior = ValidationBehavior.INCOMPLETE
+
+    check_data(
+        schema,
+        f"{schema_name}-{data_name}",
+        framework,
+        {SLOT_S1: value},
+        is_valid,
+        target_class=CLASS_C,
+        expected_behavior=expected_behavior,
+        description=f"validity {is_valid} check for value {value}",
+        # exclude_rdf=True,
+    )
+
+
 @pytest.mark.parametrize(
     "value",
     [1, 10, 15, 20, 21],
@@ -499,6 +1289,7 @@ def test_min_max(framework, min_val, max_val, equals_number: Optional[int], valu
     :param framework: generator to test
     :param min_val: minimum value for slot in schema
     :param max_val: maximum value for slot in schema
+    :param equals_number: equals_number in schema
     :param value: value of slot in data to test with
     :return:
     """
@@ -587,9 +1378,9 @@ def test_preconditions(framework, s1, s2, is_valid):
     S2 cannot be either 0 or 10. The test then checks the validity of the data.
 
     :param framework: generator to test
-    :param data_name: name of data to test with
-    :param value: value of slot in data to test with
-    :param is_valid: whether the data is valid or not
+    :param s1: value of slot S1
+    :param s2: value of slot S2
+    :param is_valid: whether the data is valid
     :return:
     """
     classes = {
@@ -672,9 +1463,10 @@ def test_classification_rules(framework, s1, s1a, s1b, is_valid):
     Tests behavior of classification rules.
 
     :param framework: generator to test
-    :param data_name: name of data to test with
-    :param value: value of slot in data to test with
-    :param is_valid: whether the data is valid or not
+    :param s1: value of slot S1
+    :param s1a: value of slot S1a
+    :param s1b: value of slot S1b
+    :param is_valid: whether the data is valid
     :return:
     """
     classes = {
diff --git a/tests/test_compliance/test_jsonpointer_compliance.py b/tests/test_compliance/test_jsonpointer_compliance.py
new file mode 100644
index 00000000..d35e56ed
--- /dev/null
+++ b/tests/test_compliance/test_jsonpointer_compliance.py
@@ -0,0 +1,141 @@
+import pytest
+
+from tests.test_compliance.helper import (
+    ValidationBehavior,
+    check_data,
+    validated_schema,
+)
+from tests.test_compliance.test_compliance import CLASS_C, CORE_FRAMEWORKS, SLOT_ID
+
+
+@pytest.mark.parametrize(
+    "data_name,data,invalid_reason",
+    [
+        (
+            "simple",
+            {
+                "id": "object1234",
+                "source": [
+                    {"property": "#/height", "provenance": "fooSource"},
+                    {"property": "#/width", "provenance": "barSource"},
+                    {"property": "#/address[0]", "provenance": "fizzSource"},
+                    {"property": "#/address[1]", "provenance": "bazSource"},
+                ],
+                "height": 10.0,
+                "width": 20.0,
+                "address": ["120 Main St", "100 Second St"],
+            },
+            None,
+        ),
+        (
+            "invalid_jsonpointer",
+            {
+                "id": "object1234",
+                "source": [
+                    {"property": "#/height", "provenance": "fooSource"},
+                    {"property": "#/width", "provenance": "barSource"},
+                    {"property": "#/address[0]", "provenance": "fizzSource"},
+                    {"property": "#/address[1]", "provenance": "bazSource"},
+                ],
+                "height": 10.0,
+                "width": 20.0,
+                "address": ["120 Main St", "100 Second St"],
+            },
+            "pointer to a non-existent property",
+        ),
+        (
+            "invalid_array_element",
+            {
+                "id": "object1234",
+                "source": [
+                    {"property": "#/height", "provenance": "fooSource"},
+                    {"property": "#/width", "provenance": "barSource"},
+                    {"property": "#/address[0]", "provenance": "fizzSource"},
+                    {"property": "#/address[99]", "provenance": "bazSource"},
+                ],
+                "height": 10.0,
+                "width": 20.0,
+                "address": ["120 Main St", "100 Second St"],
+            },
+            "array out of bounds",
+        ),
+    ],
+)
+@pytest.mark.parametrize("framework", CORE_FRAMEWORKS)
+def test_jsonpointer(framework, data_name, data, invalid_reason):
+    """
+    Tests behavior of json pointers.
+
+    Note: this feature is under discussion
+    and incompletely implemented. See:
+
+    https://github.com/linkml/linkml/issues/1469
+
+    :param framework: all should support built-in types
+    :param data_name: id for data instance
+    :param data: value to check
+    :param invalid_reason: reason for invalidity (valid if None)
+    :return:
+    """
+    CLASS_SOURCE = "Source"
+    classes = {
+        CLASS_SOURCE: {
+            "attributes": {
+                "property": {
+                    "range": "jsonpointer",
+                },
+                "provenance": {
+                    "description": "Provenance of the assignment property points at",
+                },
+            }
+        },
+        CLASS_C: {
+            "attributes": {
+                SLOT_ID: {
+                    "identifier": True,
+                },
+                "height": {
+                    "range": "float",
+                },
+                "width": {
+                    "range": "float",
+                },
+                "address": {
+                    "multivalued": True,
+                },
+                "source": {
+                    "description": "Key value to lookup source of S1/S2 assignment",
+                    "range": CLASS_SOURCE,
+                    "multivalued": True,
+                },
+            }
+        },
+    }
+    schema = validated_schema(
+        test_jsonpointer,
+        "default",
+        framework,
+        classes=classes,
+        core_elements=["jsonpointer"],
+    )
+    expected_behavior = ValidationBehavior.IMPLEMENTS
+    is_valid = invalid_reason is None
+    if not is_valid:
+        # validation behavior is not yet implemented for ANY framework.
+        # precise behavior is yet to be defined, but at a minimum
+        # any jsonpointer should be valid syntax and should resolve
+        # to a valid object within the json/tree form of the document.
+        # TODO decide:
+        #   - do we want to allow dangling pointers to reference a None assignment?
+        #   - should only inlined references be traversed?
+        expected_behavior = ValidationBehavior.INCOMPLETE
+    check_data(
+        schema,
+        data_name,
+        framework,
+        data,
+        is_valid,
+        expected_behavior=expected_behavior,
+        target_class=CLASS_C,
+        description=data_name,
+    )
diff --git a/tests/test_generators/test_docgen.py b/tests/test_generators/test_docgen.py
index 01037a12..18b8e71d 100644
--- a/tests/test_generators/test_docgen.py
+++ b/tests/test_generators/test_docgen.py
@@ -4,6 +4,7 @@ Tests generation of markdown and similar documents
 Note that docgen replaces markdowngen
 """
 import logging
+import os
 from collections import Counter
 from copy import copy
 from typing import List
@@ -426,6 +427,15 @@ def test_custom_directory(kitchen_sink_path, input_path, tmp_path):
     assert_mdfile_contains(tmp_path / "Organization.md", "FAKE TEMPLATE")
 
 
+def test_gen_custom_named_index(kitchen_sink_path, tmp_path):
+    """Tests that the name of the index page can be customized"""
+    gen = DocGenerator(kitchen_sink_path, index_name="custom-index")
+    gen.serialize(directory=str(tmp_path))
+    assert_mdfile_contains(tmp_path / "custom-index.md", "# Kitchen Sink Schema")
+    # Additionally test that the default index.md has NOT been created
+    assert not os.path.exists(tmp_path / "index.md")
+
+
 def test_html(kitchen_sink_path, input_path, tmp_path):
     """
     Tests ability to specify a complete new set of templates in a different format
diff --git a/tests/test_generators/test_javagen.py b/tests/test_generators/test_javagen.py
index a002c579..bcb225a5 100644
--- a/tests/test_generators/test_javagen.py
+++ b/tests/test_generators/test_javagen.py
@@ -1,25 +1,9 @@
 from linkml.generators.javagen import JavaGenerator
+from tests.utils.fileutils import assert_file_contains
 
 PACKAGE = "org.sink.kitchen"
 
 
-# TODO: move this somewhere reusable
-def assert_file_contains(filename, text, after=None, description=None) -> None:
-    found = False
-    is_after = False  # have we reached the after mark?
-    with open(filename) as stream:
-        for line in stream.readlines():
-            if text in line:
-                if after is None:
-                    found = True
-                else:
-                    if is_after:
-                        found = True
-            if after is not None and after in line:
-                is_after = True
-    assert found
-
-
 def test_javagen_records(kitchen_sink_path, tmp_path):
     """Generate java records"""
     gen = JavaGenerator(kitchen_sink_path, package=PACKAGE, generate_records=True)
diff --git a/tests/test_generators/test_rdfgen.py b/tests/test_generators/test_rdfgen.py
index 7b2ea859..41c135bc 100644
--- a/tests/test_generators/test_rdfgen.py
+++ b/tests/test_generators/test_rdfgen.py
@@ -35,5 +35,5 @@ def test_rdfgen(kitchen_sink_path):
 def test_rdf_type_in_jsonld(self):
     graph = Graph()
     graph.parse(data=JSONLD, format="json-ld", prefix=True)
-    ttl_str = graph.serialize(format="turtle").decode()
+    ttl_str = graph.serialize(format="turtle")
     graph.parse(data=ttl_str, format="turtle")
diff --git a/tests/test_issues/test_issue_103.py b/tests/test_issues/test_issue_103.py
index e006ef9f..4ae714af 100644
--- a/tests/test_issues/test_issue_103.py
+++ b/tests/test_issues/test_issue_103.py
@@ -18,9 +18,9 @@ def test_jsonld_prefix():
     """
 
     g = Graph().parse(data=test_json, format="json-ld", prefix=True)
-    rdfstr = g.serialize(format="turtle").decode()
+    rdfstr = g.serialize(format="turtle")
     assert "@prefix CHEBI: <http://purl.obolibrary.org/obo/CHEBI_>" in rdfstr
 
     g = Graph().parse(data=test_json, format="json-ld", prefix=False)
-    rdfstr = g.serialize(format="turtle").decode()
+    rdfstr = g.serialize(format="turtle")
     assert "@prefix CHEBI: <http://purl.obolibrary.org/obo/CHEBI_>" not in rdfstr
diff --git a/tests/test_issues/test_issue_80.py b/tests/test_issues/test_issue_80.py
index 9d8878ea..e2658748 100644
--- a/tests/test_issues/test_issue_80.py
+++ b/tests/test_issues/test_issue_80.py
@@ -20,5 +20,5 @@ def test_issue_80(input_path, snapshot):
     jsonld_context_output = ContextGenerator(input_path("issue_80.yaml")).serialize()
     assert jsonld_context_output == snapshot("issue_80.context.jsonld")
 
-    rdf_output = as_rdf(example, contexts=jsonld_context_output).serialize(format="turtle").decode()
+    rdf_output = as_rdf(example, contexts=jsonld_context_output).serialize(format="turtle")
     assert rdf_output == snapshot("issue_80.ttl")
diff --git a/tests/test_issues/test_linkml_issue_1525.py b/tests/test_issues/test_linkml_issue_1525.py
new file mode 100644
index 00000000..196d8193
--- /dev/null
+++ b/tests/test_issues/test_linkml_issue_1525.py
@@ -0,0 +1,38 @@
+from linkml.generators.javagen import JavaGenerator
+from tests.utils.fileutils import assert_file_contains
+
+schema_str = """
+id: http://example.com/person
+name: datetest
+prefixes:
+  linkml: https://w3id.org/linkml/
+imports:
+  - linkml:types
+
+classes:
+  SomethingWithDate:
+    slots:
+      - date_and_time
+      - date_only
+      - time_only
+
+slots:
+  date_and_time:
+    range: datetime
+  date_only:
+    range: date
+  time_only:
+    range: time
+"""
+
+
+def test_javagen_with_date_slots(tmp_path):
+    """
+    Check that date and time slots are rendered with the correct types in Java code.
+
+    See https://github.com/linkml/linkml/issues/1525.
+    """
+    JavaGenerator(schema_str).serialize(directory=str(tmp_path))
+    assert_file_contains(tmp_path / "SomethingWithDate.java", "ZonedDateTime dateAndTime")
+    assert_file_contains(tmp_path / "SomethingWithDate.java", "LocalDate dateOnly")
+    assert_file_contains(tmp_path / "SomethingWithDate.java", "Instant timeOnly")
diff --git a/tests/test_notebooks/input/examples.py b/tests/test_notebooks/input/examples.py
index 179caea7..60576d0c 100644
--- a/tests/test_notebooks/input/examples.py
+++ b/tests/test_notebooks/input/examples.py
@@ -108,7 +108,7 @@ jsonld = json_dumper.dumps(joe_smith, cntxt)
 print(jsonld)
 g = Graph()
 g.parse(data=jsonld, format="json-ld")
-print(g.serialize(format="turtle").decode())
+print(g.serialize(format="turtle"))
 
 
 shex = ShExGenerator(yaml).serialize(collections=False)
diff --git a/tests/test_notebooks/input/inheritance.py b/tests/test_notebooks/input/inheritance.py
index 5ee0565f..0cee7b98 100644
--- a/tests/test_notebooks/input/inheritance.py
+++ b/tests/test_notebooks/input/inheritance.py
@@ -88,7 +88,7 @@ print(shex)
 # print(jsonld)
 # g = Graph()
 # g.parse(data=jsonld, format="json-ld")
-# print(g.serialize(format="turtle").decode())
+# print(g.serialize(format="turtle"))
 #
 # from pyshex.evaluate import evaluate
 #
diff --git a/tests/test_scripts/test_gen_jsonld.py b/tests/test_scripts/test_gen_jsonld.py
index 9c0a9093..b91f7653 100644
--- a/tests/test_scripts/test_gen_jsonld.py
+++ b/tests/test_scripts/test_gen_jsonld.py
@@ -156,7 +156,7 @@ class GenJSONLDTestCase(ClickTestCase):
         g.load(tmp_jsonld_path, format="json-ld")
         g.serialize(tmp_rdf_path, format="ttl")
         g.bind("meta", METAMODEL_NAMESPACE)
-        new_ttl = g.serialize(format="turtle").decode()
+        new_ttl = g.serialize(format="turtle")
 
         # Make sure that the generated TTL matches the JSON-LD (probably not really needed, as this is more of a test
         # of rdflib than our tooling but it doesn't hurt
diff --git a/tests/test_utils/test_metamodelcore.py b/tests/test_utils/test_metamodelcore.py
index 6d3ff304..1507cec6 100644
--- a/tests/test_utils/test_metamodelcore.py
+++ b/tests/test_utils/test_metamodelcore.py
@@ -250,7 +250,7 @@ class MetamodelCoreTest(unittest.TestCase):
     rdfs:subject "http://example.org/tests/descendant1" .
 
 """,
-            as_rdf(y, context).serialize(format="turtle").decode(),
+            as_rdf(y, context).serialize(format="turtle"),
         )
         with self.assertRaises(ValueError):
             y = Pair(s, s)
diff --git a/tests/test_utils/test_uri_and_curie.py b/tests/test_utils/test_uri_and_curie.py
index 0395a3b2..5704047d 100644
--- a/tests/test_utils/test_uri_and_curie.py
+++ b/tests/test_utils/test_uri_and_curie.py
@@ -58,7 +58,7 @@ class URIAndCurieTestCase(GeneratorTestCase):
         )
         env.eval_single_file(
             env.expected_path("uriandcurie.ttl"),
-            g.serialize(format="ttl").decode(),
+            g.serialize(format="ttl"),
             lambda s: s,
             compare_rdf,
         )
diff --git a/tests/utils/compare_rdf.py b/tests/utils/compare_rdf.py
index 636011c0..db842a7b 100644
--- a/tests/utils/compare_rdf.py
+++ b/tests/utils/compare_rdf.py
@@ -38,7 +38,7 @@ def print_triples(g: Graph) -> None:
     Print the contents of g into stdout
     :param g: graph to print
     """
-    g_text = re.sub(r"@prefix.*\n", "", g.serialize(format="turtle").decode())
+    g_text = re.sub(r"@prefix.*\n", "", g.serialize(format="turtle"))
     print(g_text)
 
 
diff --git a/tests/utils/fileutils.py b/tests/utils/fileutils.py
new file mode 100644
index 00000000..16476f8a
--- /dev/null
+++ b/tests/utils/fileutils.py
@@ -0,0 +1,21 @@
+def assert_file_contains(filename, text, after=None) -> None:
+    """
+    Check that a file contains a specific bit of text in a line, and raise an assertion failure if it does not.
+
+    :param filename: the name of the file to be checked
+    :param text: the motif that must be present in the file
+    :param after: if present, the text to search for must appear after a line containing this
+    """
+    found = False
+    is_after = False
+    with open(filename) as stream:
+        for line in stream.readlines():
+            if text in line:
+                if after is None:
+                    found = True
+                else:
+                    if is_after:
+                        found = True
+            if after is not None and after in line:
+                is_after = True
+    assert found
diff --git a/tests/utils/generatortestcase.py b/tests/utils/generatortestcase.py
index 4378aebb..d9f619c7 100644
--- a/tests/utils/generatortestcase.py
+++ b/tests/utils/generatortestcase.py
@@ -20,7 +20,7 @@ class GeneratorTestCase(TestEnvironmentTestCase):
         g.bind("BIOLINK", BIOLINK_NS)
         g.bind("meta", METAMODEL_NAMESPACE)
         g.bind("owl", OWL)
-        g_text = re.sub(r"@prefix.*\n", "", g.serialize(format="turtle").decode())
+        g_text = re.sub(r"@prefix.*\n", "", g.serialize(format="turtle"))
         print(g_text)
 
     def single_file_generator(