Skip to content

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

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

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

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.

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.

Event.model_validate mutates caller-provided input dict

1 participant