-
-
Notifications
You must be signed in to change notification settings - Fork 437
Add --validators option for Pydantic v2 field validators #2906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
da7047b
Add --validators option for Pydantic v2 field validators
koxudaxi 4146987
Add validators documentation page
koxudaxi 8dff2c7
Merge main and resolve conflicts
koxudaxi a116d14
docs: update CLI reference documentation and prompt data
github-actions[bot] d41d68c
Replace hash suffix with increment for validator method names
koxudaxi 101317c
docs: update CLI reference documentation and prompt data
github-actions[bot] a80a5ca
Use ModelResolver for validator method name uniqueness
koxudaxi bcd6efd
Use ValidatorsConfigType for strict typing and _load_json_config helper
koxudaxi 5752611
Use Pydantic models for validators config validation
koxudaxi 3fb4649
Fix validators enum serialization and Pydantic v1 compatibility
koxudaxi c6c5b9c
Use ModelValidators type instead of Any for validators config
koxudaxi 5c22227
Remove unnecessary pyright ignore comment
koxudaxi 95ebd3a
Move function-internal imports to top level
koxudaxi 26a13fb
Refactor validators loading to use helper function pattern
koxudaxi c30eee3
Remove inline comment
koxudaxi d9e776f
Add tests for validators edge cases and improve coverage
koxudaxi 9d8793d
Merge main into feature/validators-option
koxudaxi db58280
Fix PR review comments: add import to docs and use model_dump helper
koxudaxi 233159c
Add tests for plain mode and all validators skipped branches
koxudaxi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| <!-- related-cli-options: --validators --> | ||
|
|
||
| # Field Validators | ||
|
|
||
| The `--validators` option allows you to add custom field validators to generated Pydantic v2 models. This enables you to inject validation logic into generated code without manually editing it. | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ```bash | ||
| datamodel-codegen --input schema.json --output model.py \ | ||
| --validators validators.json \ | ||
| --output-model-type pydantic_v2.BaseModel | ||
| ``` | ||
|
|
||
| ## Validators File Format | ||
|
|
||
| The validators file is a JSON file that maps model names to their validator definitions. | ||
|
|
||
| ### Structure | ||
|
|
||
| ```json | ||
| { | ||
| "ModelName": { | ||
| "validators": [ | ||
| { | ||
| "field": "field_name", | ||
| "function": "module.path.to.validator_function", | ||
| "mode": "after" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Fields | ||
|
|
||
| | Field | Description | Required | | ||
| |-------|-------------|----------| | ||
| | `field` | Single field name to validate | One of `field` or `fields` | | ||
| | `fields` | List of field names (for multi-field validators) | One of `field` or `fields` | | ||
| | `function` | Fully qualified path to the validator function | Yes | | ||
| | `mode` | Validator mode: `before`, `after`, `wrap`, or `plain` | No (default: `after`) | | ||
|
|
||
| ## Validator Modes | ||
|
|
||
| Pydantic v2 supports different validator modes, each with its own signature: | ||
|
|
||
| ### `before` / `after` Mode | ||
|
|
||
| Standard validators that run before or after Pydantic's own validation: | ||
|
|
||
| ```python | ||
| def validate_name(v: Any, info: ValidationInfo) -> Any: | ||
| if not v: | ||
| raise ValueError("Name cannot be empty") | ||
| return v.strip() | ||
| ``` | ||
|
|
||
| ### `wrap` Mode | ||
|
|
||
| Wrap validators receive a handler to call the next validator in the chain: | ||
|
|
||
| ```python | ||
| from pydantic import ValidationInfo, ValidatorFunctionWrapHandler | ||
|
|
||
| def wrap_validate_name( | ||
| v: Any, | ||
| handler: ValidatorFunctionWrapHandler, | ||
| info: ValidationInfo | ||
| ) -> Any: | ||
| # Pre-processing | ||
| v = v.strip() if isinstance(v, str) else v | ||
| # Call next validator | ||
| result = handler(v) | ||
| # Post-processing | ||
| return result.upper() | ||
| ``` | ||
|
|
||
| ### `plain` Mode | ||
|
|
||
| Plain validators replace Pydantic's validation entirely: | ||
|
|
||
| ```python | ||
| def plain_validate_name(v: Any) -> str: | ||
| if not isinstance(v, str): | ||
| raise TypeError("Expected string") | ||
| return v | ||
| ``` | ||
|
|
||
| ## Example | ||
|
|
||
| ### Input Schema | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "title": "User", | ||
| "properties": { | ||
| "name": {"type": "string"}, | ||
| "email": {"type": "string", "format": "email"}, | ||
| "age": {"type": "integer", "minimum": 0} | ||
| }, | ||
| "required": ["name", "email"] | ||
| } | ||
| ``` | ||
|
|
||
| ### Validators File | ||
|
|
||
| ```json | ||
| { | ||
| "User": { | ||
| "validators": [ | ||
| { | ||
| "field": "name", | ||
| "function": "myapp.validators.validate_name", | ||
| "mode": "before" | ||
| }, | ||
| { | ||
| "field": "email", | ||
| "function": "myapp.validators.validate_email", | ||
| "mode": "after" | ||
| }, | ||
| { | ||
| "fields": ["name", "email"], | ||
| "function": "myapp.validators.validate_contact_info", | ||
| "mode": "after" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Validator Functions (myapp/validators.py) | ||
|
|
||
| ```python | ||
| from typing import Any | ||
| from pydantic import ValidationInfo | ||
|
|
||
| def validate_name(v: Any, info: ValidationInfo) -> Any: | ||
| if isinstance(v, str): | ||
| return v.strip() | ||
| return v | ||
|
|
||
| def validate_email(v: Any, info: ValidationInfo) -> Any: | ||
| if isinstance(v, str) and not v.endswith("@example.com"): | ||
| # Custom email domain validation | ||
| pass | ||
| return v | ||
|
|
||
| def validate_contact_info(v: Any, info: ValidationInfo) -> Any: | ||
| # This runs for both name and email fields | ||
| return v | ||
| ``` | ||
|
|
||
| ### Generated Output | ||
|
|
||
| ```python | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from myapp.validators import validate_contact_info, validate_email, validate_name | ||
| from pydantic import BaseModel, EmailStr, ValidationInfo, conint, field_validator | ||
|
|
||
|
|
||
| class User(BaseModel): | ||
| name: str | ||
| email: EmailStr | ||
| age: conint(ge=0) | None = None | ||
|
|
||
| @field_validator('name', mode='before') | ||
| @classmethod | ||
| def validate_name_validator(cls, v: Any, info: ValidationInfo) -> Any: | ||
| return validate_name(v, info) | ||
|
|
||
| @field_validator('email', mode='after') | ||
| @classmethod | ||
| def validate_email_validator(cls, v: Any, info: ValidationInfo) -> Any: | ||
| return validate_email(v, info) | ||
|
|
||
| @field_validator('name', 'email', mode='after') | ||
| @classmethod | ||
| def validate_contact_info_validator(cls, v: Any, info: ValidationInfo) -> Any: | ||
| return validate_contact_info(v, info) | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - This feature only supports Pydantic v2 (`--output-model-type pydantic_v2.BaseModel`) | ||
| - The `ModelName` in the validators file must match the generated Python class name | ||
| - Validator functions are imported automatically based on the `function` path | ||
| - When the same validator function is used multiple times, an incrementing suffix (`_1`, `_2`, etc.) is added to ensure method name uniqueness | ||
|
|
||
| --- | ||
|
|
||
| ## See Also | ||
|
|
||
| - [CLI Reference: `--validators`](cli-reference/general-options.md) - CLI option documentation | ||
| - [Pydantic v2 Validators Documentation](https://docs.pydantic.dev/latest/concepts/validators/) - Official Pydantic documentation | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.