Require one of two optional arguments in pydantic model
Question:
I am developing an flask restufl api using, among others, openapi3, which uses pydantic models for requests and responses. In one case I want to have a request model that can have either an id or a txt object set and, if one of these is set, fulfills some further conditions (e.g., id > 0
and len(txt) == 4
).
I tried to use pydantic validators to realize this condition. I came up with the working code below, however, the validator for txt
is quite crowded.
I am wondering if there is a cleaner solution, which separates the validations of a.) id
, b.) txt
, and c.) the condition to have one of these two, but not both set?
from typing import Optional, Any
from pydantic import BaseModel, validator, Field, ValidationError
class SomePath(BaseModel):
id: Optional[int] = Field(
default=None
)
txt: Optional[str] = Field(
default=None
)
@validator('id')
def check_id(cls, v):
if int(v) <= 0:
raise ValueError(
f'{v} is not a valid id.'
)
return v
@validator('txt', always=True)
def check_txt(cls, v, values):
if v is not None:
if not len(v) == 4:
raise ValueError(
f'{v} is not a valid txt.'
)
if 'id' in values and values['id'] is not None:
raise ValueError(
'Set only id or txt.'
)
return v
else:
if 'id' in values and values['id'] is None:
raise ValueError(
'Either id or txt must be set.'
)
return v
# Test cases
path = SomePath(id='1')
assert path.id == 1
assert path.txt is None
path = SomePath(txt='test')
assert path.id is None
assert path.txt == 'test'
try:
path = SomePath(id='-1')
except ValidationError as e:
assert e.errors()[0]['msg'] == '-1 is not a valid id.'
try:
path = SomePath(txt='tes')
except ValidationError as e:
assert e.errors()[0]['msg'] == 'tes is not a valid txt.'
try:
path = SomePath(txt='testt')
except ValidationError as e:
assert e.errors()[0]['msg'] == 'testt is not a valid txt.'
try:
path = SomePath()
except ValidationError as e:
assert e.errors()[0]['msg'] == 'Either id or txt must be set.'
Answers:
You can simplify it a little.
from typing import Optional
from pydantic import BaseModel, Field, ValidationError, root_validator
class SomePath(BaseModel):
id_: Optional[int] = Field(default=None, ge=0)
txt: Optional[str] = Field(default=None, min_length=4, max_length=4)
class Config:
validate_assignment = True
@root_validator(pre=True)
def validate_xor(cls, values): # better name needed ;)
if sum([bool(v) for v in values.values()]) != 1:
raise ValueError('Either id or txt must be set.')
return values
Firstly, you can validate an integer for id
and txt
length by Field arguments:
- The
ge=0
(greater or equal 0) is used to get your non-negative integer.
- The
min_length
and max_length
are used to get a 4 character length string.
You will receive a different error message, but in my opinion “ensure this value is greater than or equal to 0” is more precise than generic “v is not valid”
You can remove your id_
validator. You don’t need it any more.
To validate xor you can use root_validator with a slightly simpler version.
You can also add Model Config:
class Config:
validate_assignment = True
to your class to prevent bypass validation with assignment after the SomePath
instance was created.
I am developing an flask restufl api using, among others, openapi3, which uses pydantic models for requests and responses. In one case I want to have a request model that can have either an id or a txt object set and, if one of these is set, fulfills some further conditions (e.g., id > 0
and len(txt) == 4
).
I tried to use pydantic validators to realize this condition. I came up with the working code below, however, the validator for txt
is quite crowded.
I am wondering if there is a cleaner solution, which separates the validations of a.) id
, b.) txt
, and c.) the condition to have one of these two, but not both set?
from typing import Optional, Any
from pydantic import BaseModel, validator, Field, ValidationError
class SomePath(BaseModel):
id: Optional[int] = Field(
default=None
)
txt: Optional[str] = Field(
default=None
)
@validator('id')
def check_id(cls, v):
if int(v) <= 0:
raise ValueError(
f'{v} is not a valid id.'
)
return v
@validator('txt', always=True)
def check_txt(cls, v, values):
if v is not None:
if not len(v) == 4:
raise ValueError(
f'{v} is not a valid txt.'
)
if 'id' in values and values['id'] is not None:
raise ValueError(
'Set only id or txt.'
)
return v
else:
if 'id' in values and values['id'] is None:
raise ValueError(
'Either id or txt must be set.'
)
return v
# Test cases
path = SomePath(id='1')
assert path.id == 1
assert path.txt is None
path = SomePath(txt='test')
assert path.id is None
assert path.txt == 'test'
try:
path = SomePath(id='-1')
except ValidationError as e:
assert e.errors()[0]['msg'] == '-1 is not a valid id.'
try:
path = SomePath(txt='tes')
except ValidationError as e:
assert e.errors()[0]['msg'] == 'tes is not a valid txt.'
try:
path = SomePath(txt='testt')
except ValidationError as e:
assert e.errors()[0]['msg'] == 'testt is not a valid txt.'
try:
path = SomePath()
except ValidationError as e:
assert e.errors()[0]['msg'] == 'Either id or txt must be set.'
You can simplify it a little.
from typing import Optional
from pydantic import BaseModel, Field, ValidationError, root_validator
class SomePath(BaseModel):
id_: Optional[int] = Field(default=None, ge=0)
txt: Optional[str] = Field(default=None, min_length=4, max_length=4)
class Config:
validate_assignment = True
@root_validator(pre=True)
def validate_xor(cls, values): # better name needed ;)
if sum([bool(v) for v in values.values()]) != 1:
raise ValueError('Either id or txt must be set.')
return values
Firstly, you can validate an integer for id
and txt
length by Field arguments:
- The
ge=0
(greater or equal 0) is used to get your non-negative integer. - The
min_length
andmax_length
are used to get a 4 character length string.
You will receive a different error message, but in my opinion “ensure this value is greater than or equal to 0” is more precise than generic “v is not valid”
You can remove your id_
validator. You don’t need it any more.
To validate xor you can use root_validator with a slightly simpler version.
You can also add Model Config:
class Config:
validate_assignment = True
to your class to prevent bypass validation with assignment after the SomePath
instance was created.