我可以覆盖 Pydantic 父模型中的字段以使它们成为可选的吗?
Can I override fields from a Pydantic parent model to make them optional?
我有两个像这样的 class。
class Parent(BaseModel):
id: int
name: str
email: str
class ParentUpdate(BaseModel):
id: Optional[int]
name: Optional[str]
email: Optional[str]
这两个实际上是相同的,但 Parent
class 使所有字段都成为必填项。
我想在 FastAPI 中将 Parent
class 用于 POST 请求正文,因此所有字段都应该是必需的。但我想将后者用于 PUT 请求正文,因为用户可以设置选择性字段并且其余部分保持不变。
我看过 Required Optional Fields 但它们与我想做的不符。
如果有办法我可以继承 ParentUpdate
中的 Parent
class 并修改 Parent
中的所有字段,使它们 Optional
会减少混乱。此外,Parent
class 中存在一些验证器,我必须在 ParentUpdate
class 中重写,我也想避免。
有什么办法吗?谢谢。
您可以将子类中的可选字段设为必填,但不能将子类中的必填字段设为可选。在 fastapi 作者 tiangolo 的样板项目中,他为您的示例使用了这样的模式:
class ParentBase(BaseModel):
"""Shared properties."""
name: str
email: str
class ParentCreate(ParentBase):
"""Properties to receive on item creation."""
# dont need id here if your db autocreates it
pass
class ParentUpdate(ParentBase):
"""Properties to receive on item update."""
# dont need id as you are likely PUTing to /parents/{id}
# other fields should not be optional in a PUT
# maybe what you are wanting is a PATCH schema?
pass
class ParentInDBBase(ParentBase):
"""Properties shared by models stored in DB - !exposed in create/update."""
# primary key exists in db, but not in base/create/update
id: int
class Parent(ParentInDBBase):
"""Properties to return to client."""
# optionally include things like relationships returned to consumer
# related_things: List[Thing]
pass
class ParentInDB(ParentInDBBase):
"""Additional properties stored in DB."""
# could be secure things like passwords?
pass
是的,我同意这非常冗长,我希望不是这样。您仍然可能最终得到更具体到 UI 中特定表单的其他模式。显然,您可以删除其中一些,因为在此示例中它们不是必需的,但根据数据库中的其他字段,可能需要它们,或者您可能需要设置默认值、验证等。
根据我对验证器的经验,你必须重新声明它们,但你可以使用共享函数,即:
def clean_article_url(cls, v):
return clean_context_url(v.strip())
class MyModel(BaseModel):
article_url: str
_clean_url = pydantic.validator("article_url", allow_reuse=True)(clean_article_url)
我提前道歉,我确信这是一个可怕的解决方法,但它对我有用:
def make_child_fields_optional(parent_class: Type[BaseModel], child_class: Type[BaseModel]):
for key in parent_class.__fields__:
child_class.__fields__.get(key).required = False
class BasePerson(BaseModel):
name: str
email: str
login: str
class UpdatePerson(BasePerson):
pass # or whatever
make_child_fields_optional(BasePerson, UpdatePerson)
我的建议是不要发明困难的模式,我也对 pydantic 功能感兴趣,但它们看起来都很丑陋且难以理解(甚至不适用于某些任务并且有约束)。
参见 Python pydantic, make every field of ancestor are Optional
来自 pydantic 维护者的回答
正如在对类似问题的回答中所述,我正在使用以下方法(致谢 Aron Podrigal):
import inspect
from pydantic import BaseModel
def optional(*fields):
"""Decorator function used to modify a pydantic model's fields to all be optional.
Alternatively, you can also pass the field names that should be made optional as arguments
to the decorator.
Taken from https://github.com/samuelcolvin/pydantic/issues/1223#issuecomment-775363074
"""
def dec(_cls):
for field in fields:
_cls.__fields__[field].required = False
return _cls
if fields and inspect.isclass(fields[0]) and issubclass(fields[0], BaseModel):
cls = fields[0]
fields = cls.__fields__
return dec(cls)
return dec
在您的示例中,您将像这样使用它:
@optional
class ParentUpdate(Parent):
pass
覆盖字段可能且容易。 (有人提到不可能将必填字段覆盖为可选字段,但我不同意)。
这个例子没有任何问题:
class Parent(BaseModel):
id: int
name: str
email: str
class ParentUpdate(Parent): ## Note that this inherits 'Parent' class (not BaseModel)
id: Optional[int] # this will convert id from required to optional
对于我的案例来说,创建一个新的 class 是唯一可行的解决方案,但打包到一个函数中非常方便:
from pydantic import BaseModel, create_model
from typing import Optional
def make_optional(baseclass):
# Extracts the fields and validators from the baseclass and make fields optional
fields = baseclass.__fields__
validators = {'__validators__': baseclass.__validators__}
optional_fields = {key: (Optional[item.type_], None) for key, item in fields.items()}
return create_model(f'{baseclass.__name__}Optional', **optional_fields, __validators__=validators)
class Parent(BaseModel):
id: int
name: str
email: str
ParentUpdate = make_optional(Parent)
前后对比:
Parent.__fields__
{'id': ModelField(name='id', type=int, required=True),
'name': ModelField(name='name', type=str, required=True),
'email': ModelField(name='email', type=str, required=True)}
ParentUpdate.__fields__
{'id': ModelField(name='id', type=Optional[int], required=False, default=None),
'name': ModelField(name='name', type=Optional[str], required=False, default=None),
'email': ModelField(name='email', type=Optional[str], required=False, default=None)}
它确实有效,而且如果需要,它还允许您过滤掉 class 的某些字段。
此外,对于 FastApi,您可以直接使用 make_optional(Parent) 作为 API 调用中的 type-hint,这将正确生成文档。这种方法的另一个优点是可以大大减少样板文件。
我有两个像这样的 class。
class Parent(BaseModel):
id: int
name: str
email: str
class ParentUpdate(BaseModel):
id: Optional[int]
name: Optional[str]
email: Optional[str]
这两个实际上是相同的,但 Parent
class 使所有字段都成为必填项。
我想在 FastAPI 中将 Parent
class 用于 POST 请求正文,因此所有字段都应该是必需的。但我想将后者用于 PUT 请求正文,因为用户可以设置选择性字段并且其余部分保持不变。
我看过 Required Optional Fields 但它们与我想做的不符。
如果有办法我可以继承 ParentUpdate
中的 Parent
class 并修改 Parent
中的所有字段,使它们 Optional
会减少混乱。此外,Parent
class 中存在一些验证器,我必须在 ParentUpdate
class 中重写,我也想避免。
有什么办法吗?谢谢。
您可以将子类中的可选字段设为必填,但不能将子类中的必填字段设为可选。在 fastapi 作者 tiangolo 的样板项目中,他为您的示例使用了这样的模式:
class ParentBase(BaseModel):
"""Shared properties."""
name: str
email: str
class ParentCreate(ParentBase):
"""Properties to receive on item creation."""
# dont need id here if your db autocreates it
pass
class ParentUpdate(ParentBase):
"""Properties to receive on item update."""
# dont need id as you are likely PUTing to /parents/{id}
# other fields should not be optional in a PUT
# maybe what you are wanting is a PATCH schema?
pass
class ParentInDBBase(ParentBase):
"""Properties shared by models stored in DB - !exposed in create/update."""
# primary key exists in db, but not in base/create/update
id: int
class Parent(ParentInDBBase):
"""Properties to return to client."""
# optionally include things like relationships returned to consumer
# related_things: List[Thing]
pass
class ParentInDB(ParentInDBBase):
"""Additional properties stored in DB."""
# could be secure things like passwords?
pass
是的,我同意这非常冗长,我希望不是这样。您仍然可能最终得到更具体到 UI 中特定表单的其他模式。显然,您可以删除其中一些,因为在此示例中它们不是必需的,但根据数据库中的其他字段,可能需要它们,或者您可能需要设置默认值、验证等。
根据我对验证器的经验,你必须重新声明它们,但你可以使用共享函数,即:
def clean_article_url(cls, v):
return clean_context_url(v.strip())
class MyModel(BaseModel):
article_url: str
_clean_url = pydantic.validator("article_url", allow_reuse=True)(clean_article_url)
我提前道歉,我确信这是一个可怕的解决方法,但它对我有用:
def make_child_fields_optional(parent_class: Type[BaseModel], child_class: Type[BaseModel]):
for key in parent_class.__fields__:
child_class.__fields__.get(key).required = False
class BasePerson(BaseModel):
name: str
email: str
login: str
class UpdatePerson(BasePerson):
pass # or whatever
make_child_fields_optional(BasePerson, UpdatePerson)
我的建议是不要发明困难的模式,我也对 pydantic 功能感兴趣,但它们看起来都很丑陋且难以理解(甚至不适用于某些任务并且有约束)。 参见 Python pydantic, make every field of ancestor are Optional 来自 pydantic 维护者的回答
正如在对类似问题的回答中所述,我正在使用以下方法(致谢 Aron Podrigal):
import inspect
from pydantic import BaseModel
def optional(*fields):
"""Decorator function used to modify a pydantic model's fields to all be optional.
Alternatively, you can also pass the field names that should be made optional as arguments
to the decorator.
Taken from https://github.com/samuelcolvin/pydantic/issues/1223#issuecomment-775363074
"""
def dec(_cls):
for field in fields:
_cls.__fields__[field].required = False
return _cls
if fields and inspect.isclass(fields[0]) and issubclass(fields[0], BaseModel):
cls = fields[0]
fields = cls.__fields__
return dec(cls)
return dec
在您的示例中,您将像这样使用它:
@optional
class ParentUpdate(Parent):
pass
覆盖字段可能且容易。 (有人提到不可能将必填字段覆盖为可选字段,但我不同意)。
这个例子没有任何问题:
class Parent(BaseModel):
id: int
name: str
email: str
class ParentUpdate(Parent): ## Note that this inherits 'Parent' class (not BaseModel)
id: Optional[int] # this will convert id from required to optional
对于我的案例来说,创建一个新的 class 是唯一可行的解决方案,但打包到一个函数中非常方便:
from pydantic import BaseModel, create_model
from typing import Optional
def make_optional(baseclass):
# Extracts the fields and validators from the baseclass and make fields optional
fields = baseclass.__fields__
validators = {'__validators__': baseclass.__validators__}
optional_fields = {key: (Optional[item.type_], None) for key, item in fields.items()}
return create_model(f'{baseclass.__name__}Optional', **optional_fields, __validators__=validators)
class Parent(BaseModel):
id: int
name: str
email: str
ParentUpdate = make_optional(Parent)
前后对比:
Parent.__fields__
{'id': ModelField(name='id', type=int, required=True),
'name': ModelField(name='name', type=str, required=True),
'email': ModelField(name='email', type=str, required=True)}
ParentUpdate.__fields__
{'id': ModelField(name='id', type=Optional[int], required=False, default=None),
'name': ModelField(name='name', type=Optional[str], required=False, default=None),
'email': ModelField(name='email', type=Optional[str], required=False, default=None)}
它确实有效,而且如果需要,它还允许您过滤掉 class 的某些字段。
此外,对于 FastApi,您可以直接使用 make_optional(Parent) 作为 API 调用中的 type-hint,这将正确生成文档。这种方法的另一个优点是可以大大减少样板文件。