docs/examples/dynamic_models.md
Models can be created dynamically using the [create_model()][pydantic.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][pydantic.main.BaseModel.model_fields] model class attribute, and derive new annotations
from the field definitions to be passed to the [create_model()][pydantic.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).
=== "Python 3.9"
```python {lint="skip" linenums="1"}
from typing import Annotated, Union
from pydantic import BaseModel, Field, create_model
def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]:
new_fields = {}
for f_name, f_info in model_cls.model_fields.items():
f_dct = f_info.asdict()
new_fields[f_name] = (
Annotated[(Union[f_dct['annotation'], None], *f_dct['metadata'], Field(**f_dct['attributes']))],
None,
)
return create_model(
f'{model_cls.__name__}Optional',
__base__=model_cls, # (1)!
**new_fields,
)
```
1. Using the original model as a base will inherit the [validators](../concepts/validators.md), [computed fields](../concepts/fields.md#the-computed_field-decorator), etc.
The parent fields are overridden by the ones we define.
=== "Python 3.10"
```python {lint="skip" requires="3.10" linenums="1"}
from typing import Annotated
from pydantic import BaseModel, Field, create_model
def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]:
new_fields = {}
for f_name, f_info in model_cls.model_fields.items():
f_dct = f_info.asdict()
new_fields[f_name] = (
Annotated[(f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes']))],
None,
)
return create_model(
f'{model_cls.__name__}Optional',
__base__=model_cls, # (1)!
**new_fields,
)
```
1. Using the original model as a base will inherit the [validators](../concepts/validators.md), [computed fields](../concepts/fields.md#the-computed_field-decorator), etc.
The parent fields are overridden by the ones we define.
=== "Python 3.11 and above"
```python {lint="skip" requires="3.11" linenums="1"}
from typing import Annotated
from pydantic import BaseModel, Field, create_model
def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]:
new_fields = {}
for f_name, f_info in model_cls.model_fields.items():
f_dct = f_info.asdict()
new_fields[f_name] = (
Annotated[f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes'])],
None,
)
return create_model(
f'{model_cls.__name__}Optional',
__base__=model_cls, # (1)!
**new_fields,
)
```
1. Using the original model as a base will inherit the [validators](../concepts/validators.md), [computed fields](../concepts/fields.md#the-computed_field-decorator), etc.
The parent fields are overridden by the ones we define.
For each field, we generate a dictionary representation of the [FieldInfo][pydantic.fields.FieldInfo] instance
using the [asdict()][pydantic.fields.FieldInfo.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][pydantic.fields.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:
=== "Python 3.9 and above"
```python {lint="skip" test="skip"}
new_annotation = Annotated[(
f_dct['annotation'] | None, # (1)!
*f_dct['metadata'], # (2)!
Field(**f_dct['attributes']), # (3)!
)]
```
1. We create a new annotation from the existing one, but adding `None` as an allowed value
(in our previous example, this is equivalent to `int | None`).
2. We unpack the metadata to be reused (in our previous example, this is equivalent to
specifying `Field(gt=1)` and `WithJsonSchema({'extra': 'data'})` as [`Annotated`][typing.Annotated]
metadata).
3. We specify the field-specific attributes by using the [`Field()`][pydantic.Field] function
(in our previous example, this is equivalent to `Field(title='F')`).
=== "Python 3.11 and above"
```python {lint="skip" test="skip"}
new_annotation = Annotated[
f_dct['annotation'] | None, # (1)!
*f_dct['metadata'], # (2)!
Field(**f_dct['attributes']), # (3)!
]
```
1. We create a new annotation from the existing one, but adding `None` as an allowed value
(in our previous example, this is equivalent to `int | None`).
2. We unpack the metadata to be reused (in our previous example, this is equivalent to
specifying `Field(gt=1)` and `WithJsonSchema({'extra': 'data'})` as [`Annotated`][typing.Annotated]
metadata).
3. We specify the field-specific attributes by using the [`Field()`][pydantic.Field] function
(in our previous example, this is equivalent to `Field(title='F')`).
and specify None as a default value (the second element of the tuple for the field definition accepted by [create_model()][pydantic.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:
=== "Python 3.9 and above"
```python {lint="skip" test="skip"}
ModelTypeT = TypeVar('ModelTypeT', bound=type[BaseModel])
def make_fields_optional(model_cls: ModelTypeT) -> ModelTypeT:
...
```
=== "Python 3.12 and above"
```python {lint="skip" test="skip"}
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 to None
for the default values. Simply replace None by MISSING in the new annotation and default value.
You might be tempted to make a copy of the original [FieldInfo][pydantic.fields.FieldInfo] instances, add a
default and/or perform other mutations, to then reuse it as [Annotated][typing.Annotated] 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.