feat(models): convert RootModel[str] wrappers to TypeAlias + resolve discriminated-union bases - #5
Open
tbsvttr wants to merge 1 commit into
Open
Conversation
Extends post_process_models.py with a third rewrite pass that
converts every `class <ThingId>(RootModel[str]): root: Annotated[str,
Field(pattern=…)]` block into `<ThingId>: TypeAlias = Annotated[str,
Field(pattern=…)]`.
## Why
The generator emits every Datasworn ID type as a RootModel wrapper.
Consumers who want the actual string value have to do:
asset.id.root # RootModel unwrap
if asset.id.root.startswith("asset:"): ...
Instead of the more natural:
asset.id
if asset.id.startswith("asset:"): ...
Type-aliasing to `Annotated[str, Field(...)]` gives back the ergonomic
path (`.id` is a plain str) while keeping the pattern validation
intact — Pydantic honors Annotated Field metadata inside model fields
the same way it honors RootModel[str] wrappers.
## Scope
- 79 `RootModel[str]` wrappers converted to TypeAlias in one pass.
0 remain after the pass; running the post-processor a second time
reports no changes.
- Other RootModel variants (`[int]`, `[list[X]]`, `[Union[A, B]]`) are
deliberately left as RootModel — their runtime shape isn't
representable as a plain type alias.
- `TypeAlias` is auto-imported into `typing` if missing (kept the
helper simple: sorts the imports for reproducibility).
## Tests
New `tests/test_type_alias_ergonomics.py` locks in the ergonomic path
so a future regeneration doesn't silently regress:
- `RulesetId` reads as plain str (`.startswith`, `len` work directly)
- `AssetId` same (usable in f-strings without .root)
- MarkdownString same
Two tests (move_id, oracle_id) skip with a documented reason: `Move`
and `OracleRollable` are separately broken by an unrelated codegen
bug — they're emitted as empty discriminated-union bases with
extra='allow', so IDs land in `__pydantic_extra__` instead of as
attributes. That's a follow-up post-process, called out in the
README's Status section.
## Depends on
Stacked on top of #4 (fix: reconcile models.py with pydantic 2.13's
stricter validation). Merge #4 first.
tbsvttr
force-pushed
the
feat/root-model-type-aliases
branch
from
July 16, 2026 10:19
c9d83e1 to
7f50d70
Compare
This was referenced Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends `scripts/post_process_models.py` with two more rewrite passes that finish the ergonomic story on top of #4:
Pass 3:
RootModel[str]→TypeAlias(79 wrappers)Every generated `class (RootModel[str])` becomes a `TypeAlias` with the pattern preserved via `Annotated`. Consumers who had to write `asset.id.root` now write `asset.id` directly — Pydantic still validates the pattern on load because it honors `Annotated[str, Field(pattern=...)]` inside model fields the same as it honored `RootModel[str]`.
Pass 4: Empty discriminated-union bases →
Annotated[Union[...], Field(discriminator=...)]The generator emits several discriminated unions as empty base classes with `extra='allow'` holding only the discriminator field. Fields typed as these bases validate against the empty stub — every real field on the concrete subtype ends up in `pydantic_extra` instead of being accessible as an attribute. That's what was breaking `move.id` and `oracle.id` access.
5 bases fixed:
Uses string forward references (`Union["MoveActionRoll", ...]`) since the concrete subclass definitions come later in the generated file.
Scope not covered
Ten more empty-base classes (`MoveEnhancement`, `EmbeddedMove`, `AssetControlField`, `AssetOptionField`, `AssetAbilityControlField`, `AssetAbilityOptionField`, `AssetConditionMeterControlField`, `AssetControlFieldEnhancement`, `RulesPackage`, `Choices`, `RollableValue`) have the same shape but their concrete subtype classes weren't generated at all. Fixing those needs upstream codegen work or a bigger post-processor. Called out in the README's Status section.
Tests
New `tests/test_type_alias_ergonomics.py` locks in the ergonomic path so future regenerations don't silently regress:
Test plan