Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support discriminated union #2336

Merged
merged 57 commits into from
Dec 18, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
22d8458
feat: add discriminated union
PrettyWood Feb 9, 2021
6dd3b73
feat: add OpenAPI spec schema
PrettyWood Feb 9, 2021
315ad3a
test: add basic example for generated schema
PrettyWood Feb 20, 2021
50d6d02
test: add validation tests
PrettyWood Feb 9, 2021
4be10c7
docs: add basic documentation
PrettyWood Feb 11, 2021
9bad44d
fix: support ForwardRef
PrettyWood Feb 20, 2021
1d6e263
test: add ForwardRef case
PrettyWood Feb 20, 2021
682bec0
fix: false positive lint error
PrettyWood Feb 20, 2021
bbb1998
improve error
PrettyWood Feb 23, 2021
c77027f
add schema/schema_json utils
PrettyWood Feb 23, 2021
6a8363f
fix tests after merge
PrettyWood Mar 13, 2021
8121c9f
refactor: add `discriminator` attribute to `FieldInfo`
PrettyWood Mar 13, 2021
d98624e
refactor: @cybojenix remarks
PrettyWood Mar 13, 2021
1c2586b
fix schema with forward ref
PrettyWood Mar 13, 2021
d7408c8
start nested
PrettyWood Mar 14, 2021
0ddb984
feat: add allowed values in error message
PrettyWood Mar 14, 2021
eb5e517
fix wrong check
PrettyWood Mar 14, 2021
1170e15
test: add nested examples
PrettyWood Mar 14, 2021
0dfa998
remove uncovered code as we don't need it
PrettyWood Mar 14, 2021
e39bdc5
docs: add nested example
PrettyWood Mar 14, 2021
dba3129
fix: support properly Annotated Field syntax
PrettyWood Mar 14, 2021
c2ec4f9
support naked annotated
PrettyWood Mar 15, 2021
4fa7bf5
fix: handle TypeError
PrettyWood Apr 7, 2021
052bfa5
make error loc more explicit
PrettyWood Apr 7, 2021
80614b8
fix behaviour with basemodel instance as value
PrettyWood Apr 19, 2021
5bd6b1d
support schema for dataclasses
PrettyWood Apr 19, 2021
8a8588f
Merge branch 'master' into PrettyWood-f/discriminated-union
samuelcolvin May 1, 2021
51e945c
tweak examples
samuelcolvin May 1, 2021
0d90c40
refactor: context manager just around code that fails
PrettyWood May 1, 2021
8b38d16
refactor: add docstring + tweak on `get_sub_types`
PrettyWood May 1, 2021
57d13fd
refactor: move `get_discriminator_values` in `utils.py`
PrettyWood May 1, 2021
8de1cbd
refactor: create `MissingDiscriminator` and `InvalidDiscriminator`
PrettyWood May 1, 2021
4aa037a
refactor: move logic in `_validate_discriminated_union`
PrettyWood May 2, 2021
e0090a3
refactor: remove `DiscriminatedUnionConfig`
PrettyWood May 2, 2021
e323de9
docs: schema/schema_json
PrettyWood May 2, 2021
3be7c09
tests: add tests with other `Literal` types
PrettyWood May 3, 2021
aefcdf1
Merge branch 'master' into f/discriminated-union
PrettyWood May 12, 2021
417601a
Merge branch 'master' into f/discriminated-union
PrettyWood Sep 6, 2021
44c6222
update 3.10
PrettyWood Sep 6, 2021
a5fe444
add schema docstring
PrettyWood Sep 6, 2021
ea1f32a
weird bug on 3.8 with `Literal[None]`
PrettyWood Sep 5, 2021
259255a
bump to view docs & coverage
samuelcolvin Dec 5, 2021
6d98976
bump to prompt tests
samuelcolvin Dec 5, 2021
6f4d437
Merge branch 'master' into f/discriminated-union
PrettyWood Dec 8, 2021
d3d3e0a
Merge branch 'master' into f/discriminated-union
PrettyWood Dec 9, 2021
242717a
move tests in dedicated file
PrettyWood Dec 9, 2021
b8b0ba2
chore: rewording
PrettyWood Dec 9, 2021
901aa01
refactor: replace property by direct slot
PrettyWood Dec 9, 2021
a41b403
refactor: faster check
PrettyWood Dec 9, 2021
d52e777
refactor: missing discriminator
PrettyWood Dec 9, 2021
f20fc57
refactor: change error to ConfigError
PrettyWood Dec 9, 2021
a73225b
refactor: use display_as_type
PrettyWood Dec 9, 2021
4d0c134
fix: mypy
PrettyWood Dec 9, 2021
f8cc585
fix: duplicate
PrettyWood Dec 9, 2021
2b0c378
feat: handle alias
PrettyWood Dec 11, 2021
4f86219
feat: handle nested unions
PrettyWood Dec 11, 2021
b974edb
tweak first example
samuelcolvin Dec 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/619-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a discriminated union. See [the doc](https://pydantic-docs.helpmanual.io/usage/types/#discriminated-unions) for more information.
20 changes: 20 additions & 0 deletions docs/examples/schema_ad_hoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Literal, Union

from typing_extensions import Annotated

from pydantic import BaseModel, Field, schema_json


class Cat(BaseModel):
pet_type: Literal['cat']
cat_name: str


class Dog(BaseModel):
pet_type: Literal['dog']
dog_name: str


Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved

print(schema_json(Pet, title='The Pet Schema', indent=2))
30 changes: 30 additions & 0 deletions docs/examples/types_union_discriminated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Literal, Union

from pydantic import BaseModel, Field, ValidationError


class Cat(BaseModel):
pet_type: Literal['cat']
name: str


class Dog(BaseModel):
pet_type: Literal['dog']
name: str


class Lizard(BaseModel):
pet_type: Literal['reptile', 'lizard']
name: str


class Model(BaseModel):
pet: Union[Cat, Dog, Lizard] = Field(..., discriminator='pet_type')
n: int


print(Model(pet={'pet_type': 'dog', 'name': 'woof'}, n='1'))
try:
Model(pet={'pet_type': 'dog'}, n='1')
except ValidationError as e:
print(e)
50 changes: 50 additions & 0 deletions docs/examples/types_union_discriminated_nested.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Literal, Union

from typing_extensions import Annotated

from pydantic import BaseModel, Field, ValidationError


class BlackCat(BaseModel):
pet_type: Literal['cat']
color: Literal['black']
black_name: str


class WhiteCat(BaseModel):
pet_type: Literal['cat']
color: Literal['white']
white_name: str


# Can also be written with a custom root type
#
# class Cat(BaseModel):
# __root__: Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')]

Cat = Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')]


class Dog(BaseModel):
pet_type: Literal['dog']
name: str


Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]


