File: dynamic_models.md

package info (click to toggle)
pydantic 2.12.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,640 kB
  • sloc: python: 75,984; javascript: 181; makefile: 115; sh: 38
file content (199 lines) | stat: -rw-r--r-- 7,802 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
Models can be [created dynamically](../concepts/models.md#dynamic-model-creation) 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'{type.__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'{type.__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'{type.__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:

```python {lint="skip" test="skip"}
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:

```python {lint="skip" test="skip"}
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](../concepts/experimental.md#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.