Package: pydantic-async-validation
pydantic-async-validationAdd async validation to your pydantic models 🥳. This allows you to add validation that actually checks the database
or makes an API call or just use any code you did write async.Note that validation cannot happen during model creation, so you have to callawait obj.model_async_validate()yourself. This is due to the fact that__init__()will always be a sync method and you cannot sanely call async
methods from sync methods.Note:pydantic-async-validationis compatible withpydanticversions2.xonly. It supports
Python3.8,3.9,3.10and3.11. This is also ensured running all tests on all those versions
usingtox.Example usageimportpydanticfrompydantic_async_validationimportasync_field_validator,AsyncValidationModelMixinclassSomethingModel(AsyncValidationModelMixin,pydantic.BaseModel):name:str@async_field_validator('name')asyncdefvalidate_name(self,value:str)->None:ifvalue=="invalid":raiseValueError("Invalid name")valid_instance=SomethingModel(name="valid")awaitvalid_instance.model_async_validate()invalid_instance=SomethingModel(name="invalid")awaitinvalid_instance.model_async_validate()# will raise normal pydantic ValidationErrorField validatorsYou can useasync_field_validatorto add async validators to your model. The first argument is the name of the field
to validate. You may also pass additional field names, the validator will then be called for all fields. As validation
is happening after the instance was created, you can access all fields of the model and the validator should just be a
normal instance method (acceptingselfas its first parameter).Field validators may use any combination of the following arguments:value: The value of the field to validate (same asgetattr(self, field))field: The name of the field being validated, can be useful if you use the same validator for multiple fieldsconfig: The config of the validator, seeValidationInfofor detailsYou may also pass additional keyword arguments toasync_field_validator, they will be passed to the validator config
(ValidationInfoinstance) and be available in the validator config asconfig.extra.Example:importpydanticfrompydantic_async_validationimportasync_field_validator,AsyncValidationModelMixin,ValidationInfoclassSomethingModel(AsyncValidationModelMixin,pydantic.BaseModel):name:strother_name:str@async_field_validator('name','other_name',some_extra='value')asyncdefvalidate_name(self,value:str,field:str,config:ValidationInfo)->None:ifvalue=="invalid":# Using ValueErrorraiseValueError(f"Invalid{field}with extra{config.extra['some_extra']}")Model validatorsYou can useasync_model_validatorto add async validators to your model. The validator will be called after all field
validators have been called. The validator should be a normal instance method (acceptingselfas its first parameter).Model validators may use any combination of the following arguments:config: The config of the validator, seeValidationInfofor detailsExample:importpydanticfrompydantic_async_validationimportasync_model_validator,AsyncValidationModelMixin,ValidationInfoclassSomethingModel(AsyncValidationModelMixin,pydantic.BaseModel):name:strother_name:str@async_model_validator(some_extra='value')asyncdefvalidate_names(self,config:ValidationInfo)->None:# Using assertionassertself.name!=self.other_name,f"Names are equal with extra{config.extra['some_extra']}"When to use field vs. model validatorsAs validation happens after the model instance was created, you can access all fields just usingselfanyways. So
field vs. model validation is kind of the same thing. However field validators allow you to get thevalueof the
field as its parameter, so this is perfect when you reuse validators or want to validate multiple fields with the same
validator. Also field validators will tie theValidationErrorto the field, so it will contain the detail about which
field failed to validate. In general you should use field validators when you want to validate a single field. I also
suggest using thevalueparameter to have a clean and consistent interface for your validators.Model validators on the other hand should be used when you need to validate multiple fields at once. This is especially
useful when you want to validate that multiple fields are consistent with each other. For example you might want to
validate that a start date is before an end date. In this case you would use a model validator and access both fields
usingself. Note that model validators will be tied to"__root__"in theValidationErroras there is no specific
field to tie it to.Handling validation errorsLike with normal pydantic validation, you can catchValidationErrorand access theerrors()method to get a list of
all errors. Like with pydantic errors will be collected and be raised as oneValidationErrorat the end of validation,
including all errors that occurred.model_async_validate()will also try to validate child model instances, that are also using theAsyncValidationModelMixin. This means the following example code will work as expected:importpydanticfrompydantic_async_validationimportasync_field_validator,AsyncValidationModelMixin,ValidationInfoclassSomethingModel(AsyncValidationModelMixin,pydantic.BaseModel):name:str@async_field_validator('name')asyncdefvalidate_name(self,value:str,field:str,config:ValidationInfo)->None:ifvalue=="invalid":raiseValueError(f"Value may not be 'invalid'")classParentModel(AsyncValidationModelMixin,pydantic.BaseModel):child:SomethingModelinvalid_instance=ParentModel(child=SomethingModel(name="invalid"))awaitinvalid_instance.model_async_validate()# will raise normal pydantic ValidationErrorNote theValidationErrorwill now have the location of the error set to"child.name".Recursive validation will happen in those cases:Child models as direct instance variables (as in example above)Child models in list items (likechild: List[SomethingModel])Child models in dict values (likechild: Dict[str, SomethingModel])FastAPI supportWhen using FastAPI you also can use theAsyncValidationModelMixin, note however that FastAPI will see anyValidationErrorrisen in endpoint methods as unhandled exceptions and thus will return a HTTP 500 error. FastAPI
will only handle the validation errors happening during handling the endpoint parameters in a special way and
convert those toRequestValidationError- which will then be handled by the default exception handler forRequestValidationErrorFastAPI provides. This will then result in a HTTP 422 return code.When usingpydantic_async_validationthis would be a major drawback, as usingmodel_async_validatefor
validating input (/request) data is a totally fine use case and you cannot push this into the normal request
validation step FastAPI does. To solve this issue you can use theensure_request_validation_errorscontext manager
provided inpydantic_async_validation.fastapi. This will ensure that anyValidationErrorrisen inside the context
manager will be converted to aRequestValidationError. ThoseRequestValidationErrors will then be handled by
the default exception handler forRequestValidationErrorwhich FastAPI provides. This will then again result in a
HTTP 422 return code.Example for usage with FastAPI:importfastapiimportpydanticfrompydantic_async_validationimportAsyncValidationModelMixinfrompydantic_async_validation.fastapiimportensure_request_validation_errorsclassSomethingModel(AsyncValidationModelMixin,pydantic.BaseModel):...app=fastapi.FastAPI()@app.get("/return-http-422-on-async-validation-error")asyncdefreturn_http_422_on_async_validation_error():instance=SomethingModel(...)withensure_request_validation_errors():awaitinstance.model_async_validate()You may also useensure_request_validation_errorsto do additional validation on the request data using normal
pydantic validation and converting thoseValidationErrors toRequestValidationErrors. 😉Note:When using FastAPI you should installpydantic-async-validationusingpip install pydantic-async-validation[fastapi]to ensure FastAPI is installed in a compatible version.ContributingIf you want to contribute to this project, feel free to just fork the project,
create a dev branch in your fork and then create a pull request (PR). If you
are unsure about whether your changes really suit the project please create an
issue first, to talk about this.