From 0091b9ca87d2903cf412ca3668656e8961af9d07 Mon Sep 17 00:00:00 2001 From: Isaac To Date: Thu, 6 Aug 2026 12:01:08 -0700 Subject: [PATCH 1/2] refactor: narrow multi-value `schemaKey` `Literal`s to a single value `Contributor` and `Activity` admitted their subclasses' keys alongside their own. `DandiBaseModel.ensure_schemakey` pins every instance to its own class name, so the extra values were unreachable at runtime, and in the generated JSON Schema they produced a `const` and a contradicting `enum` for the same property: the post-processing in `__get_pydantic_json_schema__` collapses an `enum` into a `const` only when the `enum` holds a single member. The widened `Literal`s were load-bearing for `mypy` rather than for validation, in that they made each subclass override a subtype of the base annotation. Narrowing them therefore needs `# type: ignore[assignment]` on the five subclass overrides, following the precedent set by `Asset.schemaKey`. Co-Authored-By: Claude Code 2.1.223 / claude-opus-5 --- dandischema/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dandischema/models.py b/dandischema/models.py index c91afad0..52eccb26 100644 --- a/dandischema/models.py +++ b/dandischema/models.py @@ -928,7 +928,7 @@ class Contributor(DandiBaseModel): description="Identifier associated with a sponsored or gift award.", json_schema_extra={"nskey": DANDI_NSKEY}, ) - schemaKey: Literal["Contributor", "Organization", "Person"] = Field( + schemaKey: Literal["Contributor"] = Field( "Contributor", validate_default=True, json_schema_extra={"readOnly": True} ) @@ -965,7 +965,7 @@ class Organization(Contributor): description="Contact for the organization", json_schema_extra={"nskey": "schema"}, ) - schemaKey: Literal["Organization"] = Field( + schemaKey: Literal["Organization"] = Field( # type: ignore[assignment] "Organization", validate_default=True, json_schema_extra={"readOnly": True} ) _ldmeta = { @@ -1014,7 +1014,7 @@ class Person(Contributor): description="An organization that this person is affiliated with.", json_schema_extra={"nskey": "schema"}, ) - schemaKey: Literal["Person"] = Field( + schemaKey: Literal["Person"] = Field( # type: ignore[assignment] "Person", validate_default=True, json_schema_extra={"readOnly": True} ) @@ -1289,7 +1289,7 @@ class Activity(DandiBaseModel): description="A listing of equipment used for the activity.", json_schema_extra={"nskey": "prov"}, ) - schemaKey: Literal["Activity", "Project", "Session", "PublishActivity"] = Field( + schemaKey: Literal["Activity"] = Field( "Activity", validate_default=True, json_schema_extra={"readOnly": True} ) @@ -1311,7 +1311,7 @@ class Project(Activity): description="A brief description of the project.", json_schema_extra={"nskey": "schema"}, ) - schemaKey: Literal["Project"] = Field( + schemaKey: Literal["Project"] = Field( # type: ignore[assignment] "Project", validate_default=True, json_schema_extra={"readOnly": True} ) @@ -1328,13 +1328,13 @@ class Session(Activity): description="A brief description of the session.", json_schema_extra={"nskey": "schema"}, ) - schemaKey: Literal["Session"] = Field( + schemaKey: Literal["Session"] = Field( # type: ignore[assignment] "Session", validate_default=True, json_schema_extra={"readOnly": True} ) class PublishActivity(Activity): - schemaKey: Literal["PublishActivity"] = Field( + schemaKey: Literal["PublishActivity"] = Field( # type: ignore[assignment] "PublishActivity", validate_default=True, json_schema_extra={"readOnly": True} ) From e45422b97bc0b525dfc81596606fac261924f3f8 Mon Sep 17 00:00:00 2001 From: Isaac To Date: Thu, 6 Aug 2026 12:02:54 -0700 Subject: [PATCH 2/2] refactor: express `wasGeneratedBy` as an explicit union of activity types `CommonModel.wasGeneratedBy` was annotated `Sequence[Activity]`, which reads as "any `Activity` subclass" but reduces to "exactly `Activity`" for JSON input, since `ensure_schemakey` rejects a subclass key on `Activity` itself. It now lists the activity types outright: `Activity`, `Project`, `PublishActivity`, and `Session`. `BareAsset` no longer narrows that set. Its `List[Union[Session, Project, Activity]]` admitted `Activity` yet turned away `PublishActivity`, which is inconsistent, as every `PublishActivity` is an `Activity`. LinkML cannot express such a set either once `schemaKey` serves as the type designator, since a class-valued range expands over the class's descendants, whether that class stands as the range itself or as one of the alternatives within an `any_of`. The field is redeclared only for a `title` and a `description`, reworded after `Dandiset.wasGeneratedBy`. The old pair named a class set that no longer holds and treated the value as names, as in "Name of the session, project or activity." `Sequence` gives way to `list`. 23e56855, in #100, introduced it only to sidestep `list`'s invariance for `Dandiset`'s override, which now takes a `# type: ignore[assignment]` instead. Co-Authored-By: Claude Code 2.1.223 / claude-opus-5 --- dandischema/models.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dandischema/models.py b/dandischema/models.py index 52eccb26..791e8142 100644 --- a/dandischema/models.py +++ b/dandischema/models.py @@ -11,7 +11,6 @@ List, Literal, Optional, - Sequence, Type, TypeVar, Union, @@ -1637,9 +1636,9 @@ class CommonModel(DandiBaseModel): None, json_schema_extra={"nskey": DANDI_NSKEY} ) - wasGeneratedBy: Optional[Sequence[Activity]] = Field( - None, json_schema_extra={"nskey": "prov"} - ) + wasGeneratedBy: Optional[ + list[Union[Activity, Project, PublishActivity, Session]] + ] = Field(None, json_schema_extra={"nskey": "prov"}) schemaKey: str = Field( "CommonModel", validate_default=True, json_schema_extra={"readOnly": True} ) @@ -1784,7 +1783,7 @@ def contributor_musthave_contact( json_schema_extra={"readOnly": True, "nskey": "schema"}, ) - wasGeneratedBy: Optional[Sequence[Project]] = Field( + wasGeneratedBy: Optional[list[Project]] = Field( # type: ignore[assignment] None, title="Associated projects", description="Project(s) that generated this Dandiset.", @@ -1927,10 +1926,12 @@ class BareAsset(CommonModel): description="Associated participant(s) or subject(s).", json_schema_extra={"nskey": "prov"}, ) - wasGeneratedBy: Optional[List[Union[Session, Project, Activity]]] = Field( + wasGeneratedBy: Optional[ + list[Union[Activity, Project, PublishActivity, Session]] + ] = Field( None, - title="Name of the session, project or activity.", - description="Describe the session, project or activity that generated this asset.", + title="Associated activities", + description="Activities that generated this asset.", json_schema_extra={"nskey": "prov"}, )