我可以覆盖 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,这将正确生成文档。这种方法的另一个优点是可以大大减少样板文件。