Data validation
Turbulette integrates with Pydantic for data validation, and can generates pydantic models directly from the GraphQL schema.
One of the most useful usages of Pydantic in an API is validating input data. As our API is typed thanks to GraphQL we already know what will be this input data, or in other words, we have the necessary information to generate Pydantic models that will validate input data.
GraphQLModel
Take the following GraphQL schema:
extend type Mutation {
registerCard(input: CreditCardInput!): SuccessPayload!
}
type SuccessPayload {
success: Boolean
errors: [String]
}
input CreditCardInput {
number: String!
cvc: Int!
expiry: Date!
name: String!
}
Tip
Note the errors
fields in the SuccessPayload
. This where validation error messages will appear if validation fails.
If you forget it, you won't see any error messages in you GraphQL response
To start, create a 📄 pyd_models.py
in your app directory: this is where Turbulette will look
for pydantic models that needs to binded to the schema:
# pyd_models.py
from turbulette.validation import GraphQLModel
from pydantic import PaymentCardNumber
class CreditCard(GraphQLModel):
class GraphQL:
gql_type = "CreditCardInput"
GraphQLModel
is derived from the base Pydantic BaseModel
class with the ability to
dynamically add fields to the existing class. Turbulette use it to add fields inferred
from the GraphQL specified by gql_type
attribute.
Right now, our CreditCard
pydantic model is strictly equivalent to this pure pydantic model:
# pyd_models.py
from datetime import datetime
from pydantic import BaseModel
class CreditCard(BaseModel):
number: str
cvc: int
expiration: datetime
name: str
The expiration field has the type Date
in our GraphQL schema. This is a custom scalar provided by Turbulette,
and is mapped to a Python Datetime
object in pydantic models.
Validate decorator
On the resolver side, Turbulette has a decorator that can be used to easily validate data using a pydantic model:
# resolvers/card.py
from ..pyd_models import CarInputModel
from turbulette.validation import validate
from turbulette import mutation
@mutation.field("registerCard")
@validate(CardInput)
async def add_card(obj, info, **kwargs):
return {
"success": True
}
When using the @validate()
, you will find validated data in kwargs["_val_data"]
if the pydantic validation succeed.
Validators
At this point, the validation doesn't add much on top of GraphQL typing (just the date parsing for expiration field), so let's add some pydantic validators:
# pyd_models.py
from turbulette.validation import GraphQLModel, validator
class CreditCard(GraphQLModel):
class GraphQL:
gql_type = "CreditCardInput"
@validator("cvc")
def check_cvc(val) -> None:
if len(str(val)) != 3:
raise ValueError("cvc number must be composed of 3 digits")
@validator("expiry")
def check_expiration(val: DateTime) -> None:
if val <= datetime.now():
raise ValueError("Expiry date has passed")
Tip
For those who have already used Pydantic, you probably know about the @validator
decorator used to add custom validation rules on fields.
But here, we use a @validator
imported from turbulette.validation
, why?
They're almost identical. Turbulette's validator is just a shortcut to the Pydantic one with check_fields=False
as a default, instead of True
,
because we use an inherited BaseModel
. The above snippet would correctly work if we used Pydantic's validator and explicitly set @validator("expiration", check_fields=False)
.
If you try entering wrong expiry date or cvc, you will get validation errors has expected:
mutation {
registerCard(input: {
number: "4000000000000002"
cvc: 111111111 # wrong
expiry: "1875-05-04" # wrong
name: "John Doe"
}) {
errors
success
}
}
Gives us:
{
"data": {
"registerCard": {
"errors": [
"cvc: cvc must be composed of 3 digits",
"expiry: Expiry date has passed"
],
"success": null
}
}
}
Override fields typing
As we register credit cards, we also want to validate the card number. Fortunately, pydantic
offers some custom types, including a PaymentCardNumber
one.
So what we want is to type the number
as PaymentCardNumber
instead of str
. We can do this by using
the fields
attributes in the GraphQL
inner class:
# pyd_models.py
class CardInput(GraphQLModel):
class GraphQL:
gql_type = "CreditCardInput"
fields = {
"number": PaymentCardNumber
}
@validator("cvc")
def check_cvc(val) -> None:
if len(str(val)) != 3:
raise ValueError("cvc must be composed of 3 digits")
@validator("expiry")
def check_expiration(val: DateTime) -> None:
if val <= datetime.now():
raise ValueError("Expiry date has passed")
The equivalent typing using pydantic's BaseModel is:
# pyd_models.py
from datetime import datetime
from pydantic import BaseModel
from pydantic.types import PaymentCardNumber
class CreditCard(BaseModel):
number: PaymentCardNumber
cvc: int
expiration: datetime
name: str