class Model(BaseModel):
pet: Pet
n: int


m = Model(pet={'pet_type': 'cat', 'color': 'black', 'black_name': 'felix'}, n=1)
print(m)
try:
Model(pet={'pet_type': 'cat', 'color': 'red'}, n='1')
except ValidationError as e:
print(e)
try:
Model(pet={'pet_type': 'cat', 'color': 'black'}, n='1')
except ValidationError as e:
print(e)
13 changes: 13 additions & 0 deletions docs/usage/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ The format of `$ref`s (`"#/definitions/FooBar"` above) can be altered by calling
with the `ref_template` keyword argument, e.g. `ApplePie.schema(ref_template='/schemas/{model}.json#/')`, here `{model}`
will be replaced with the model naming using `str.format()`.

## Getting schema of a specified type

_Pydantic_ includes two standalone utility functions `schema` and `schema_json` that can be used to
apply the schema generation logic used for _pydantic_ models in a more ad-hoc way.
These functions behave similarly to `BaseModel.schema` and `BaseModel.schema_json`,
but work with arbitrary pydantic-compatible types.

```py
{!.tmp_examples/schema_ad_hoc.py!}
```
_(This script is complete, it should run "as is")_


## Field customisation

Optionally, the `Field` function can be used to provide extra information about the field and validations.
Expand Down
33 changes: 33 additions & 0 deletions docs/usage/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,39 @@ _(This script is complete, it should run "as is")_

See more details in [Required Fields](models.md#required-fields).

#### Discriminated Unions (a.k.a. Tagged Unions)

When `Union` is used with multiple submodels, you sometimes know exactly which submodel needs to
be checked and validated and want to enforce this.
To do that you can set the same field - let's call it `my_discriminator` - in each of the submodels
with a discriminated value, which is one (or many) `Literal` value(s).
For your `Union`, you can set the discriminator in its value: `Field(discriminator='my_discriminator')`.

Setting a discriminated union has many benefits:

- validation is faster since it is only attempted against one model
- only one explicit error is raised in case of failure
- the generated JSON schema implements the [associated OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject)

```py
{!.tmp_examples/types_union_discriminated.py!}
```
_(This script is complete, it should run "as is")_

!!! note
Using the [Annotated Fields syntax](../schema/#typingannotated-fields) can be handy to regroup
the `Union` and `discriminator` information. See below for an example!

#### Nested Discriminated Unions

Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators.
In this case you can always create "intermediate" models with `__root__` and add your discriminator.

```py
{!.tmp_examples/types_union_discriminated_nested.py!}
```
_(This script is complete, it should run "as is")_

### Enums and Choices

*pydantic* uses python's standard `enum` classes to define choices.
Expand Down
2 changes: 2 additions & 0 deletions pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
'parse_file_as',
'parse_obj_as',
'parse_raw_as',
'schema',
'schema_json',
# types
'NoneStr',
'NoneBytes',
Expand Down
24 changes: 23 additions & 1 deletion pydantic/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from decimal import Decimal
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Set, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Sequence, Set, Tuple, Type, Union

from .typing import display_as_type

Expand Down Expand Up @@ -95,6 +95,8 @@
'InvalidLengthForBrand',
'InvalidByteSize',
'InvalidByteSizeUnit',
'MissingDiscriminator',
'InvalidDiscriminator',
)


Expand Down Expand Up @@ -581,3 +583,23 @@ class InvalidByteSize(PydanticValueError):

class InvalidByteSizeUnit(PydanticValueError):
msg_template = 'could not interpret byte unit: {unit}'


class MissingDiscriminator(PydanticValueError):
code = 'discriminated_union.missing_discriminator'
msg_template = 'Discriminator {discriminator_key!r} is missing in value'


class InvalidDiscriminator(PydanticValueError):
code = 'discriminated_union.invalid_discriminator'
msg_template = (
'No match for discriminator {discriminator_key!r} and value {discriminator_value!r} '
'(allowed values: {allowed_values})'
)

def __init__(self, *, discriminator_key: str, discriminator_value: Any, allowed_values: Sequence[Any]) -> None:
super().__init__(
discriminator_key=discriminator_key,
discriminator_value=discriminator_value,
allowed_values=', '.join(map(repr, allowed_values)),
)