Skip to content

Fix serialization of unions with nested PEP 695 type aliases#331

Open
PraneelBhatia wants to merge 2 commits into
Fatal1ty:masterfrom
PraneelBhatia:fix-pep695-aliases-in-unions
Open

Fix serialization of unions with nested PEP 695 type aliases#331
PraneelBhatia wants to merge 2 commits into
Fatal1ty:masterfrom
PraneelBhatia:fix-pep695-aliases-in-unions

Conversation

@PraneelBhatia

Copy link
Copy Markdown

Fixes #330

When a union contains a PEP 695 type alias as one of its members, serializer generation crashed with TypeError: issubclass() arg 1 must be a class, because pack_union passed the raw TypeAliasType object to issubclass(get_type_origin(type_arg), Collection). The deserialization path had the symmetric problem: UnionUnpackerBuilder generated type-match conditions from the alias's __name__, which raised NameError at decode time.

Changes:

  • Added a resolve_type_alias_type() helper to mashumaro/core/meta/helpers.py that fully resolves TypeAliasType objects, including parameterized aliases (type ListOf[T] = list[T]) via substitute_type_params.
  • Applied it to union members in pack_union and UnionUnpackerBuilder._add_body.
  • Refactored the existing alias-unwrapping branches in pack_special_typing_primitive and unpack_special_typing_primitive to use the same helper (behavior unchanged).
  • Added regression tests covering the issue's repro, the TO_DICT_ADD_OMIT_NONE_FLAG variant with eager and lazy compilation, codecs, and parameterized aliases as union members. All fail before the fix.

Copilot AI review requested due to automatic review settings June 5, 2026 18:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves Mashumaro’s support for PEP 695 TypeAliasType when aliases are used inside Union/| types (including parameterized aliases), and adds regression tests covering dict serialization, omit-none behavior, and codecs.

Changes:

  • Add resolve_type_alias_type() helper to centralize TypeAliasType unwrapping (including parameter substitution for parameterized aliases).
  • Apply alias resolution in pack/unpack union handling and in special typing primitive paths.
  • Add tests reproducing issue #330 and additional union+alias scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/test_pep_695.py Adds regression tests for TypeAliasType nested in unions (incl. omit-none flag + codecs).
mashumaro/core/meta/types/unpack.py Resolves type aliases before selecting union unpackers and consolidates alias handling.
mashumaro/core/meta/types/pack.py Resolves type aliases before selecting union packers and consolidates alias handling.
mashumaro/core/meta/helpers.py Introduces resolve_type_alias_type() helper for reuse across pack/unpack.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +647 to +658
def resolve_type_alias_type(typ: Type) -> Type:
while True:
if is_type_alias_type(typ):
typ = typ.__value__
elif is_type_alias_type(get_type_origin(typ)):
origin = get_type_origin(typ)
type_params = getattr(origin, "__type_params__", ())
args = get_args(typ)
param_map = dict(zip(type_params, args))
typ = substitute_type_params(origin.__value__, param_map)
else:
return typ

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added cycle protection in 829f7c8: the resolver tracks seen values and bounds the number of resolution steps, raising TypeError: Cannot resolve recursive type alias ... for both exact cycles (type A = B; type B = A) and non-repeating chains (type G[T] = G[list[T]]). The seen set tracks values rather than id()s because each substitution builds a new (equal) object for parameterized cycles; is_hashable() guards unhashable values and the step bound is the backstop. Raising a targeted error seemed more useful than returning the unresolved alias, which would just push the non-termination into the packer/unpacker dispatch. Tests cover both shapes, including at dataclass creation time.

Comment on lines +653 to +656
type_params = getattr(origin, "__type_params__", ())
args = get_args(typ)
param_map = dict(zip(type_params, args))
typ = substitute_type_params(origin.__value__, param_map)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the silent truncation in 829f7c8: missing trailing parameters now fall back to their PEP 696 defaults when defined (TypeAliasType subscription doesn't auto-fill defaults, so Pair[int] with TypeVar("V", default=str) previously leaked an unsubstituted ~V into the resolved type), and a genuine mismatch raises TypeError: Too few/Too many arguments for ...; actual N, expected M in the same format as the existing generics arity check. The check is skipped for TypeVarTuple/ParamSpec parameters since their subscription arity is flexible. One behavior note: invalid annotations like Pair[int] without a default used to serialize with a half-substituted value and now fail at class creation — happy to relax that if you'd rather keep them lenient.

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.29577% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.91%. Comparing base (e6894ef) to head (829f7c8).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
mashumaro/core/meta/helpers.py 96.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #331      +/-   ##
==========================================
- Coverage   99.92%   99.91%   -0.01%     
==========================================
  Files         101      101              
  Lines       12007    12131     +124     
==========================================
+ Hits        11998    12121     +123     
- Misses          9       10       +1     

☔ 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PEP 695 aliases nested inside unions can crash serializer generation

2 participants