Skip to content

Commit ebad320

Browse files
committed
Add ability to specify dataclass name in dataclass_schema
1 parent 3603f10 commit ebad320

File tree

3 files changed

+63
-4
lines changed

3 files changed

+63
-4
lines changed

pydantic_core/core_schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3111,6 +3111,7 @@ class DataclassSchema(TypedDict, total=False):
31113111
type: Required[Literal['dataclass']]
31123112
cls: Required[Type[Any]]
31133113
schema: Required[CoreSchema]
3114+
cls_name: str
31143115
post_init: bool # default: False
31153116
revalidate_instances: Literal['always', 'never', 'subclass-instances'] # default: 'never'
31163117
strict: bool # default: False
@@ -3124,6 +3125,7 @@ def dataclass_schema(
31243125
cls: Type[Any],
31253126
schema: CoreSchema,
31263127
*,
3128+
cls_name: str | None = None,
31273129
post_init: bool | None = None,
31283130
revalidate_instances: Literal['always', 'never', 'subclass-instances'] | None = None,
31293131
strict: bool | None = None,
@@ -3139,6 +3141,7 @@ def dataclass_schema(
31393141
Args:
31403142
cls: The dataclass type, used to to perform subclass checks
31413143
schema: The schema to use for the dataclass fields
3144+
cls_name: The name to use for the class in error locs, etc; this is useful for generics (default: cls.__name__)
31423145
post_init: Whether to call `__post_init__` after validation
31433146
revalidate_instances: whether instances of models and dataclasses (including subclass instances)
31443147
should re-validate defaults to config.revalidate_instances, else 'never'
@@ -3151,6 +3154,7 @@ def dataclass_schema(
31513154
return dict_not_none(
31523155
type='dataclass',
31533156
cls=cls,
3157+
cls_name=cls_name,
31543158
schema=schema,
31553159
post_init=post_init,
31563160
revalidate_instances=revalidate_instances,

src/validators/dataclass.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ impl BuildValidator for DataclassValidator {
428428
let py = schema.py();
429429

430430
let class: &PyType = schema.get_as_req(intern!(py, "cls"))?;
431+
let name = match schema.get_as_req::<String>(intern!(py, "cls_name")) {
432+
Ok(name) => name,
433+
Err(_) => class.getattr(intern!(py, "__name__"))?.extract()?,
434+
};
431435
let sub_schema: &PyAny = schema.get_as_req(intern!(py, "schema"))?;
432436
let validator = build_validator(sub_schema, config, definitions)?;
433437

@@ -447,9 +451,7 @@ impl BuildValidator for DataclassValidator {
447451
config,
448452
intern!(py, "revalidate_instances"),
449453
)?)?,
450-
// as with model, get the class's `__name__`, not using `class.name()` since it uses `__qualname__`
451-
// which is not what we want here
452-
name: class.getattr(intern!(py, "__name__"))?.extract()?,
454+
name,
453455
frozen: schema.get_as(intern!(py, "frozen"))?.unwrap_or(false),
454456
}
455457
.into())

tests/validators/test_dataclasses.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import re
3-
from typing import Any, Dict, List, Union
3+
from typing import Any, Dict, List, Optional, Union
44

55
import pytest
66
from dirty_equals import IsListOrTuple, IsStr
@@ -1137,3 +1137,56 @@ class Model:
11371137
v.validate_assignment(instance, 'number', 2)
11381138
assert instance.number == 2
11391139
assert calls == [({'number': 1}, ({'number': 1}, None)), ({'number': 2}, ({'number': 2}, None))]
1140+
1141+
1142+
@dataclasses.dataclass
1143+
class FooParentDataclass:
1144+
foo: Optional[FooDataclass]
1145+
1146+
1147+
def test_custom_dataclass_names():
1148+
# Note: normally you would use the same values for DataclassArgsSchema.dataclass_name and DataclassSchema.cls_name,
1149+
# but I have purposely made them different here to show which parts of the errors are affected by which.
1150+
# I have used square brackets in the names to hint that the most likely reason for using a value different from
1151+
# cls.__name__ is for use with generic types.
1152+
schema = core_schema.dataclass_schema(
1153+
FooParentDataclass,
1154+
core_schema.dataclass_args_schema(
1155+
'FooParentDataclass',
1156+
[
1157+
core_schema.dataclass_field(
1158+
name='foo',
1159+
schema=core_schema.union_schema(
1160+
[
1161+
core_schema.dataclass_schema(
1162+
FooDataclass,
1163+
core_schema.dataclass_args_schema(
1164+
'FooDataclass[dataclass_args_schema]',
1165+
[
1166+
core_schema.dataclass_field(name='a', schema=core_schema.str_schema()),
1167+
core_schema.dataclass_field(name='b', schema=core_schema.bool_schema()),
1168+
],
1169+
),
1170+
cls_name='FooDataclass[cls_name]',
1171+
),
1172+
core_schema.none_schema(),
1173+
]
1174+
),
1175+
)
1176+
],
1177+
),
1178+
)
1179+
1180+
v = SchemaValidator(schema)
1181+
with pytest.raises(ValidationError) as exc_info:
1182+
v.validate_python({'foo': 123})
1183+
assert exc_info.value.errors(include_url=False) == [
1184+
{
1185+
'ctx': {'dataclass_name': 'FooDataclass[dataclass_args_schema]'},
1186+
'input': 123,
1187+
'loc': ('foo', 'FooDataclass[cls_name]'),
1188+
'msg': 'Input should be a dictionary or an instance of FooDataclass[dataclass_args_schema]',
1189+
'type': 'dataclass_type',
1190+
},
1191+
{'input': 123, 'loc': ('foo', 'none'), 'msg': 'Input should be None', 'type': 'none_required'},
1192+
]

0 commit comments

Comments
 (0)