Skip to content

fix(events): avoid mutating event validation input - #6314

Closed
VectorPeak wants to merge 1 commit into
google:mainfrom
VectorPeak:fix-event-model-validate-input-copy
Closed

fix(events): avoid mutating event validation input#6314
VectorPeak wants to merge 1 commit into
google:mainfrom
VectorPeak:fix-event-model-validate-input-copy

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:
Event._accept_convenience_kwargs() routes convenience inputs such as message, state, route, and node_path into the model's canonical nested fields. It currently does that by calling pop() directly on the data mapping received by the mode="before" model validator.

The before validator receives a dict in both common entry paths:

  • Event(message="Hello!", state={"key": "value"}) receives a temporary dict assembled for model construction, so consuming convenience keys there stays internal.
  • Event.model_validate(payload) receives the caller-provided dict itself. In that path, the validator mutates the original payload by replacing the convenience keys with content, actions, and node_info.

That second path is the bug: the issue is not just that a dict can theoretically reach the validator, but that the public model_validate(dict) API can hand the validator the same mapping object still owned by the caller.

A minimal reproduction before this fix:

payload = {
    "message": "Hello!",
    "state": {"key": "value"},
    "route": "next",
    "node_path": "root.node",
}
Event.model_validate(payload)
print(payload)

Before the fix, payload is rewritten in place. That can surprise callers that reuse the same mapping for logging, retries, comparisons, or subsequent validation.

Solution:
Copy the incoming mapping before consuming convenience keys. This keeps the existing convenience-key routing behavior unchanged while ensuring Event.model_validate() does not mutate caller-owned input.

The change is intentionally narrow:

  • message still routes to content
  • state still routes to actions.state_delta
  • route still routes to actions.route
  • node_path still routes to node_info.path
  • only the caller-owned input dictionary is preserved

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Passed targeted unit tests locally:

D:\ZXY\Github\adk-python\.tox\py312\Scripts\python.exe -m pytest tests/unittests/events/test_event.py::TestMessageSerialization::test_model_validate_does_not_mutate_input_dict -q
1 passed, 4 warnings in 1.25s

D:\ZXY\Github\adk-python\.tox\py312\Scripts\python.exe -m pytest tests/unittests/events/test_event.py -q
48 passed, 7 warnings in 1.25s

Additional local checks:

uvx --from ruff==0.15.17 ruff check src/google/adk/events/event.py tests/unittests/events/test_event.py
All checks passed!

uvx --from pyink==25.12 pyink --check src/google/adk/events/event.py tests/unittests/events/test_event.py
2 files would be left unchanged.

git diff --check
passed

Manual End-to-End (E2E) Tests:

Not applicable for this validator-level change. The regression is covered by a focused unit test that exercises the public Event.model_validate() path and verifies both behaviors: the caller-provided payload remains unchanged, and the resulting Event still receives content, actions.state_delta, actions.route, and node_info.path.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

This PR does not remove the existing pop()-based convenience-key routing. It only makes the validator operate on a local copy, so the canonical event output remains unchanged while avoiding a top-level mutation of caller-provided dictionaries.

copybara-service Bot pushed a commit that referenced this pull request Jul 6, 2026
Merge #6314

PiperOrigin-RevId: 943391685
@adk-bot

adk-bot commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Thank you @VectorPeak for your contribution! 🎉

Your changes have been successfully imported and merged via Copybara in commit 6b385e4.

Closing this PR as the changes are now in the main branch.

@adk-bot adk-bot added the merged [Status] This PR is merged label Jul 6, 2026
@adk-bot adk-bot closed this Jul 6, 2026
@rohityan rohityan self-assigned this Jul 6, 2026
@rohityan rohityan added the services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc label Jul 6, 2026
FrigaZzz pushed a commit to FrigaZzz/adk-python that referenced this pull request Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged [Status] This PR is merged services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event.model_validate mutates caller-provided input dict

3 participants