Dynamic models
Models can be created dynamically using the create_model()
factory function.
In this example, we will show how to dynamically derive a model from an existing one, making every field optional. To achieve this,
we will make use of the model_fields
model class attribute, and derive new annotations
from the field definitions to be passed to the create_model()
factory. Of course, this example can apply
to any use case where you need to derive a new model from another (remove default values, add aliases, etc).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
- Using the original model as a base will inherit the validators, computed fields, etc. The parent fields are overridden by the ones we define.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
- Using the original model as a base will inherit the validators, computed fields, etc. The parent fields are overridden by the ones we define.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
- Using the original model as a base will inherit the validators, computed fields, etc. The parent fields are overridden by the ones we define.
For each field, we generate a dictionary representation of the FieldInfo
instance
using the asdict()
method, containing the annotation, metadata and attributes.
With the following model:
class Model(BaseModel):
f: Annotated[int, Field(gt=1), WithJsonSchema({'extra': 'data'}), Field(title='F')] = 1
The FieldInfo
instance of f
will have three items in its dictionary representation:
annotation
:int
.metadata
: A list containing the type-specific constraints and other metadata:[Gt(1), WithJsonSchema({'extra': 'data'})]
.attributes
: The remaining field-specific attributes:{'title': 'F'}
.
With that in mind, we can recreate an annotation that "simulates" the one from the original model:
new_annotation = Annotated[(
f_dct['annotation'] | None, # (1)!
*f_dct['metadata'], # (2)!
Field(**f_dct['attributes']), # (3)!
)]
-
We create a new annotation from the existing one, but adding
None
as an allowed value (in our previous example, this is equivalent toint | None
). -
We unpack the metadata to be reused (in our previous example, this is equivalent to specifying
Field(gt=1)
andWithJsonSchema({'extra': 'data'})
asAnnotated
metadata). -
We specify the field-specific attributes by using the
Field()
function (in our previous example, this is equivalent toField(title='F')
).
new_annotation = Annotated[
f_dct['annotation'] | None, # (1)!
*f_dct['metadata'], # (2)!
Field(**f_dct['attributes']), # (3)!
]
-
We create a new annotation from the existing one, but adding
None
as an allowed value (in our previous example, this is equivalent toint | None
). -
We unpack the metadata to be reused (in our previous example, this is equivalent to specifying
Field(gt=1)
andWithJsonSchema({'extra': 'data'})
asAnnotated
metadata). -
We specify the field-specific attributes by using the
Field()
function (in our previous example, this is equivalent toField(title='F')
).
and specify None
as a default value (the second element of the tuple for the field definition accepted by create_model()
).
Here is a demonstration of our factory function:
from pydantic import BaseModel, Field
class Model(BaseModel):
a: Annotated[int, Field(gt=1)]
ModelOptional = make_fields_optional(Model)
m = ModelOptional()
print(m.a)
#> None
A couple notes on the implementation:
-
Our
make_fields_optional()
function is defined as returning an arbitrary Pydantic model class (-> type[BaseModel]
). An alternative solution can be to use a type variable to preserve the input class:ModelTypeT = TypeVar('ModelTypeT', bound=type[BaseModel]) def make_fields_optional(model_cls: ModelTypeT) -> ModelTypeT: ...
def make_fields_optional[ModelTypeT: type[BaseModel]](model_cls: ModelTypeT) -> ModelTypeT: ...
However, note that static type checkers won't be able to understand that all fields are now optional.
-
The experimental
MISSING
sentinel can be used as an alternative toNone
for the default values. Simply replaceNone
byMISSING
in the new annotation and default value. -
You might be tempted to make a copy of the original
FieldInfo
instances, add a default and/or perform other mutations, to then reuse it asAnnotated
metadata. While this may work in some cases, it is not a supported pattern, and could break or be deprecated at any point. We strongly encourage using the pattern from this example instead.