Skip to content

Commit c61a55e

Browse files
authored
Merge branch 'main' into grpc-tests
2 parents 2d978cc + 3647c6c commit c61a55e

15 files changed

Lines changed: 667 additions & 19 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
---
22
name: Python Dapr Client SDK Bug report (not workflow SDK)
33
about: Report a bug in python-sdk (not dapr-ext-workflow)
4-
title: '[BUG] <title>'
4+
title: "[BUG] <title>"
55
labels: kind/bug
66
assignees: ''
77

88
---
9+
910
## Expected Behavior
1011

1112
<!-- Briefly describe what you expect to happen -->

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
---
22
name: Python Dapr Client SDK Feature Request (not workflow)
33
about: Create a Feature Request for python-sdk (not dapr-ext-workflow)
4-
title: '[FEATURE REQUEST] <title>'
4+
title: "[FEATURE REQUEST] <title>"
55
labels: kind/enhancement
66
assignees: ''
77

88
---
9+
910
## Describe the feature
1011

1112
## Release Note

.github/ISSUE_TEMPLATE/question.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
name: Question (not workflow)
33
about: Ask a question about the python sdk (not workflow)
4-
title: '[QUESTION] <title>'
4+
title: "[QUESTION] <title>"
55
labels: kind/question
66
assignees: ''
77

88
---
9+
910
## Ask your question here

.github/ISSUE_TEMPLATE/workflow_bug_report.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
---
22
name: WORKFLOW SDK Bug report
33
about: Report a bug in dapr-ext-workflow
4-
title: '[WORKFLOW SDK BUG] <title>'
5-
labels: ["kind/enhancement","dapr-ext-workflow"]
6-
assignees:
7-
- cgillum
8-
- DeepanshuA
4+
title: "[WORKFLOW SDK BUG] <title>"
5+
labels: dapr-ext-workflow, kind/enhancement
6+
assignees: ''
97

108
---
9+
1110
## Expected Behavior
1211

1312
<!-- Briefly describe what you expect to happen -->

.github/ISSUE_TEMPLATE/workflow_feature_request.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
---
22
name: WORKFLOW SDK Feature Request
33
about: Create a Feature Request for dapr-ext-workflow
4-
title: '[WORKFLOW SDK FEATURE REQUEST] <title>'
5-
labels: ["kind/enhancement","dapr-ext-workflow"]
6-
assignees:
7-
- cgillum
8-
- DeepanshuA
4+
title: "[WORKFLOW SDK FEATURE REQUEST] <title>"
5+
labels: dapr-ext-workflow, kind/enhancement
6+
assignees: ''
97

108
---
9+
1110
## Describe the WORKFLOW SDK feature
1211

1312
## Release Note
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
name: WORKFLOW Question
33
about: Ask a question about workflow
4-
title: '[WORKFLOW QUESTION] <title>'
5-
labels: ["kind/question", "dapr-ext-workflow"]
4+
title: "[WORKFLOW QUESTION] <title>"
5+
labels: dapr-ext-workflow, kind/question
66
assignees: ''
77

88
---
9+
910
## Ask your WORKFLOW SDK question here

