Skip to content

Commit a2b2756

Browse files
Add --validators option for Pydantic v2 field validators (#2906)
* Add --validators option for Pydantic v2 field validators * Add validators documentation page * docs: update CLI reference documentation and prompt data 🤖 Generated by GitHub Actions * Replace hash suffix with increment for validator method names * docs: update CLI reference documentation and prompt data 🤖 Generated by GitHub Actions * Use ModelResolver for validator method name uniqueness * Use ValidatorsConfigType for strict typing and _load_json_config helper * Use Pydantic models for validators config validation * Fix validators enum serialization and Pydantic v1 compatibility * Use ModelValidators type instead of Any for validators config * Remove unnecessary pyright ignore comment * Move function-internal imports to top level * Refactor validators loading to use helper function pattern * Remove inline comment * Add tests for validators edge cases and improve coverage * Fix PR review comments: add import to docs and use model_dump helper * Add tests for plain mode and all validators skipped branches --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8ee5860 commit a2b2756

27 files changed

Lines changed: 922 additions & 6 deletions

docs/cli-reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This documentation is auto-generated from test cases.
1212
| 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior |
1313
| 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior |
1414
| 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior |
15-
| 🎨 [Template Customization](template-customization.md) | 18 | Output formatting and custom rendering |
15+
| 🎨 [Template Customization](template-customization.md) | 19 | Output formatting and custom rendering |
1616
| 📘 [OpenAPI-only Options](openapi-only-options.md) | 7 | OpenAPI-specific features |
1717
| 📋 [GraphQL-only Options](graphql-only-options.md) | 1 | |
1818
| ⚙️ [General Options](general-options.md) | 15 | Utilities and meta options |
@@ -219,6 +219,7 @@ This documentation is auto-generated from test cases.
219219
### V {#v}
220220

221221
- [`--validation`](openapi-only-options.md#validation)
222+
- [`--validators`](template-customization.md#validators)
222223
- [`--version`](utility-options.md#version)
223224

224225
### W {#w}

docs/cli-reference/quick-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ datamodel-codegen [OPTIONS]
150150
| [`--treat-dot-as-module`](template-customization.md#treat-dot-as-module) | Treat dots in schema names as module separators. |
151151
| [`--use-double-quotes`](template-customization.md#use-double-quotes) | Use double quotes for string literals in generated code. |
152152
| [`--use-exact-imports`](template-customization.md#use-exact-imports) | Import exact types instead of modules. |
153+
| [`--validators`](template-customization.md#validators) | Add custom field validators to generated Pydantic v2 models. |
153154
| [`--wrap-string-literal`](template-customization.md#wrap-string-literal) | Wrap long string literals across multiple lines. |
154155

155156
### 📘 OpenAPI-only Options
@@ -347,6 +348,7 @@ All options sorted alphabetically:
347348
- [`--use-union-operator`](typing-customization.md#use-union-operator) - Use | operator for Union types (PEP 604).
348349
- [`--use-unique-items-as-set`](typing-customization.md#use-unique-items-as-set) - Generate set types for arrays with uniqueItems constraint.
349350
- [`--validation`](openapi-only-options.md#validation) - Enable validation constraints (deprecated, use --field-const...
351+
- [`--validators`](template-customization.md#validators) - Add custom field validators to generated Pydantic v2 models.
350352
- [`--version`](utility-options.md#version) - Show program version and exit
351353
- [`--watch`](general-options.md#watch) - Watch input file(s) for changes and regenerate output automa...
352354
- [`--watch-delay`](general-options.md#watch-delay) - Set debounce delay in seconds for watch mode.

docs/cli-reference/template-customization.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
| [`--treat-dot-as-module`](#treat-dot-as-module) | Treat dots in schema names as module separators. |
2222
| [`--use-double-quotes`](#use-double-quotes) | Use double quotes for string literals in generated code. |
2323
| [`--use-exact-imports`](#use-exact-imports) | Import exact types instead of modules. |
24+
| [`--validators`](#validators) | Add custom field validators to generated Pydantic v2 models.... |
2425
| [`--wrap-string-literal`](#wrap-string-literal) | Wrap long string literals across multiple lines. |
2526

2627
---
@@ -2692,6 +2693,83 @@ modules are generated. For single-file output, the difference is minimal.
26922693

26932694
---
26942695

2696+
## `--validators` {#validators}
2697+
2698+
Add custom field validators to generated Pydantic v2 models.
2699+
2700+
The `--validators` option takes a JSON file defining validators per model.
2701+
Each validator specifies the field(s) to validate, the validation function
2702+
to import, and optionally the mode (before/after/wrap/plain).
2703+
This allows injecting custom validation logic into generated models.
2704+
2705+
**See also:** [Field Validators](../validators.md)
2706+
2707+
!!! tip "Usage"
2708+
2709+
```bash
2710+
datamodel-codegen --input schema.json --validators tests/data/jsonschema/field_validators_config.json --output-model-type pydantic_v2.BaseModel --disable-timestamp # (1)!
2711+
```
2712+
2713+
1. :material-arrow-left: `--validators` - the option documented here
2714+
2715+
??? example "Examples"
2716+
2717+
**Input Schema:**
2718+
2719+
```json
2720+
{
2721+
"$schema": "http://json-schema.org/draft-07/schema#",
2722+
"title": "User",
2723+
"type": "object",
2724+
"properties": {
2725+
"name": {
2726+
"type": "string"
2727+
},
2728+
"email": {
2729+
"type": "string",
2730+
"format": "email"
2731+
},
2732+
"age": {
2733+
"type": "integer",
2734+
"minimum": 0
2735+
}
2736+
},
2737+
"required": ["name", "email"]
2738+
}
2739+
```
2740+
2741+
**Output:**
2742+
2743+
```python
2744+
# generated by datamodel-codegen:
2745+
# filename: field_validators.json
2746+
2747+
from __future__ import annotations
2748+
2749+
from typing import Any
2750+
2751+
from myapp.validators import validate_email, validate_name
2752+
from pydantic import BaseModel, EmailStr, ValidationInfo, conint, field_validator
2753+
2754+
2755+
class User(BaseModel):
2756+
name: str
2757+
email: EmailStr
2758+
age: conint(ge=0) | None = None
2759+
2760+
@field_validator('name', mode='before')
2761+
@classmethod
2762+
def validate_name_validator(cls, v: Any, info: ValidationInfo) -> Any:
2763+
return validate_name(v, info)
2764+
2765+
@field_validator('email', mode='after')
2766+
@classmethod
2767+
def validate_email_validator(cls, v: Any, info: ValidationInfo) -> Any:
2768+
return validate_email(v, info)
2769+
```
2770+
2771+
---
2772+
26952773
## `--wrap-string-literal` {#wrap-string-literal}
26962774

26972775
Wrap long string literals across multiple lines.

docs/validators.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<!-- related-cli-options: --validators -->
2+
3+
# Field Validators
4+
5+
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.
6+
7+
## Basic Usage
8+
9+
```bash
10+
datamodel-codegen --input schema.json --output model.py \
11+
--validators validators.json \
12+
--output-model-type pydantic_v2.BaseModel
13+
```
14+
15+
## Validators File Format
16+
17+
The validators file is a JSON file that maps model names to their validator definitions.
18+
19+
### Structure
20+
21+
```json
22+
{
23+
"ModelName": {
24+
"validators": [
25+
{
26+
"field": "field_name",
27+
"function": "module.path.to.validator_function",
28+
"mode": "after"
29+
}
30+
]
31+
}
32+
}
33+
```
34+
35+
### Fields
36+
37+
| Field | Description | Required |
38+
|-------|-------------|----------|
39+
| `field` | Single field name to validate | One of `field` or `fields` |
40+
| `fields` | List of field names (for multi-field validators) | One of `field` or `fields` |
41+
| `function` | Fully qualified path to the validator function | Yes |
42+
| `mode` | Validator mode: `before`, `after`, `wrap`, or `plain` | No (default: `after`) |
43+
44+
## Validator Modes
45+
46+
Pydantic v2 supports different validator modes, each with its own signature:
47+
48+
### `before` / `after` Mode
49+
50+
Standard validators that run before or after Pydantic's own validation:
51+
52+
```python
53+
def validate_name(v: Any, info: ValidationInfo) -> Any:
54+
if not v:
55+
raise ValueError("Name cannot be empty")
56+
return v.strip()
57+
```
58+
59+
### `wrap` Mode
60+
61+
Wrap validators receive a handler to call the next validator in the chain:
62+
63+
```python
64+
from pydantic import ValidationInfo, ValidatorFunctionWrapHandler
65+
66+
def wrap_validate_name(
67+
v: Any,
68+
handler: ValidatorFunctionWrapHandler,
69+
info: ValidationInfo
70+
) -> Any:
71+
# Pre-processing
72+
v = v.strip() if isinstance(v, str) else v
73+
# Call next validator
74+
result = handler(v)
75+
# Post-processing
76+
return result.upper()
77+
```
78+
79+
### `plain` Mode
80+
81+
Plain validators replace Pydantic's validation entirely:
82+
83+
```python
84+
def plain_validate_name(v: Any) -> str:
85+
if not isinstance(v, str):
86+
raise TypeError("Expected string")
87+
return v
88+
```
89+
90+
## Example
91+
92+
### Input Schema
93+
94+
```json
95+
{
96+
"type": "object",
97+
"title": "User",
98+
"properties": {
99+
"name": {"type": "string"},
100+
"email": {"type": "string", "format": "email"},
101+
"age": {"type": "integer", "minimum": 0}
102+
},
103+
"required": ["name", "email"]
104+
}
105+
```
106+
107+
### Validators File
108+
109+
```json
110+
{
111+
"User": {
112+
"validators": [
113+
{
114+
"field": "name",
115+
"function": "myapp.validators.validate_name",
116+
"mode": "before"
117+
},
118+
{
119+
"field": "email",
120+
"function": "myapp.validators.validate_email",
121+
"mode": "after"
122+
},
123+
{
124+
"fields": ["name", "email"],
125+
"function": "myapp.validators.validate_contact_info",
126+
"mode": "after"
127+
}
128+
]
129+
}
130+
}
131+
```
132+
133+
### Validator Functions (myapp/validators.py)
134+
135+
```python
136+
from typing import Any
137+
from pydantic import ValidationInfo
138+
139+
def validate_name(v: Any, info: ValidationInfo) -> Any:
140+
if isinstance(v, str):
141+
return v.strip()
142+
return v
143+
144+
def validate_email(v: Any, info: ValidationInfo) -> Any:
145+
if isinstance(v, str) and not v.endswith("@example.com"):
146+
# Custom email domain validation
147+
pass
148+
return v
149+
150+
def validate_contact_info(v: Any, info: ValidationInfo) -> Any:
151+
# This runs for both name and email fields
152+
return v
153+
```
154+
155+
### Generated Output
156+
157+
```python
158+
from __future__ import annotations
159+
160+
from typing import Any
161+
162+
from myapp.validators import validate_contact_info, validate_email, validate_name
163+
from pydantic import BaseModel, EmailStr, ValidationInfo, conint, field_validator
164+
165+
166+
class User(BaseModel):
167+
name: str
168+
email: EmailStr
169+
age: conint(ge=0) | None = None
170+
171+
@field_validator('name', mode='before')
172+
@classmethod
173+
def validate_name_validator(cls, v: Any, info: ValidationInfo) -> Any:
174+
return validate_name(v, info)
175+
176+
@field_validator('email', mode='after')
177+
@classmethod
178+
def validate_email_validator(cls, v: Any, info: ValidationInfo) -> Any:
179+
return validate_email(v, info)
180+
181+
@field_validator('name', 'email', mode='after')
182+
@classmethod
183+
def validate_contact_info_validator(cls, v: Any, info: ValidationInfo) -> Any:
184+
return validate_contact_info(v, info)
185+
```
186+
187+
## Notes
188+
189+
- This feature only supports Pydantic v2 (`--output-model-type pydantic_v2.BaseModel`)
190+
- The `ModelName` in the validators file must match the generated Python class name
191+
- Validator functions are imported automatically based on the `function` path
192+
- When the same validator function is used multiple times, an incrementing suffix (`_1`, `_2`, etc.) is added to ensure method name uniqueness
193+
194+
---
195+
196+
## See Also
197+
198+
- [CLI Reference: `--validators`](cli-reference/general-options.md) - CLI option documentation
199+
- [Pydantic v2 Validators Documentation](https://docs.pydantic.dev/latest/concepts/validators/) - Official Pydantic documentation

0 commit comments

Comments
 (0)