Skip to content

Conversation

@jlowin
Copy link
Collaborator

@jlowin jlowin commented Dec 22, 2025

The PydanticAdapter was previously limited to BaseModel subclasses and list[BaseModel]. This change generalizes it to support any type that Pydantic can serialize, including:

  • Sequences: list[int], tuple[str, ...], set[float]
  • Dicts: dict[str, int]
  • Primitives: datetime, UUID, int, str
  • Dataclasses and TypedDicts
# All of these now work with proper type inference:
PydanticAdapter[list[int]](store, pydantic_model=list[int])
PydanticAdapter[datetime](store, pydantic_model=datetime)
PydanticAdapter[dict[str, int]](store, pydantic_model=dict[str, int])
PydanticAdapter[tuple[int, str]](store, pydantic_model=tuple[int, str])

Types that naturally serialize to dicts (BaseModel, dataclass, TypedDict, dict) are stored directly. All other types are wrapped in {"items": <value>} for consistent dict-based storage.

Uses TypeForm[T] from typing_extensions to properly type the pydantic_model parameter, enabling type checkers to accept generic aliases like list[Tool].

This change is required for fastmcp's caching middleware, which uses PydanticAdapter[list[Tool]], PydanticAdapter[list[Resource]], etc.

Summary by CodeRabbit

  • New Features

    • PydanticAdapter now supports many more pydantic-serializable types (collections, dataclasses, TypedDict, primitives, datetime, etc.) and automatically wraps non-dict values for storage when needed.
  • Tests

    • Added round-trip tests for the new type variants, including checks that dict-like values are stored without wrapping.
  • Documentation

    • Minor table/formatting adjustments in adapter and store docs.

✏️ Tip: You can customize this high-level summary in your review settings.

Support list[int], tuple, set, dict, datetime, and other types beyond
just BaseModel. Uses TypeForm[T] for proper type checker support.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Warning

Rate limit exceeded

@jlowin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a83b189 and da2a9c1.

📒 Files selected for processing (3)
  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py
  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/base.py
  • key-value/key-value-sync/tests/code_gen/adapters/test_pydantic.py
📝 Walkthrough

Walkthrough

PydanticAdapter now supports any pydantic-serializable type (BaseModel, dataclass, TypedDict, dict, sequences, primitives). Non-dict-serializable values are stored wrapped as {"items": ...}. Internal helpers and state were added and _is_list_model was renamed to _needs_wrapping.

Changes

Cohort / File(s) Summary
Pydantic adapter core
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py
Reworked PydanticAdapter to accept TypeForm[T] and a general TypeVar("T"), initialize a pydantic TypeAdapter, unwrap Annotated[...], detect dataclass/TypedDict/sequence origins, and decide wrapping. Added _type_adapter, _needs_wrapping, _check_needs_wrapping, _serializes_to_dict, updated imports and docstring; implements storing non-dict-serializable types as {"items": ...}.
Pydantic adapter base
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py
Renamed _is_list_model_needs_wrapping, updated validation/serialization branches and error messages to reflect generalized wrapping semantics and adjusted logger context.
Tests
key-value/key-value-aio/tests/adapters/test_pydantic.py
Added round-trip tests for additional type hints: list[int], tuple[int, str, float], set[int], datetime, and dict[str, int]; verifies dicts are stored raw (no items wrapper).
Docs / Minor formatting
AGENTS.md, docs/adapters.md, docs/stores.md, docs/wrappers.md
Cosmetic table/header formatting changes; no behavioral changes.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title precisely captures the main change: generalizing PydanticAdapter from BaseModel-only to any pydantic-serializable type, matching the PR's core objective.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6736113 and 8bca0b5.

📒 Files selected for processing (3)
  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py
  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py
  • key-value/key-value-aio/tests/adapters/test_pydantic.py
🧰 Additional context used
🧬 Code graph analysis (3)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py (5)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (1)
  • _get_model_type_name (97-99)
key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/base.py (1)
  • _get_model_type_name (36-42)
key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py (1)
  • _get_model_type_name (53-55)
