Skip to content

refactor: eliminate multiple value choices in schemaKey Literals - #437

Open
candleindark wants to merge 2 commits into
masterfrom
eliminate-multiple-schemaKey-value-choices
Open

refactor: eliminate multiple value choices in schemaKey Literals#437
candleindark wants to merge 2 commits into
masterfrom
eliminate-multiple-schemaKey-value-choices

Conversation

@candleindark

@candleindark candleindark commented Aug 6, 2026

Copy link
Copy Markdown
Member

Eliminates the multiple value choices in the schemaKey Literals of Contributor and Activity, the last two classes that offered more than one.

The extra choices were unreachable at runtime, since ensure_schemakey pins every instance to its own class name, and in the generated JSON Schema they produced a const and a contradicting enum on the same property. They existed to keep mypy quiet, so removing them costs # type: ignore[assignment] on five subclass overrides, as Asset.schemaKey already does.

That exposed wasGeneratedBy, which the second commit adjusts:

  • CommonModel.wasGeneratedBy becomes list[Union[Activity, Project, PublishActivity, Session]]. Its former Sequence[Activity] reads as "any Activity subclass", but for JSON input it accepted only Activity itself.
  • BareAsset stops narrowing that set and keeps the field only for a reworded title and description.
  • Sequence gives way to list.
Why the multiple value choices are a problem

The schemaKey post-processing in __get_pydantic_json_schema__ collapses enum into const only for a single-member enum, so both classes emitted the two keys at once. For Activity:

"schemaKey": {"const": "Activity", "default": "Activity",
              "enum": ["Activity", "Project", "Session", "PublishActivity"]}

A validator has to satisfy both, so the effective constraint was already just "Activity" while the enum advertised three values the schema would never accept. Anything reading the schema to enumerate valid keys, a UI or a code generator, saw the wrong set.

Why BareAsset.wasGeneratedBy goes away

List[Union[Session, Project, Activity]] admitted Activity yet turned away PublishActivity, which is inconsistent, since every PublishActivity is an Activity.

That set is also inexpressible in LinkML once schemaKey serves as the type designator: a class-valued range expands over the class's descendants, whether the class stands as the range itself or as an alternative within an any_of. See designates_type: true makes generated artifacts resolve a superclass-typed slot value to its concrete subtype in the migration playbook.

The consequence for this field is recorded there too: the BareAsset.wasGeneratedBy range override in models_merge.yaml is a deliberate no-op. gen-pydantic already renders CommonModel.wasGeneratedBy as exactly the union proposed here, and the override renders as a nested union that flattens to the same four members. That finding says the override should be revisited once the Pydantic definition is corrected, which is what this PR does.

Separately, the old title, "Name of the session, project or activity.", described a name field rather than a list of activities, and both strings named a class set that no longer holds. They now follow Dandiset.wasGeneratedBy: title="Associated activities", description="Activities that generated this asset."

Why Sequence gives way to list

Sequence was never a modeling decision. 23e56855, part of #100, the PR that first added mypy here, switched both fields from List to Sequence purely to sidestep list's invariance for Dandiset's narrowing override, and the name appears nowhere else in models.py.

Pydantic's Sequences documentation says "In most cases, you will want to use the built-in types (such as list or tuple) as type coercion will apply", reserving Sequence for "when you want to preserve the input type during serialization", which these models do not need given they round-trip through JSON. Concretely, dandi-cli calls .append on BareAsset.wasGeneratedBy, which Sequence does not provide.

Both defects entered together, incidentally: f224281a introduced all three multi-value Literals and 23e56855 introduced Sequence three minutes later, both in #100, neither with a validation rationale.

Behavior change and downstream check

Asset and BareAsset metadata now accept a PublishActivity in wasGeneratedBy, which the JSON Schema previously rejected. This is a widening only, so nothing previously valid becomes invalid.

  • dandi-cli's dandi/metadata/core.py calls .append on BareAsset.wasGeneratedBy, which type-checks again now that the field is a list.
  • dandi-archive has no Python reference to wasGeneratedBy; it handles metadata as dicts through dandischema.metadata.validate / migrate. Its frontend wasGeneratedBy (web/src/types/schema.ts, typed Project[]) is the Dandiset field, whose accepted class set is unchanged here: Dandiset.wasGeneratedBy only moves from Sequence[Project] to list[Project].

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.96%. Comparing base (510f161) to head (e45422b).

Files with missing lines Patch % Lines
dandischema/models.py 0.00% 10 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #437   +/-   ##
=======================================
  Coverage   47.96%   47.96%           
=======================================
  Files          19       19           
  Lines        2427     2427           
=======================================
  Hits         1164     1164           
  Misses       1263     1263           
Flag Coverage Δ
unittests 47.96% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`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 <noreply@anthropic.com>
@candleindark
candleindark force-pushed the eliminate-multiple-schemaKey-value-choices branch from 652c023 to 75c5301 Compare August 6, 2026 19:55
@candleindark
candleindark marked this pull request as ready for review August 6, 2026 21:28
@candleindark candleindark changed the title refactor: eliminate multi-value schemaKey Literals refactor: eliminate multiple value choices in schemaKey Literals Aug 6, 2026
@candleindark candleindark added patch Increment the patch version when merged bug Something isn't working labels Aug 6, 2026
…ypes

`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`. 23e5685, 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 <noreply@anthropic.com>
@yarikoptic
yarikoptic force-pushed the eliminate-multiple-schemaKey-value-choices branch from 75c5301 to e45422b Compare August 7, 2026 19:30
@yarikoptic yarikoptic added the release Create a release when this pr is merged label Aug 7, 2026
@yarikoptic

Copy link
Copy Markdown
Member

I added release just to see how schemata changes or not... to be removed before merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working patch Increment the patch version when merged release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants