Pydantic: How to validate json when it's class provided in one of the fields?

Created on 7 Jun 2020  ·  4Comments  ·  Source: samuelcolvin/pydantic

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?

question

Most helpful comment

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.

All 4 comments

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 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.


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)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sbv-trueenergy picture sbv-trueenergy  ·  3Comments

iwoloschin picture iwoloschin  ·  3Comments

drpoggi picture drpoggi  ·  3Comments

ashpreetbedi picture ashpreetbedi  ·  3Comments

mgresko picture mgresko  ·  3Comments