key-value/key-value-sync/src/key_value/sync/code_gen/adapters/dataclass/adapter.py (1)
  • _get_model_type_name (69-71)
key-value/key-value-shared/src/key_value/shared/errors/key_value.py (1)
  • DeserializationError (14-15)
key-value/key-value-aio/tests/adapters/test_pydantic.py (3)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (1)
  • PydanticAdapter (16-99)
key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py (1)
  • PydanticAdapter (17-55)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py (4)
  • put (199-208)
  • get (126-126)
  • get (129-129)
  • get (131-158)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (2)
key-value/key-value-aio/src/key_value/aio/protocols/key_value.py (1)
  • AsyncKeyValue (185-190)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py (1)
  • BasePydanticAdapter (18-268)
🪛 GitHub Actions: Run Tests
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py

[error] 57-57: PLR0911 Too many return statements (8 > 6)


[error] 80-80: SIM103 Return the negated condition directly

🔇 Additional comments (13)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py (3)

27-27: LGTM: Field rename correctly generalizes wrapping logic.

The rename from _is_list_model to _needs_wrapping accurately reflects the expanded type support. All references consistently use the new name.

Also applies to: 59-59, 117-117


44-46: LGTM: Docstrings accurately describe the wrapping strategy.

The updated docstrings clearly explain the distinction between dict-serializing types (stored directly) and wrapped types (stored as {"items": value}).

Also applies to: 102-105


62-62: LGTM: Error messaging generalized for broader type support.

Removing "list" from error messages and adding structured logging context correctly supports the expanded type coverage.

Also applies to: 67-74

key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (5)

1-8: LGTM: Type system changes enable generic alias support.

The shift from type[T] to TypeForm[T] and the unbound TypeVar correctly enable parameterization with generic aliases like list[int]. The new imports support the expanded type introspection logic.

Also applies to: 13-13, 36-36


17-28: LGTM: Documentation clearly describes expanded type support.

The docstring accurately lists all supported type categories and explains the wrapping convention.

Also applies to: 44-50


52-55: LGTM: Initialization correctly delegates wrapping decision.

Creating the TypeAdapter and computing _needs_wrapping via the helper method is the right approach.


64-68: LGTM: Recursive Annotated handling is correct.

Unwrapping Annotated[T, ...] to the underlying type ensures proper wrapping detection for annotated types.


71-95: LGTM: Wrapping logic correctly distinguishes dict-serializable types.

The method correctly identifies:

  • Dict-serializable (no wrap): BaseModel, dataclass, TypedDict, dict, Mapping
  • Needs wrapping: sequences, primitives, and all other types

The defensive isinstance and try/except guards prevent TypeErrors from introspection edge cases.

key-value/key-value-aio/tests/adapters/test_pydantic.py (5)

124-128: LGTM: Test validates list[int] support.

Correctly verifies round-trip behavior for a simple list of primitives.


227-232: LGTM: Test validates tuple support.

Correctly tests fixed-length tuple serialization with mixed types.


234-239: LGTM: Test validates set support.

Correctly verifies set round-trip behavior.


241-247: LGTM: Test validates datetime support.

Correctly tests primitive type wrapping with a timezone-aware datetime.


249-262: LGTM: Test validates dict storage without wrapping.

Excellent test that not only verifies round-trip behavior but also inspects raw storage to confirm dict types are stored directly without the {"items": ...} wrapper.

@claude
Copy link

claude bot commented Dec 22, 2025

Test Failure Analysis

Summary: The workflow failed due to 2 Ruff linting violations in the _check_needs_wrapping method of the PydanticAdapter class.

Root Cause: The new _check_needs_wrapping method introduced in this PR has:

  1. SIM103 (Return the negated condition directly) - Lines 80-83 in adapter.py can be simplified
  2. PLR0911 (Too many return statements) - The method has 8 return statements, exceeding the configured limit of 6

Suggested Solution:

Fix SIM103 Violation

In key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py, replace lines 80-83:

# Current code (lines 80-83):
if is_typeddict(pydantic_model):  # pyright: ignore[reportUnknownArgumentType]
    return False
# Everything else: int, str, datetime, UUID, Enum, etc.
return True

