|
| 1 | +Models can be [created dynamically](../concepts/models.md#dynamic-model-creation) using the [`create_model()`][pydantic.create_model] |
| 2 | +factory function. |
| 3 | + |
| 4 | +In this example, we will show how to dynamically derive a model from an existing one, making every field optional. To achieve this, |
| 5 | +we will make use of the [`model_fields`][pydantic.main.BaseModel.model_fields] model class attribute, and derive new annotations |
| 6 | +from the field definitions to be passed to the [`create_model()`][pydantic.create_model] factory. Of course, this example can apply |
| 7 | +to any use case where you need to derive a new model from another (remove default values, add aliases, etc). |
| 8 | + |
| 9 | +=== "Python 3.9" |
| 10 | + |
| 11 | + ```python {lint="skip" linenums="1"} |
| 12 | + from typing import Annotated, Union |
| 13 | + |
| 14 | + from pydantic import BaseModel, Field, create_model |
| 15 | + |
| 16 | + |
| 17 | + def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]: |
| 18 | + new_fields = {} |
| 19 | + |
| 20 | + for f_name, f_info in model_cls.model_fields.items(): |
| 21 | + f_dct = f_info.asdict() |
| 22 | + new_fields[f_name] = ( |
| 23 | + Annotated[(Union[f_dct['annotation'], None], *f_dct['metadata'], Field(**f_dct['attributes']))], |
| 24 | + None, |
| 25 | + ) |
| 26 | + |
| 27 | + return create_model( |
| 28 | + f'{type.__name__}Optional', |
| 29 | + __base__=model_cls, # (1)! |
| 30 | + **new_fields, |
| 31 | + ) |
| 32 | + ``` |
| 33 | + |
| 34 | + 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. |
| 35 | + The parent fields are overridden by the ones we define. |
| 36 | + |
| 37 | +=== "Python 3.10" |
| 38 | + |
| 39 | + ```python {lint="skip" requires="3.10" linenums="1"} |
| 40 | + from typing import Annotated |
| 41 | + |
| 42 | + from pydantic import BaseModel, Field, create_model |
| 43 | + |
| 44 | + |
| 45 | + def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]: |
| 46 | + new_fields = {} |
| 47 | + |
| 48 | + for f_name, f_info in model_cls.model_fields.items(): |
| 49 | + f_dct = f_info.asdict() |
| 50 | + new_fields[f_name] = ( |
| 51 | + Annotated[(f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes']))], |
| 52 | + None, |
| 53 | + ) |
| 54 | + |
| 55 | + return create_model( |
| 56 | + f'{type.__name__}Optional', |
| 57 | + __base__=model_cls, # (1)! |
| 58 | + **new_fields, |
| 59 | + ) |
| 60 | + ``` |
| 61 | + |
| 62 | + 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. |
| 63 | + The parent fields are overridden by the ones we define. |
| 64 | + |
| 65 | +=== "Python 3.11 and above" |
| 66 | + |
| 67 | + ```python {lint="skip" requires="3.11" linenums="1"} |
| 68 | + from typing import Annotated |
| 69 | + |
| 70 | + from pydantic import BaseModel, Field, create_model |
| 71 | + |
| 72 | + |
| 73 | + def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]: |
| 74 | + new_fields = {} |
| 75 | + |
| 76 | + for f_name, f_info in model_cls.model_fields.items(): |
| 77 | + f_dct = f_info.asdict() |
| 78 | + new_fields[f_name] = ( |
| 79 | + Annotated[f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes'])], |
| 80 | + None, |
| 81 | + ) |
| 82 | + |
| 83 | + return create_model( |
| 84 | + f'{type.__name__}Optional', |
| 85 | + __base__=model_cls, # (1)! |
| 86 | + **new_fields, |
| 87 | + ) |
| 88 | + ``` |
| 89 | + |
| 90 | + 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. |
| 91 | + The parent fields are overridden by the ones we define. |
| 92 | + |
| 93 | +For each field, we generate a dictionary representation of the [`FieldInfo`][pydantic.fields.FieldInfo] instance |
| 94 | +using the [`asdict()`][pydantic.fields.FieldInfo.asdict] method, containing the annotation, metadata and attributes. |
| 95 | + |
| 96 | +With the following model: |
| 97 | + |
| 98 | +```python {lint="skip" test="skip"} |
| 99 | +class Model(BaseModel): |
| 100 | + f: Annotated[int, Field(gt=1), WithJsonSchema({'extra': 'data'}), Field(title='F')] = 1 |
| 101 | +``` |
| 102 | + |
| 103 | +The [`FieldInfo`][pydantic.fields.FieldInfo] instance of `f` will have three items in its dictionary representation: |
| 104 | + |
| 105 | +* `annotation`: `int`. |
| 106 | +* `metadata`: A list containing the type-specific constraints and other metadata: `[Gt(1), WithJsonSchema({'extra': 'data'})]`. |
| 107 | +* `attributes`: The remaining field-specific attributes: `{'title': 'F'}`. |
| 108 | + |
| 109 | +With that in mind, we can recreate an annotation that "simulates" the one from the original model: |
| 110 | + |
| 111 | +=== "Python 3.9 and above" |
| 112 | + |
| 113 | + ```python {lint="skip" test="skip"} |
| 114 | + new_annotation = Annotated[( |
| 115 | + f_dct['annotation'] | None, # (1)! |
| 116 | + *f_dct['metadata'], # (2)! |
| 117 | + Field(**f_dct['attributes']), # (3)! |
| 118 | + )] |
| 119 | + ``` |
| 120 | + |
| 121 | + 1. We create a new annotation from the existing one, but adding `None` as an allowed value |
| 122 | + (in our previous example, this is equivalent to `int | None`). |
| 123 | + |
| 124 | + 2. We unpack the metadata to be reused (in our previous example, this is equivalent to |
| 125 | + specifying `Field(gt=1)` and `WithJsonSchema({'extra': 'data'})` as [`Annotated`][typing.Annotated] |
| 126 | + metadata). |
| 127 | + |
| 128 | + 3. We specify the field-specific attributes by using the [`Field()`][pydantic.Field] function |
| 129 | + (in our previous example, this is equivalent to `Field(title='F')`). |
| 130 | + |
| 131 | +=== "Python 3.11 and above" |
| 132 | + |
| 133 | + ```python {lint="skip" test="skip"} |
| 134 | + new_annotation = Annotated[ |
| 135 | + f_dct['annotation'] | None, # (1)! |
| 136 | + *f_dct['metadata'], # (2)! |
| 137 | + Field(**f_dct['attributes']), # (3)! |
| 138 | + ] |
| 139 | + ``` |
| 140 | + |
| 141 | + 1. We create a new annotation from the existing one, but adding `None` as an allowed value |
| 142 | + (in our previous example, this is equivalent to `int | None`). |
| 143 | + |
| 144 | + 2. We unpack the metadata to be reused (in our previous example, this is equivalent to |
| 145 | + specifying `Field(gt=1)` and `WithJsonSchema({'extra': 'data'})` as [`Annotated`][typing.Annotated] |
| 146 | + metadata). |
| 147 | + |
| 148 | + 3. We specify the field-specific attributes by using the [`Field()`][pydantic.Field] function |
| 149 | + (in our previous example, this is equivalent to `Field(title='F')`). |
| 150 | + |
| 151 | +and specify `None` as a default value (the second element of the tuple for the field definition accepted by [`create_model()`][pydantic.create_model]). |
| 152 | + |
| 153 | +Here is a demonstration of our factory function: |
| 154 | + |
| 155 | +```python {lint="skip" test="skip"} |
| 156 | +from pydantic import BaseModel, Field |
| 157 | + |
| 158 | + |
| 159 | +class Model(BaseModel): |
| 160 | + a: Annotated[int, Field(gt=1)] |
| 161 | + |
| 162 | + |
| 163 | +ModelOptional = make_fields_optional(Model) |
| 164 | + |
| 165 | +m = ModelOptional() |
| 166 | +print(m.a) |
| 167 | +#> None |
| 168 | +``` |
| 169 | + |
| 170 | +A couple notes on the implementation: |
| 171 | + |
| 172 | +* Our `make_fields_optional()` function is defined as returning an arbitrary Pydantic model class (`-> type[BaseModel]`). |
| 173 | + An alternative solution can be to use a type variable to preserve the input class: |
| 174 | + |
| 175 | + === "Python 3.9 and above" |
| 176 | + |
| 177 | + ```python {lint="skip" test="skip"} |
| 178 | + ModelTypeT = TypeVar('ModelTypeT', bound=type[BaseModel]) |
| 179 | + |
| 180 | + def make_fields_optional(model_cls: ModelTypeT) -> ModelTypeT: |
| 181 | + ... |
| 182 | + ``` |
| 183 | + |
| 184 | + === "Python 3.12 and above" |
| 185 | + |
| 186 | + ```python {lint="skip" test="skip"} |
| 187 | + def make_fields_optional[ModelTypeT: type[BaseModel]](model_cls: ModelTypeT) -> ModelTypeT: |
| 188 | + ... |
| 189 | + ``` |
| 190 | + |
| 191 | + However, note that static type checkers *won't* be able to understand that all fields are now optional. |
| 192 | + |
| 193 | +* The experimental [`MISSING` sentinel](../concepts/experimental.md#missing-sentinel) can be used as an alternative to `None` |
| 194 | + for the default values. Simply replace `None` by `MISSING` in the new annotation and default value. |
| 195 | + |
| 196 | +* You might be tempted to make a copy of the original [`FieldInfo`][pydantic.fields.FieldInfo] instances, add a |
| 197 | + default and/or perform other mutations, to then reuse it as [`Annotated`][typing.Annotated] metadata. While this |
| 198 | + may work in some cases, it is **not** a supported pattern, and could break or be deprecated at any point. We strongly |
| 199 | + encourage using the pattern from this example instead. |
0 commit comments