Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for partial model validation #3179

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions changes/3179-lsapan.md
@@ -0,0 +1 @@
Add support for partial model validation
3 changes: 3 additions & 0 deletions docs/usage/model_config.md
Expand Up @@ -115,6 +115,9 @@ not be included in the model schemas. **Note**: this means that attributes on th
**`copy_on_model_validation`**
: whether or not inherited models used as fields should be reconstructed (copied) on validation instead of being kept untouched (default: `True`)

**`partial`**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I let you see with the author of the other PR and others for the best wording (total was my suggestion to mimic TypedDict but that's just an idea)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm quite flexible on phrasing. I was basing it off of Django REST Framework as that's where I'm coming to FastAPI / pydantic from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on "partial", because:

  1. It matches (roughly) TypeScript which is kind of the gold standard for type hints in dynamic languages
  2. It means you can talk about "partial models" which is cleaner than "non-total models"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can we add a note to the docs explaining that the word "total" could also be used with the opposite meaning.

: whether or not the model should allow required fields to be omitted. Useful when working with CRUD operations, and you need to use an existing model for a PATCH operation. (default: `False`)

## Change behaviour globally

If you wish to change the behaviour of _pydantic_ globally, you can create your own custom `BaseModel`
Expand Down
1 change: 1 addition & 0 deletions pydantic/config.py
Expand Up @@ -62,6 +62,7 @@ class BaseConfig:
json_dumps: Callable[..., str] = json.dumps
json_encoders: Dict[Type[Any], AnyCallable] = {}
underscore_attrs_are_private: bool = False
partial: bool = False

# Whether or not inherited models as fields should be reconstructed as base model
copy_on_model_validation: bool = True
Expand Down
17 changes: 17 additions & 0 deletions pydantic/main.py
Expand Up @@ -756,6 +756,20 @@ def update_forward_refs(cls, **localns: Any) -> None:
for f in cls.__fields__.values():
update_field_forward_refs(f, globalns=globalns, localns=localns)

@classmethod
def partial_model(cls: Type['Model']) -> Type['Model']:
lsapan marked this conversation as resolved.
Show resolved Hide resolved
"""
Create a version of the model that is suitable for partial validation.
Useful when working with CRUD operations, and you need to use an
existing model for a PATCH operation.
"""

class PartialModel(cls): # type: ignore
class Config(cls.Config): # type: ignore
partial = True

return PartialModel

def __iter__(self) -> 'TupleGenerator':
"""
so `dict(model)` works
Expand Down Expand Up @@ -978,6 +992,9 @@ def validate_model( # noqa: C901 (ignore complexity)
using_name = True

if value is _missing:
if config.partial:
lsapan marked this conversation as resolved.
Show resolved Hide resolved
continue

if field.required:
errors.append(ErrorWrapper(MissingError(), loc=field.alias))
continue
Expand Down
14 changes: 14 additions & 0 deletions tests/test_edge_cases.py
Expand Up @@ -1892,3 +1892,17 @@ class Config:
arbitrary_types_allowed = True

assert Model().x == Foo()


def test_config_partial():
class Foo(BaseModel):
a: str = Field(...)
b: str = Field(...)

Bar = Foo.partial_model()

assert Foo.Config.partial is False
assert Bar.Config.partial is True

b = Bar(a='x')
lsapan marked this conversation as resolved.
Show resolved Hide resolved
assert b.dict(exclude_unset=True) == {'a': 'x'}