With:

# Simplified version:
# TypedDict serializes to dict; everything else needs wrapping
return not is_typeddict(pydantic_model)  # pyright: ignore[reportUnknownArgumentType]

Fix PLR0911 Violation (Too Many Return Statements)

You have two options:

Option 1: Disable the rule for this specific method by adding a comment:

def _check_needs_wrapping(self, pydantic_model: Any) -> bool:  # noqa: PLR0911
    """Determine if this type serializes to a non-dict and needs wrapping.

Option 2: Refactor to reduce return statements (more complex, but cleaner):
Consolidate some of the logic to reduce branches. For example, combine the BaseModel and dataclass checks.

Important Notes

  • After fixing the async code (key-value-aio), run make codegen to regenerate the sync library
  • Run make precommit locally before pushing to ensure all checks pass
  • The codegen job is failing because it runs ruff check --fix, which can fix most issues automatically but fails if unfixable errors remain (like PLR0911)
Detailed Analysis

The static analysis job found these violations:

PLR0911 Too many return statements (8 > 6)
  --> src/key_value/aio/adapters/pydantic/adapter.py:57:9

SIM103 Return the negated condition directly
  --> src/key_value/aio/adapters/pydantic/adapter.py:80:13

The codegen check failed because after generating the sync library, ruff check --fix found "18 errors (16 fixed, 2 remaining)". The 2 remaining errors are the same violations in both async and sync versions.

Relevant log excerpt:

Found 18 errors (16 fixed, 2 remaining).
subprocess.CalledProcessError: Command ['uv', 'run', 'ruff', 'check', '--fix', 
  '/home/runner/work/py-key-value/py-key-value/key-value/key-value-sync/src/key_value/sync'] 
  returned non-zero exit status 1.
Related Files
  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py:57-100 - Contains the _check_needs_wrapping method with the violations
  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py - Generated sync version with same issues
  • scripts/build_sync_library.py:112 - Runs ruff check --fix as part of codegen, which is where the failure occurs
  • Makefile:53 - The codegen target that triggers the build script

@claude
Copy link

claude bot commented Dec 22, 2025

Test Failure Analysis - Updated

Summary: The codegen_check job failed because the sync library wasn't regenerated after the latest changes to the async library.

Root Cause: After fixing the linting issues (PLR0911, SIM103) by refactoring the code, the author committed the changes to the async library () but forgot to run make codegen to regenerate the sync library (key-value-sync). The CI detected uncommitted changes in the generated files.

Suggested Solution:

Run the following commands locally and commit the generated changes:

make codegen
git add key-value/key-value-sync/
git commit -m "Regenerate sync library after pydantic adapter changes"
git push

Important: The async-first development workflow requires:

  1. Make all changes to key-value/key-value-aio/
  2. Run make codegen to regenerate key-value/key-value-sync/
  3. Commit BOTH the async and sync changes together

Alternatively, run make precommit which includes codegen, linting, and type checking.

Detailed Analysis

The CI workflow runs these steps:

  1. make sync - Install dependencies
  2. make codegen - Generate sync library from async library
  3. make lint - Run linting (auto-fixes applied)
  4. ❌ Check for uncommitted changes - FAILED

After codegen ran in CI, it detected uncommitted changes in:

  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py
  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/base.py
  • key-value/key-value-sync/tests/code_gen/adapters/test_pydantic.py

The changes include:

  • Updated imports (added contextlib, Mapping, is_dataclass, etc.)
  • Changed T TypeVar from bound=BaseModel | Sequence[BaseModel] to unconstrained
  • Added _check_needs_wrapping() and _serializes_to_dict() methods
  • Updated parameter type from type[T] to TypeForm[T]
  • Updated docstrings to reflect broader type support

These are the exact changes made to the async library in the latest commit, properly converted to sync code.

Related Files

Source files (async library):

  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py - Contains the changes that need to be propagated
  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py - Updated base class

Generated files (sync library):

  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py - Needs to be regenerated
  • key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/base.py - Needs to be regenerated
  • key-value/key-value-sync/tests/code_gen/adapters/test_pydantic.py - Needs to be regenerated

Build script:

  • scripts/build_sync_library.py - The codegen script that transforms async to sync

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (1)

94-96: Consider more accurate type name for error messages.

Now that the adapter supports more than BaseModel subclasses (dataclasses, TypedDict, primitives, etc.), returning "Pydantic model" is slightly misleading. Consider deriving a more specific name from self._type_adapter for clearer error messages, e.g., using type(self._type_adapter).__name__ or similar.

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bca0b5 and e8d4a0a.

📒 Files selected for processing (1)
  • key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py
🧰 Additional context used
🧬 Code graph analysis (1)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (1)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/base.py (1)
  • BasePydanticAdapter (18-268)
🔇 Additional comments (5)
key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py (5)

1-9: LGTM: Imports support expanded type handling.

All new imports (contextlib, Mapping, is_dataclass, type introspection utilities, TypeForm) are necessary for the generalized adapter and properly sourced from standard library or typing_extensions.


14-14: LGTM: Unbound TypeVar enables broader type support.

Removing the BaseModel bound is necessary to support the expanded range of pydantic-serializable types (dataclasses, TypedDict, primitives, sequences).


31-56: LGTM: Initialization correctly sets up TypeAdapter and wrapping logic.

The TypeForm[T] parameter type properly supports generic aliases, and all required base class fields are initialized correctly. The @bear_spray decorator usage is appropriately documented.


58-72: LGTM: Correct wrapping detection with Annotated unwrapping.

The logic properly handles Annotated types by recursively unwrapping them and correctly returns the negation of _serializes_to_dict to determine wrapping needs.


74-92: Logic is correct; past SIM103 feedback appears addressed.

The type-checking logic correctly identifies dict-serializable types (BaseModel, dataclass, TypedDict, dict, Mapping). The current implementation at line 83 (return is_typeddict(pydantic_model)) is already in simplified form, addressing the past SIM103 warning mentioned in previous reviews.

The method has 6 return statements, meeting the PLR0911 threshold, but the early-return pattern is clear and appropriate for type discrimination logic.

Comment on lines +18 to +29
"""Adapter around a KVStore-compliant Store that allows type-safe persistence of any pydantic-serializable type.
# Beartype cannot handle the parameterized type annotation (type[T]) used here for this generic adapter.
# Using @bear_spray to bypass beartype's runtime checks for this specific method.
Supports:
- Pydantic models (BaseModel subclasses)
- Dataclasses and TypedDicts
- Dict types (dict[K, V])
- Sequences (list[T], tuple[T, ...], set[T])
- Primitives and other types (int, str, datetime, UUID, etc.)
Types that serialize to dicts (BaseModel, dataclass, TypedDict, dict) are stored directly.
All other types are wrapped in {"items": <value>} for consistent dict-based storage.
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clear docstring documenting expanded capabilities.

The docstring comprehensively lists supported types and explains the wrapping logic. One optional enhancement: consider mentioning that Annotated[T, ...] types are unwrapped to T before processing.

🤖 Prompt for AI Agents
In key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py around
lines 18 to 29, update the module docstring to mention that
typing.Annotated[...] types are unwrapped to their underlying T before
processing; explicitly state that Annotated[T, ...] will be treated the same as
T for the purposes of the type checks and wrapping logic (i.e., treated as
model/dict types when T is such, or wrapped in {"items": value} when T is a
primitive/other), keeping the rest of the listed supported types and behavior
unchanged.

Add required spaces to table separators to pass MD060 linting.
Run codegen to sync PydanticAdapter changes.
@sonarqubecloud
Copy link

jlowin added a commit to jlowin/fastmcp that referenced this pull request Dec 22, 2025
Fixes type errors when upgrading to ty 0.0.5:

- Add base_url validation to auth providers (AWS, Azure, Discord, GitHub, Google, WorkOS)
- Add partial[] to tool/prompt decorator return types
- Add type ignores for PydanticAdapter generic aliases (pending strawgate/py-key-value#250)
- Add type ignores for Depends() in tests (type checker doesn't understand DI unwrapping)
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.

1 participant