fix: handle missing channelData in ConversationUpdateActivity for Direct Line#361
fix: handle missing channelData in ConversationUpdateActivity for Direct Line#361rajan-chari wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adjusts the ConversationUpdateActivity model in the API package to accept conversationUpdate payloads that omit channelData (notably from Direct Line), preventing Pydantic validation failures during activity parsing.
Changes:
- Changes
ConversationUpdateActivity.channel_datatoOptional[ConversationChannelData] = None(instead of required). - Removes the prior
pyright: ignoresuppression tied to the incompatible override.
| class ConversationUpdateActivity(_ConversationUpdateBase, ActivityBase): | ||
| """Output model for received conversation update activities with required fields and read-only properties.""" | ||
|
|
||
| channel_data: ConversationChannelData # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] | ||
| channel_data: Optional[ConversationChannelData] = None | ||
| """Channel data with event type information.""" |
There was a problem hiding this comment.
Making ConversationUpdateActivity.channel_data optional fixes Pydantic validation, but it also means downstream code must handle channel_data=None. The apps router evaluates all selectors (ActivityRouter.select_handlers iterates every route), and several selectors in packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py access activity.channel_data.event_type for ConversationUpdateActivity without a None-guard (e.g., channel_created, team_deleted, etc.). With a Direct Line conversationUpdate missing channel_data, this will raise AttributeError during routing and still fail the request. Update those selectors to short-circuit when activity.channel_data is None before reading event_type.
Manual Test Results — Revised Approach (model_validator)Tested on branch
Approach reviewThe revised model_validator approach is much cleaner than the previous Optional approach:
Pyright
CI
|
Direct Line sends conversationUpdate activities without channelData, causing a Pydantic ValidationError because channel_data is required. Add a model_validator(mode="before") that defaults to an empty ConversationChannelData when channelData is absent. This preserves the required-field type contract for Teams developers (who always receive channelData) while allowing non-Teams channels to deserialize cleanly. Matches the TypeScript SDK pattern: types declare channelData as required (conversation-update.ts:25), runtime handles absence defensively (router.ts:73-87). In Python, Pydantic enforces at runtime, so the validator bridges the gap. Fixes #239
3b0af3e to
bf2393b
Compare
Q&A: Should the model_validator default only apply for Direct Line?A reviewer question came up: should we check Answer: No — the universal approach is architecturally correct.
If early warning is desired: Add a log warning in the router or middleware — if |
Summary
Bug: Direct Line sends
conversationUpdateactivities withoutchannelData, butConversationUpdateActivity.channel_datais a required field. Pydantic rejects the payload with aValidationErrorbefore any handler code runs.Fix: Add a
model_validator(mode="before")onConversationUpdateActivitythat injects an emptyConversationChannelDatawhenchannelDatais absent from the incoming payload. This keepschannel_dataas a required, non-optional field — preserving the type contract for Teams developers.Design rationale
We considered four approaches and chose Option C (default at deserialization) after cross-SDK review:
channel_dataOptionalWhy Option C: Matches the TypeScript SDK pattern. In TS,
IConversationUpdateActivitydeclareschannelDataas required (conversation-update.ts:25) but the router uses optional chaining defensively (router.ts:73-87). Python's Pydantic enforces types at runtime (unlike TS interfaces which are erased), so themodel_validatorbridges the gap — it runs before field validation and ensureschannelDatais always present.Result:
channel_datapopulated normally, event-type routing works as beforechannel_datais an emptyConversationChannelData(withevent_type=None), so Teams-specific handlers (e.g.on_channel_created) don't fire, but the genericon_conversation_updatehandler still doesConversationChannelData— no None-guards needed anywhereFiles changed
conversation_update.py— addedmodel_validator, extensive docstring explaining the designactivity_route_configs.py— reverted to original (no None-guards needed)Test plan
conversationUpdatewithoutchannelData— should succeed (wasValidationError)conversationUpdatewithchannelData— should parse and route normallyon_channel_created,on_team_archived, etc. still fire for Teams eventsFixes #239
🤖 Generated with Claude Code