Skip to content

feat: replace check_expected_keys with typed pydantic models (#1034) - #1071

Open
reachsridhard wants to merge 10 commits into
taverntesting:masterfrom
reachsridhard:feat/pydantic-type-checking
Open

feat: replace check_expected_keys with typed pydantic models (#1034)#1071
reachsridhard wants to merge 10 commits into
taverntesting:masterfrom
reachsridhard:feat/pydantic-type-checking

Conversation

@reachsridhard

@reachsridhard reachsridhard commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the 9-year-old check_expected_keys pattern with pydantic models that use extra='forbid' for key validation and proper type annotations for value validation.

This replaces #1065 by addressing the review feedback: all fields now have specific types instead of Optional[Any], so pydantic actually validates both keys and values — otherwise using dataclasses/dacite would be just as effective.

Changes

  • New: tavern/_core/pydantic_models.py — typed BaseModel subclasses for REST, MQTT, and gRPC request/response/client specs
  • Modified: All plugin modules to use validate_keys() instead of check_expected_keys()
  • New: tests/unit/test_pydantic_models.py — tests for key validation + type enforcement
  • Modified: pyproject.toml — pydantic as runtime dependency

Type annotations

Model Field examples
RestRequestSpec method: str, headers: dict, stream: bool, json: JSONType, timeout: Union[float, list]
MQTTRequestSpec topic: str, qos: int, retain: bool, payload: Union[str, bytes, int, float]
MQTTConnectArgs host: str, port: int, keepalive: int
GRPCResponseSpec status: Union[str, int, list[str], list[int]], body: dict
GRPCClientTopLevel attempt_reflection: bool, connect: dict, metadata: dict

Closes

Closes #1034

Summary by CodeRabbit

  • New Features

    • Added structured validation for REST, MQTT and gRPC request, response and connection settings.
    • Configuration now supports clearer type checking and rejects unrecognised keys with tailored errors.
    • Added Pydantic as a runtime dependency.
  • Tests

    • Added comprehensive coverage for valid configurations, invalid types and unexpected keys across supported protocols.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds shared Pydantic models for REST, MQTT, and gRPC specifications, replaces plugin-specific key checks, adds type validation coverage, and moves Pydantic to the runtime dependency set.

Changes

Pydantic validation

Layer / File(s) Summary
Shared validation models
pyproject.toml, tavern/_core/pydantic_models.py
Adds the constrained Pydantic runtime dependency and typed models for REST, MQTT, and gRPC request and client configuration.
Plugin validation integration
tavern/_plugins/rest/request.py, tavern/_plugins/mqtt/*, tavern/_plugins/grpc/*
Replaces inline check_expected_keys calls with shared Pydantic validation for request, response, and client configuration blocks.
Validation coverage
tests/unit/test_pydantic_models.py
Tests accepted keys, unexpected keys, nested configuration blocks, and supported value types across all specification models.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: replacing check_expected_keys with Pydantic models.
Linked Issues check ✅ Passed The changes replace check_expected_keys with typed Pydantic validators as requested in #1034.
Out of Scope Changes check ✅ Passed No obvious out-of-scope changes; the dependency and tests support the Pydantic validation refactor.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tavern/_core/pydantic_models.py`:
- Around line 19-52: Sanitize type-validation errors in
_BaseKeyValidator.validate_keys so UnexpectedKeysError never exposes raw input
values. Enable Pydantic’s input-repr hiding option in model_config and replace
the direct str(e) fallback with a safe validation-error summary that retains
field and type information without including offending values; preserve the
existing unexpected-key handling.

In `@tests/unit/test_pydantic_models.py`:
- Around line 300-309: Reformat the data dictionary literals in
test_grpc_request_body_can_be_dict and test_grpc_request_body_can_be_string
using ruff-format’s multi-line layout so the tests pass formatting checks; do
not change their behavior or assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf83fe31-983a-4a53-9831-50bfe3530dd6

📥 Commits

Reviewing files that changed from the base of the PR and between a05b9ae and d9609c8.

📒 Files selected for processing (9)
  • pyproject.toml
  • tavern/_core/pydantic_models.py
  • tavern/_plugins/grpc/client.py
  • tavern/_plugins/grpc/request.py
  • tavern/_plugins/grpc/response.py
  • tavern/_plugins/mqtt/client.py
  • tavern/_plugins/mqtt/request.py
  • tavern/_plugins/rest/request.py
  • tests/unit/test_pydantic_models.py

Comment thread tavern/_core/pydantic_models.py
Comment thread tests/unit/test_pydantic_models.py Outdated
TypeConvertToken objects (from !force_format_include, !int, etc.) and
dict values (from $ext function calls) exist at validation time before
resolution. Add these to Union types so pydantic accepts them while
still rejecting clearly wrong types (e.g. int for headers, list for
method).
Integration tests use cookies as a list (e.g. cookie name lists,
cookie override dicts in list form, empty list to send no cookies).
@reachsridhard

Copy link
Copy Markdown
Contributor Author

@michaelboulton, as per your suggestion in PR 1065, I have updated the code to include type checking.

@michaelboulton

Copy link
Copy Markdown
Member

@michaelboulton, as per your suggestion in PR 1065, I have updated the code to include type checking.

I think that they can still be reduced a bit more, for example:
https://github.com/taverntesting/tavern/pull/1071/files#diff-5df5c3f4f269aa4e95074500ee3aa7e6d59ea080fc7a1811123713f2f60b30d2R106
I think this can just be a Optional[bool] = None (or it might have to be a Optional[bool | TypeConvertToken], not sure) instead of being a dict which it should never be

Boolean fields (enable, clean_session, stream, follow_redirects,
retain, secure, attempt_reflection) should never receive  dicts.
Keep TypeConvertToken for !bool YAML tag support.
@reachsridhard

Copy link
Copy Markdown
Contributor Author

@michaelboulton, as per your suggestion in PR 1065, I have updated the code to include type checking.

I think that they can still be reduced a bit more, for example: https://github.com/taverntesting/tavern/pull/1071/files#diff-5df5c3f4f269aa4e95074500ee3aa7e6d59ea080fc7a1811123713f2f60b30d2R106 I think this can just be a Optional[bool] = None (or it might have to be a Optional[bool | TypeConvertToken], not sure) instead of being a dict which it should never be

@michaelboulton

Done in the latest commit. Removed dict from all boolean fields. Kept TypeConvertToken for ! bool YAML tag support (used in integration tests like verify: !bool "{verify_false}").

Thank you!!

reachsridhard and others added 2 commits August 25, 2026 01:55
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@michaelboulton

Copy link
Copy Markdown
Member

@michaelboulton, as per your suggestion in PR 1065, I have updated the code to include type checking.

I think that they can still be reduced a bit more, for example: #1071 (files) I think this can just be a Optional[bool] = None (or it might have to be a Optional[bool | TypeConvertToken], not sure) instead of being a dict which it should never be

@michaelboulton

Done in the latest commit. Removed dict from all boolean fields. Kept TypeConvertToken for ! bool YAML tag support (used in integration tests like verify: !bool "{verify_false}").

Thank you!!

Sorry, to be clear, I meant for all the fields, not just boolean ones.

Another thing while I'm looking at it again, the type checking on the conversion tokens should be more strict (eg ints can only be int | IntToken , bools can be bool | BoolToken. Some of the fields also shouldn't be optional (for example, the mqtt 'host' isn't optional and it has to be present so that should be a str | TypeConvertToken)

I think this is quite a lot of work, if you don't mind I could make those changes and push it to your branch?

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.

Replace 'check_expected_keys' with pydantic or something

2 participants