-
Notifications
You must be signed in to change notification settings - Fork 4
Description
[Refactor] Decouple Core Interfaces from Pydantic (Support Dataclasses, TypedDict, etc.)
🚀 Feature Request
Is your feature request related to a problem? Please describe.
Currently, the cqrs library is tightly coupled with pydantic. The core base classes (Request, Response, Event, NotificationEvent) in src/cqrs/base.py inherit directly from pydantic.BaseModel.
This imposes several limitations:
- Performance: Pydantic introduces validation overhead that may not be necessary for all use cases (e.g., internal events trusted by the producer).
- Flexibility: Users cannot use Python's native
dataclasses,attrs,msgspec,NamedTuple, orTypedDict. - Dependency Conflicts: Forcing a specific version of Pydantic can cause version conflicts in consuming projects.
Describe the solution you'd like
I propose making the library agnostic to the data modeling library used by the client. The core logic (dispatching, mediation, transportation) should work with protocols or standard Python structures rather than concrete Pydantic implementations.
The library should support:
- Pydantic Models (Backward compatibility/Opt-in)
- Python Standard
dataclasses TypedDict/dictNamedTuple- Plain Python Classes
🛠 Implementation Plan
1. Modify Base Interfaces (src/cqrs/base.py)
Remove the inheritance from pydantic.BaseModel. Base classes should become lightweight Mixins or Protocols to act as markers for the dispatcher.
# Current:
class Request(pydantic.BaseModel):
...
# Proposed:
# As a Mixin/Marker
class Request:
passOr as a Protocol
class IRequest(typing.Protocol):
pass#### 2. Abstract Serialization/Deserialization
Refactor serializers (src/cqrs/serializers) and deserializers (src/cqrs/deserializers) to use a strategy pattern or a unified helper that detects the object type.
The logic needs to handle extraction and instantiation dynamically:
- If Pydantic: Use
.model_dump()/.model_validate() - If Dataclass: Use
dataclasses.asdict()/ Constructor - If Dict: Pass through
- If Plain Class: Use
__dict__or constructor kwargs
3. Update Dispatchers and Brokers
Update dispatcher and message_brokers to rely on the abstract serializer rather than calling Pydantic methods directly on the message objects.
🔄 Migration Guide & Examples
This change will require minor updates in client code, primarily making inheritance explicit.
Scenario 1: Continuing with Pydantic
Users who want to keep using Pydantic will need to explicitly inherit from BaseModel.
# Before:
from cqrs import Request
class CreateUserCmd(Request):
name: str
# After:
from pydantic import BaseModel
from cqrs import Request
# Explicitly inherit from BaseModel. Request becomes just a marker.
class CreateUserCmd(BaseModel, Request):
name: strScenario 2: Switching to Dataclasses
Users can now use standard library dataclasses.
from dataclasses import dataclass
from cqrs import Request
@dataclass
class CreateUserCmd(Request):
name: strScenario 3: Using TypedDict
For simple payloads without behavioral logic.
from typing import TypedDict
class CreateUserCmd(TypedDict):
name: str
# The handler simply accepts the dict
# async def handle(command: CreateUserCmd): ...Additional context
Internal library models (like SagaResult or OutboxedEvent) may continue to use Pydantic for internal robustness, but this implementation detail should not leak into the public API required for defining Domain Events or Commands.