Skip to content

Commit 8c59fa2

Browse files
sydney-runkleViicosdavidhewitt
authored
Improving the alias configuration API for validation and serialization (#1640)
Co-authored-by: Victorien <[email protected]> Co-authored-by: David Hewitt <[email protected]>
1 parent 7dc19c3 commit 8c59fa2

33 files changed

+901
-238
lines changed

benches/main.rs

+65-55
Large diffs are not rendered by default.

python/pydantic_core/_pydantic_core.pyi

+22-4
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ class SchemaValidator:
9797
context: Any | None = None,
9898
self_instance: Any | None = None,
9999
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
100+
by_alias: bool | None = None,
101+
by_name: bool | None = None,
100102
) -> Any:
101103
"""
102104
Validate a Python object against the schema and return the validated object.
@@ -114,6 +116,8 @@ class SchemaValidator:
114116
allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences
115117
and mappings are ignored.
116118
`'trailing-strings'` means any final unfinished JSON string is included in the result.
119+
by_alias: Whether to use the field's alias when validating against the provided input data.
120+
by_name: Whether to use the field's name when validating against the provided input data.
117121
118122
Raises:
119123
ValidationError: If validation fails.
@@ -130,6 +134,8 @@ class SchemaValidator:
130134
from_attributes: bool | None = None,
131135
context: Any | None = None,
132136
self_instance: Any | None = None,
137+
by_alias: bool | None = None,
138+
by_name: bool | None = None,
133139
) -> bool:
134140
"""
135141
Similar to [`validate_python()`][pydantic_core.SchemaValidator.validate_python] but returns a boolean.
@@ -148,6 +154,8 @@ class SchemaValidator:
148154
context: Any | None = None,
149155
self_instance: Any | None = None,
150156
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
157+
by_alias: bool | None = None,
158+
by_name: bool | None = None,
151159
) -> Any:
152160
"""
153161
Validate JSON data directly against the schema and return the validated Python object.
@@ -168,6 +176,8 @@ class SchemaValidator:
168176
allow_partial: Whether to allow partial validation; if `True` incomplete JSON will be parsed successfully
169177
and errors in the last element of sequences and mappings are ignored.
170178
`'trailing-strings'` means any final unfinished JSON string is included in the result.
179+
by_alias: Whether to use the field's alias when validating against the provided input data.
180+
by_name: Whether to use the field's name when validating against the provided input data.
171181
172182
Raises:
173183
ValidationError: If validation fails or if the JSON data is invalid.
@@ -183,6 +193,8 @@ class SchemaValidator:
183193
strict: bool | None = None,
184194
context: Any | None = None,
185195
allow_partial: bool | Literal['off', 'on', 'trailing-strings'] = False,
196+
by_alias: bool | None = None,
197+
by_name: bool | None = None,
186198
) -> Any:
187199
"""
188200
Validate a string against the schema and return the validated Python object.
@@ -199,6 +211,8 @@ class SchemaValidator:
199211
allow_partial: Whether to allow partial validation; if `True` errors in the last element of sequences
200212
and mappings are ignored.
201213
`'trailing-strings'` means any final unfinished JSON string is included in the result.
214+
by_alias: Whether to use the field's alias when validating against the provided input data.
215+
by_name: Whether to use the field's name when validating against the provided input data.
202216
203217
Raises:
204218
ValidationError: If validation fails or if the JSON data is invalid.
@@ -216,6 +230,8 @@ class SchemaValidator:
216230
strict: bool | None = None,
217231
from_attributes: bool | None = None,
218232
context: Any | None = None,
233+
by_alias: bool | None = None,
234+
by_name: bool | None = None,
219235
) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any] | None, set[str]]:
220236
"""
221237
Validate an assignment to a field on a model.
@@ -230,6 +246,8 @@ class SchemaValidator:
230246
If `None`, the value of [`CoreConfig.from_attributes`][pydantic_core.core_schema.CoreConfig] is used.
231247
context: The context to use for validation, this is passed to functional validators as
232248
[`info.context`][pydantic_core.core_schema.ValidationInfo.context].
249+
by_alias: Whether to use the field's alias when validating against the provided input data.
250+
by_name: Whether to use the field's name when validating against the provided input data.
233251
234252
Raises:
235253
ValidationError: If validation fails.
@@ -283,7 +301,7 @@ class SchemaSerializer:
283301
mode: str | None = None,
284302
include: _IncEx | None = None,
285303
exclude: _IncEx | None = None,
286-
by_alias: bool = True,
304+
by_alias: bool | None = None,
287305
exclude_unset: bool = False,
288306
exclude_defaults: bool = False,
289307
exclude_none: bool = False,
@@ -329,7 +347,7 @@ class SchemaSerializer:
329347
indent: int | None = None,
330348
include: _IncEx | None = None,
331349
exclude: _IncEx | None = None,
332-
by_alias: bool = True,
350+
by_alias: bool | None = None,
333351
exclude_unset: bool = False,
334352
exclude_defaults: bool = False,
335353
exclude_none: bool = False,
@@ -374,7 +392,7 @@ def to_json(
374392
indent: int | None = None,
375393
include: _IncEx | None = None,
376394
exclude: _IncEx | None = None,
377-
by_alias: bool = True,
395+
by_alias: bool | None = None,
378396
exclude_none: bool = False,
379397
round_trip: bool = False,
380398
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
@@ -450,7 +468,7 @@ def to_jsonable_python(
450468
*,
451469
include: _IncEx | None = None,
452470
exclude: _IncEx | None = None,
453-
by_alias: bool = True,
471+
by_alias: bool | None = None,
454472
exclude_none: bool = False,
455473
round_trip: bool = False,
456474
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',

python/pydantic_core/core_schema.py

+14-20
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ class CoreConfig(TypedDict, total=False):
5454
`field_names` to construct error `loc`s. Default is `True`.
5555
revalidate_instances: Whether instances of models and dataclasses should re-validate. Default is 'never'.
5656
validate_default: Whether to validate default values during validation. Default is `False`.
57-
populate_by_name: Whether an aliased field may be populated by its name as given by the model attribute,
58-
as well as the alias. (Replaces 'allow_population_by_field_name' in Pydantic v1.) Default is `False`.
5957
str_max_length: The maximum length for string fields.
6058
str_min_length: The minimum length for string fields.
6159
str_strip_whitespace: Whether to strip whitespace from string fields.
@@ -74,6 +72,9 @@ class CoreConfig(TypedDict, total=False):
7472
regex_engine: The regex engine to use for regex pattern validation. Default is 'rust-regex'. See `StringSchema`.
7573
cache_strings: Whether to cache strings. Default is `True`, `True` or `'all'` is required to cache strings
7674
during general validation since validators don't know if they're in a key or a value.
75+
validate_by_alias: Whether to use the field's alias when validating against the provided input data. Default is `True`.
76+
validate_by_name: Whether to use the field's name when validating against the provided input data. Default is `False`. Replacement for `populate_by_name`.
77+
serialize_by_alias: Whether to serialize by alias. Default is `False`, expected to change to `True` in V3.
7778
"""
7879