examples/workflow/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,31 @@ dapr run --app-id wf-versioning-example -- python3 versioning.py part1
508508
dapr run --app-id wf-versioning-example --log-level debug -- python3 versioning.py part2
509509
```
510510
<!--END_STEP-->
511+
512+
### Pydantic models as workflow/activity inputs
513+
514+
This example shows how to pass [Pydantic](https://docs.pydantic.dev/) `BaseModel`
515+
instances directly as workflow and activity inputs. When a workflow or activity
516+
annotates its input parameter with a `BaseModel` subclass, the runtime
517+
reconstructs the model from the decoded JSON payload automatically — no manual
518+
`model_validate` call is needed at the receiving side.
519+
520+
The wire format remains plain JSON, so workflows and activities stay
521+
interop-friendly with non-Python Dapr apps. Outputs coming back from activities
522+
arrive as dicts; reconstructing them into a typed instance is a one-liner
523+
(`OrderResult.model_validate(...)`).
524+
525+
<!--STEP
526+
name: Run the pydantic models example
527+
expected_stdout_lines:
528+
- "[workflow] received order O-100 for Acme amount=42.0"
529+
- "[activity] approving order O-100"
530+
- "[workflow] activity returned approved=True"
531+
- "[client] workflow output: order_id=O-100 approved=True message=auto-approved"
532+
timeout_seconds: 60
533+
-->
534+
535+
```sh
536+
dapr run --app-id wf-pydantic-example -- python3 pydantic_models.py
537+
```
538+
<!--END_STEP-->
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2026 The Dapr Authors
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
"""Native Pydantic model support in Dapr workflows and activities.
13+
14+
Inputs annotated with a Pydantic BaseModel are reconstructed automatically on
15+
the receiving side — no manual serialization is needed. Outputs are emitted
16+
as plain JSON so the wire format stays interop-friendly with non-Python Dapr
17+
apps.
18+
"""
19+
20+
from dapr.ext.workflow import (
21+
DaprWorkflowClient,
22+
DaprWorkflowContext,
23+
WorkflowActivityContext,
24+
WorkflowRuntime,
25+
)
26+
from pydantic import BaseModel
27+
28+
29+
class OrderRequest(BaseModel):
30+
order_id: str
31+
customer: str
32+
amount: float
33+
34+
35+
class OrderResult(BaseModel):
36+
order_id: str
37+
approved: bool
38+
message: str
39+
40+
41+
wfr = WorkflowRuntime()
42+
instance_id = 'pydantic-demo'
43+
44+
45+
@wfr.workflow(name='order_workflow')
46+
def order_workflow(ctx: DaprWorkflowContext, order: OrderRequest):
47+
# `order` arrives as a real OrderRequest instance — the runtime reads the
48+
# annotation and reconstructs the model from the decoded JSON automatically.
49+
if not ctx.is_replaying:
50+
print(
51+
f'[workflow] received order {order.order_id} '
52+
f'for {order.customer} amount={order.amount}',
53+
flush=True,
54+
)
55+
raw = yield ctx.call_activity(approve_order, input=order)
56+
# Activity results come back as a plain dict. One line turns them into a
57+
# typed instance.
58+
result = OrderResult.model_validate(raw)
59+
if not ctx.is_replaying:
60+
print(
61+
f'[workflow] activity returned approved={result.approved}',
62+
flush=True,
63+
)
64+
return result
65+
66+
67+
@wfr.activity(name='approve_order')
68+
def approve_order(ctx: WorkflowActivityContext, order: OrderRequest) -> OrderResult:
69+
# Same story: `order` is already an OrderRequest instance here.
70+
print(f'[activity] approving order {order.order_id}', flush=True)
71+
if order.amount <= 100.0:
72+
return OrderResult(order_id=order.order_id, approved=True, message='auto-approved')
73+
return OrderResult(order_id=order.order_id, approved=False, message='needs review')
74+
75+
76+
def main():
77+
wfr.start()
78+
client = DaprWorkflowClient()
79+
80+
order = OrderRequest(order_id='O-100', customer='Acme', amount=42.0)
81+
client.schedule_new_workflow(workflow=order_workflow, input=order, instance_id=instance_id)
82+
state = client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
83+
84+
# state.serialized_output is a JSON string — reconstruct a typed instance.
85+
output = OrderResult.model_validate_json(state.serialized_output)
86+
print(
87+
f'[client] workflow output: order_id={output.order_id} '
88+
f'approved={output.approved} message={output.message}',
89+
flush=True,
90+
)
91+
92+
client.purge_workflow(instance_id)
93+
wfr.shutdown()
94+
95+
96+
if __name__ == '__main__':
97+
main()

examples/workflow/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dapr-ext-workflow>=1.17.0.dev
22
dapr>=1.17.0.dev
3+
pydantic>=2.0

ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/internal/shared.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Any, Optional, Sequence, Union
1818

1919
import grpc
20+
from dapr.ext.workflow import _model_protocol
2021

2122
ClientInterceptor = Union[
2223
grpc.UnaryUnaryClientInterceptor,
@@ -156,6 +157,16 @@ def encode(self, obj: Any) -> str:
156157
return super().encode(obj)
157158

158159
def default(self, obj):
160+
# Dapr-specific: objects implementing the duck-typed model protocol
161+
# (model_dump + model_validate) are emitted as plain JSON objects with
162+
# no AUTO_SERIALIZED marker, so the payload stays readable by
163+
# non-Python SDKs and by workflows/activities that don't import the
164+
# same class. Type-directed reconstruction happens at the
165+
# activity/workflow input boundary in
166+
# dapr.ext.workflow.workflow_runtime. No pydantic dependency — any
167+
# class matching the protocol works (Pydantic v2, SQLModel, custom).
168+
if _model_protocol.is_model(obj):
169+
return _model_protocol.dump_model(obj)
159170
if dataclasses.is_dataclass(obj):
160171
# Dataclasses are not serializable by default, so we convert them to a dict and mark them for
161172
# automatic deserialization by the receiver

0 commit comments

Comments
 (0)