Hello.
I'd like to validate jsons (as well as provide schema for FastAPI), which type (or 'kind') is described by one of its fields. I.e. we have Product, which can be Book or Computer, Book can have number of pages, and Computer can have vendor name.
json1 = { "kind": "book", "pages": 42 }
json2 = { "kind": "computer", "vendor": "apple" }
So I wrote the following models hierarchy:
class Product(BaseModel):
kind: str
class Book(Product):
kind = 'book'
pages: int
@validator('kind')
def check_kind(v):
if v != 'book': raise ValueError()
return v
class Computer(Product):
kind = 'computer'
vendor: str
@validator('kind')
def check_kind(v):
if v != 'computer': raise ValueError()
return v
So what is correct way to validate jsons with those models and get correct model class with content? Means i'd like to have some magic like this:
p1 = magic(**json1) # Book(...)
p2 = magic(**json2) # Computer(...)
FastAPI can do this magic:
app = FastAPI()
@app.get('/')
async def index(product: Union[Book, Computer]):
return product
cli = TestClient(app)
print(cli.get('/', json={'kind': 'computer', 'vendor': 'apple'}).json()) # {'kind': 'computer', 'vendor': 'apple'}
print(cli.get('/', json={'kind': 'book', 'pages': 42}).json()) # {'kind': 'book', 'pages': 42}
I'd like to understand what is under the hood? If I understand correctly, it iterates over types in Union until there is no ValidationError and provide correspondent class. Can pydantic do it by itself?
Also is there more elegant way to define such models? I.e. without validators?
Would this work for you?
from pydantic import BaseModel, validator
from pydantic.error_wrappers import ValidationError
from typing import Union
from typing_extensions import Literal
class Product(BaseModel):
kind: str
class Book(Product):
kind: Literal['book']
pages: int
class Computer(Product):
kind: Literal['computer']
vendor: str
class Container(BaseModel):
product: Union[Book, Computer]
def main():
a = {'kind': 'computer', 'vendor': 'apple'} # valid
b = {'kind': 'book', 'pages': 42} # valid
c = {'kind': 'book', 'vendor': 'apple'} # invalid
d = {'kind': 'computer', 'pages': 24} # invalid
x = {'kind': 'computer', 'vendor': 'dell'} # valid
y = {'kind': 'book', 'pages': 346436} # valid
print(Container(product=a).product)
print(Container(product=b).product)
try:
print(Container(product=c).product)
except ValidationError as exc:
print(str(exc))
try:
print(Container(product=d).product)
except ValidationError as exc:
print(str(exc))
print(Container(product=x).product)
print(Container(product=y).product)
if __name__ == '__main__':
main()
=>
$ python pyd-test-union.py
kind='computer' vendor='apple'
kind='book' pages=42
2 validation errors for Container
product -> pages
field required (type=value_error.missing)
product -> kind
unexpected value; permitted: 'computer' (type=value_error.const; given=book; permitted=('computer',))
2 validation errors for Container
product -> kind
unexpected value; permitted: 'book' (type=value_error.const; given=computer; permitted=('book',))
product -> vendor
field required (type=value_error.missing)
kind='computer' vendor='dell'
kind='book' pages=346436
I'm unclear if you can get a magic parent class that can determine which product you're working with and automatically use the correct subclass. With Fast API I suspect that it's doing something similar as the approach outlined above, where you have a container and within that container you use a Union type.
Yes. thanks! Literal type hints are exactly what I am looking for!
It would be even better if there was a method without Container class, but
this method also works, thanks one more time :) !
вс, 7 июн. 2020 г. в 13:38, Atheuz notifications@github.com:
Would this work for you?
from pydantic import BaseModel, validator
from pydantic.error_wrappers import ValidationError
from typing import Union
from typing_extensions import Literalclass Product(BaseModel):
kind: strclass Book(Product):
kind: Literal['book']
pages: intclass Computer(Product):
kind: Literal['computer']
vendor: strclass Container(BaseModel):
product: Union[Book, Computer]def main():
a = {'kind': 'computer', 'vendor': 'apple'} # valid
b = {'kind': 'book', 'pages': 42} # valid
c = {'kind': 'book', 'vendor': 'apple'} # invalid
d = {'kind': 'computer', 'pages': 24} # invalid
x = {'kind': 'computer', 'vendor': 'dell'} # valid
y = {'kind': 'book', 'pages': 346436} # valid
print(Container(product=a).product)
print(Container(product=b).product)
try:
print(Container(product=c).product)
except ValidationError as exc:
print(str(exc))
try:
print(Container(product=d).product)
except ValidationError as exc:
print(str(exc))
print(Container(product=x).product)
print(Container(product=y).product)if __name__ == '__main__':
main()=>
$ python pyd-test-union.py
kind='computer' vendor='apple'
kind='book' pages=42
2 validation errors for Container
product -> pages
field required (type=value_error.missing)
product -> kind
unexpected value; permitted: 'computer' (type=value_error.const; given=book; permitted=('computer',))
2 validation errors for Container
product -> kind
unexpected value; permitted: 'book' (type=value_error.const; given=computer; permitted=('book',))
product -> vendor
field required (type=value_error.missing)
kind='computer' vendor='dell'
kind='book' pages=346436I'm unclear if you can get a magic parent class that can determine which
product you're working with and automatically use the correct subclass.
With Fast API I suspect that it's doing something similar as the approach
outlined above, where you have a container and within that container you
use a Union type.—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/samuelcolvin/pydantic/issues/1608#issuecomment-640194909,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AHRLXIQPBOANROZ5GIJSIPDRVNU23ANCNFSM4NWUUMAA
.
@Atheuz's answer is correct, but you can also do
class Container(BaseModel):
__root__: Union[Book, Computer]
container = Product(**{'kind': 'book', 'pages': 346436})
product = container.__root__
Samuel, sorry, seems this does not work
container = Product(**{'kind': 'book', 'pages': 346436})
product = container.__root__
AttributeError: 'Product' object has no attribute '__root__'
Suppose you means not Product but Container, but it also does not work
container = Container(**{'kind': 'book', 'pages': 346436})
pydantic.error_wrappers.ValidationError: 1 validation error for Container
__root__
field required (type=value_error.missing)
Most helpful comment
Would this work for you?
=>
I'm unclear if you can get a magic parent class that can determine which product you're working with and automatically use the correct subclass. With Fast API I suspect that it's doing something similar as the approach outlined above, where you have a container and within that container you use a Union type.