7980
title: str
@@ -91,7 +92,6 @@ class CoreConfig(TypedDict, total=False):
9192
# whether to validate default values during validation, default False
9293
validate_default: bool
9394
# used on typed-dicts and arguments
94-
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
9595
# fields related to string fields only
9696
str_max_length: int
9797
str_min_length: int
@@ -111,6 +111,9 @@ class CoreConfig(TypedDict, total=False):
111111
coerce_numbers_to_str: bool # default: False
112112
regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex'
113113
cache_strings: Union[bool, Literal['all', 'keys', 'none']] # default: 'True'
114+
validate_by_alias: bool # default: True
115+
validate_by_name: bool # default: False
116+
serialize_by_alias: bool # default: False
114117

115118

116119
IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None'
@@ -2885,7 +2888,6 @@ class TypedDictSchema(TypedDict, total=False):
28852888
# all these values can be set via config, equivalent fields have `typed_dict_` prefix
28862889
extra_behavior: ExtraBehavior
28872890
total: bool # default: True
2888-
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
28892891
ref: str
28902892
metadata: dict[str, Any]
28912893
serialization: SerSchema
@@ -2901,7 +2903,6 @@ def typed_dict_schema(
29012903
extras_schema: CoreSchema | None = None,
29022904
extra_behavior: ExtraBehavior | None = None,
29032905
total: bool | None = None,
2904-
populate_by_name: bool | None = None,
29052906
ref: str | None = None,
29062907
metadata: dict[str, Any] | None = None,
29072908
serialization: SerSchema | None = None,
@@ -2935,7 +2936,6 @@ class MyTypedDict(TypedDict):
29352936
metadata: Any other information you want to include with the schema, not used by pydantic-core
29362937
extra_behavior: The extra behavior to use for the typed dict
29372938
total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config
2938-
populate_by_name: Whether the typed dict should populate by name
29392939
serialization: Custom serialization schema
29402940
"""
29412941
return _dict_not_none(
@@ -2947,7 +2947,6 @@ class MyTypedDict(TypedDict):
29472947
extras_schema=extras_schema,
29482948
extra_behavior=extra_behavior,
29492949
total=total,
2950-
populate_by_name=populate_by_name,
29512950
ref=ref,
29522951
metadata=metadata,
29532952
serialization=serialization,
@@ -3009,9 +3008,7 @@ class ModelFieldsSchema(TypedDict, total=False):
30093008
computed_fields: list[ComputedField]
30103009
strict: bool
30113010
extras_schema: CoreSchema
3012-
# all these values can be set via config, equivalent fields have `typed_dict_` prefix
30133011
extra_behavior: ExtraBehavior
3014-
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
30153012
from_attributes: bool
30163013
ref: str
30173014
metadata: dict[str, Any]
@@ -3026,7 +3023,6 @@ def model_fields_schema(
30263023
strict: bool | None = None,
30273024
extras_schema: CoreSchema | None = None,
30283025
extra_behavior: ExtraBehavior | None = None,
3029-
populate_by_name: bool | None = None,
30303026
from_attributes: bool | None = None,
30313027
ref: str | None = None,
30323028
metadata: dict[str, Any] | None = None,
@@ -3055,7 +3051,6 @@ def model_fields_schema(
30553051
ref: optional unique identifier of the schema, used to reference the schema in other places
30563052
metadata: Any other information you want to include with the schema, not used by pydantic-core
30573053
extra_behavior: The extra behavior to use for the typed dict
3058-
populate_by_name: Whether the typed dict should populate by name
30593054
from_attributes: Whether the typed dict should be populated from attributes
30603055
serialization: Custom serialization schema
30613056
"""
@@ -3067,7 +3062,6 @@ def model_fields_schema(
30673062
strict=strict,
30683063
extras_schema=extras_schema,
30693064
extra_behavior=extra_behavior,
3070-
populate_by_name=populate_by_name,
30713065
from_attributes=from_attributes,
30723066
ref=ref,
30733067
metadata=metadata,
@@ -3251,7 +3245,6 @@ class DataclassArgsSchema(TypedDict, total=False):
32513245
dataclass_name: Required[str]
32523246
fields: Required[list[DataclassField]]
32533247
computed_fields: list[ComputedField]
3254-
populate_by_name: bool # default: False
32553248
collect_init_only: bool # default: False
32563249
ref: str
32573250
metadata: dict[str, Any]
@@ -3264,7 +3257,6 @@ def dataclass_args_schema(
32643257
fields: list[DataclassField],
32653258
*,
32663259
computed_fields: list[ComputedField] | None = None,
3267-
populate_by_name: bool | None = None,
32683260
collect_init_only: bool | None = None,
32693261
ref: str | None = None,
32703262
metadata: dict[str, Any] | None = None,
@@ -3292,7 +3284,6 @@ def dataclass_args_schema(
32923284
dataclass_name: The name of the dataclass being validated
32933285
fields: The fields to use for the dataclass
32943286
computed_fields: Computed fields to use when serializing the dataclass
3295-
populate_by_name: Whether to populate by name
32963287
collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__`
32973288
ref: optional unique identifier of the schema, used to reference the schema in other places
32983289
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -3304,7 +3295,6 @@ def dataclass_args_schema(
33043295
dataclass_name=dataclass_name,
33053296
fields=fields,
33063297
computed_fields=computed_fields,
3307-
populate_by_name=populate_by_name,
33083298
collect_init_only=collect_init_only,
33093299
ref=ref,
33103300
metadata=metadata,
@@ -3433,7 +3423,8 @@ def arguments_parameter(
34333423
class ArgumentsSchema(TypedDict, total=False):
34343424
type: Required[Literal['arguments']]
34353425
arguments_schema: Required[list[ArgumentsParameter]]
3436-
populate_by_name: bool
3426+
validate_by_name: bool
3427+
validate_by_alias: bool
34373428
var_args_schema: CoreSchema
34383429
var_kwargs_mode: VarKwargsMode
34393430
var_kwargs_schema: CoreSchema
@@ -3445,7 +3436,8 @@ class ArgumentsSchema(TypedDict, total=False):
34453436
def arguments_schema(
34463437
arguments: list[ArgumentsParameter],
34473438
*,
3448-
populate_by_name: bool | None = None,
3439+
validate_by_name: bool | None = None,
3440+
validate_by_alias: bool | None = None,
34493441
var_args_schema: CoreSchema | None = None,
34503442
var_kwargs_mode: VarKwargsMode | None = None,
34513443
var_kwargs_schema: CoreSchema | None = None,
@@ -3472,7 +3464,8 @@ def arguments_schema(
34723464
34733465
Args:
34743466
arguments: The arguments to use for the arguments schema
3475-
populate_by_name: Whether to populate by name
3467+
validate_by_name: Whether to populate by the parameter names, defaults to `False`.
3468+
validate_by_alias: Whether to populate by the parameter aliases, defaults to `True`.
34763469
var_args_schema: The variable args schema to use for the arguments schema
34773470
var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'uniform'`, every value of the
34783471
keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`,
@@ -3485,7 +3478,8 @@ def arguments_schema(
34853478
return _dict_not_none(
34863479
type='arguments',
34873480
arguments_schema=arguments,
3488-
populate_by_name=populate_by_name,
3481+
validate_by_name=validate_by_name,
3482+
validate_by_alias=validate_by_alias,
34893483
var_args_schema=var_args_schema,
34903484
var_kwargs_mode=var_kwargs_mode,
34913485
var_kwargs_schema=var_kwargs_schema,

src/errors/validation_exception.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ impl ValidationError {
344344
let extra = state.extra(
345345
py,
346346
&SerMode::Json,
347-
true,
347+
None,
348348
false,
349349
false,
350350
true,

src/lookup_key.rs

+44
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,47 @@ fn py_get_attrs<'py>(obj: &Bound<'py, PyAny>, attr_name: &Py<PyString>) -> PyRes
577577
}
578578
}
579579
}
580+
581+
#[derive(Debug)]
582+
#[allow(clippy::struct_field_names)]
583+
pub struct LookupKeyCollection {
584+
by_name: LookupKey,
585+
by_alias: Option<LookupKey>,
586+
by_alias_then_name: Option<LookupKey>,
587+
}
588+
589+
impl LookupKeyCollection {
590+
pub fn new(py: Python, validation_alias: Option<Bound<'_, PyAny>>, field_name: &str) -> PyResult<Self> {
591+
let by_name = LookupKey::from_string(py, field_name);
592+
593+
if let Some(va) = validation_alias {
594+
let by_alias = Some(LookupKey::from_py(py, &va, None)?);
595+
let by_alias_then_name = Some(LookupKey::from_py(py, &va, Some(field_name))?);
596+
Ok(Self {
597+
by_name,
598+
by_alias,
599+
by_alias_then_name,
600+
})
601+
} else {
602+
Ok(Self {
603+
by_name,
604+
by_alias: None,
605+
by_alias_then_name: None,
606+
})
607+
}
608+
}
609+
610+
pub fn select(&self, validate_by_alias: bool, validate_by_name: bool) -> PyResult<&LookupKey> {
611+
let lookup_key_selection = match (validate_by_alias, validate_by_name) {
612+
(true, true) => self.by_alias_then_name.as_ref().unwrap_or(&self.by_name),
613+
(true, false) => self.by_alias.as_ref().unwrap_or(&self.by_name),
614+
(false, true) => &self.by_name,
615+
(false, false) => {
616+
// Note: we shouldn't hit this branch much, as this is enforced in `pydantic` with a `PydanticUserError`
617+
// at config creation time / validation function call time.
618+
return py_schema_err!("`validate_by_name` and `validate_by_alias` cannot both be set to `False`.");
619+
}
620+
};
621+
Ok(lookup_key_selection)
622+
}
623+
}

src/serializers/computed_fields.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl ComputedFields {
9898
exclude: next_exclude.as_ref(),
9999
extra: &field_extra,
100100
};
101-
let key = match extra.by_alias {
101+
let key = match extra.serialize_by_alias_or(computed_field.serialize_by_alias) {
102102
true => computed_field.alias.as_str(),
103103
false => computed_field.property_name.as_str(),
104104
};
@@ -116,6 +116,7 @@ struct ComputedField {
116116
serializer: CombinedSerializer,
117117
alias: String,
118118
alias_py: Py<PyString>,
119+
serialize_by_alias: Option<bool>,
119120
}
120121

121122
impl ComputedField {
@@ -139,6 +140,7 @@ impl ComputedField {
139140
serializer,
140141
alias: alias_py.extract()?,
141142
alias_py: alias_py.into(),
143+
serialize_by_alias: config.get_as(intern!(py, "serialize_by_alias"))?,
142144
})
143145
}
144146

@@ -163,7 +165,7 @@ impl ComputedField {
163165
if extra.exclude_none && value.is_none(py) {
164166
return Ok(());
165167
}
166-
let key = match extra.by_alias {
168+
let key = match extra.serialize_by_alias_or(self.serialize_by_alias) {
167169
true => self.alias_py.bind(py),
168170
false => property_name_py,
169171
};

0 commit comments

Comments
 (0)