Description
Question
Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())"
:
pydantic version: 1.5.1
pydantic compiled: True
install path: /.../venv/lib/python3.7/site-packages/pydantic
python version: 3.7.4 (default, Sep 7 2019, 18:27:02) [Clang 10.0.1 (clang-1001.0.46.4)]
platform: Darwin-19.4.0-x86_64-i386-64bit
optional deps. installed: ['typing-extensions']
Hey Samuel & team,
First of all, huge fan of Pydantic, it makes working in Python so much better. I've been through the docs and many of the issues several times, and until this point I've found all the answers I was looking for. If I missed one in this case - sorry in advance!
Looking to be able to transform a model at runtime to another with the same fields but where all are Optional
, akin to Typescript's Partial (hence the issue title). Something similar is discussed in part in at least one FastAPI issue, but since the answer there is basically "duplicate your model" and since this is inherently a pydantic
thing, I thought I'd ask here.
By way of a little bit of hacking on model internals, I've been able to get the functionality I need for a relatively trivial use case:
from __future__ import annotations
from pydantic import BaseModel, create_model
from typing import Optional
class BaseSchemaModel(BaseModel):
# ...
@classmethod
def to_partial(cls) -> BaseSchemaModel:
return get_partial_model(cls)
def get_partial_model(model: BaseModel) -> BaseModel:
"""
Return a model similar to the original, but where all fields are Optional.
Note: this minimal implementation means that many Pydantic features will be discarded
(such as alias). This is not by design and could stand to improve.
"""
new_fields = {
name: (Optional[model.__annotations__.get(name)], model.__fields__[name].default or None)
for name in model.__fields__
}
return create_model("GeneratedPartialModel", **new_fields, __config__=model.__config__)
verified with a simple test:
import pytest
from pydantic import ValidationError
from typing import Optional
from app.utilities.pydantic import BaseSchemaModel
def test_partial_model():
class TestModel(BaseSchemaModel):
a: int
b: int = 2
c: str
d: Optional[str]
with pytest.raises(ValidationError):
TestModel()
model = TestModel(a=1, c='hello')
assert model.dict() == {'a': 1, 'b': 2, 'c': 'hello', 'd': None}
PartialTestModel = TestModel.to_partial()
partial_model = PartialTestModel()
assert partial_model.dict() == {'a': None, 'b': 2, 'c': None, 'd': None}
However this doesn't support much of Pydantic's functionality (aliases, for a start). In the interest of a better issue report and possibly a PR, I tried a couple other things, but in the time I allocated did not get them to work:
from pydantic import BaseModel, create_model
from pydantic.fields import ModelField
class Original(BaseModel):
a: int
b: str
c: Optional[str]
class PartialModel(BaseModel):
a: Optional[int]
b: Optional[str]
c: Optional[str]
def copy_model_field(field: ModelField, **kwargs) -> ModelField:
params = (
'name',
'type_',
'class_validators',
'model_config',
'default',
'default_factory',
'required',
'alias',
'field_info'
)
return ModelField(**{param: kwargs.get(param, getattr(field, param)) for param in params})
# Doesn't work - ModelField not acceptable in place of FieldInfo
GeneratedPartialModel = create_model('GeneratedPartialModel', **{name: copy_model_field(field, required=False) for name, field in Original.__fields__.items()})
# Doesn't work - field_info doesn't contain all the necessary information
GeneratedPartialModel = create_model('GeneratedPartialModel', **{name: copy_model_field(field, required=False).field_info for name, field in Original.__fields__.items()})
# This works for my use case - but without aliases and probably without some other functionality as well
new_fields = {name: (Optional[Original.__annotations__.get(name)], Original.__fields__[name].default or None) for name in Original.__fields__}
GeneratedPartialModel = create_model('GeneratedPartialModel', **new_fields)
Would be happy to put in a PR for this, if
a. it doesn't exist already
b. it would be useful
c. I knew where that would best fit - on ModelMetaclass
? BaseModel
? A utility function to_partial
?
Thanks!