Skip to main content

An input validation library for Graphene

Project description

Graphene input validator

Important: this is a proof of concept and most likely not ready for production use.

The GraphQL Python ecosystem (i.e. graphene) lacks a proper way of validating input and returning meaningful errors to the client. This PoC aims at solving that. The client will know it needs to look into extensions for validation errors because of the error message ValidationError.

This library provides a class decorator @validated, for mutations, that allows for field level and input level validation similarly to DRF serializers' validate methods. To validate a field you'll need to declare a static method named validate_{field_name}. Input wide validation (e.g. for fields that depend on other fields) can be performed in the validate method. validate will only be called if all field level validation methods succeed.

It also supports recursive validation so that you can use nested InputFields and validation will be performed all the way down to the scalars.

To indicate an invalid value the corresponding validation method should raise an instance of a subclass of ValidationError. Validation methods also allow to manipulate the value on the fly (for example to minimize DB queries by swapping an ID for the corresponding object) which will then replace the corresponding value in the main input (to be used in validate and the mutation itself).

A ValidationError subclass can report one or more validation errors. Its error_details attribute must be an iterable of dictionaries, providing the details for the validation errors. The error detail mappings can contain any members, but as a convention code member is encouraged to be included.

For field level errors the error details will be amended with a path member that helps the client determine which slice of input is invalid, useful for rich forms and field highlighting on the UI.

A SingleValidationError class is provided for validation errors that only contain a single error detail. This class also supports a meta error detail property, to inform the clients of potential constraints on the input itself.

Note that verbose messages aren't supported because I strongly believe those should be handled on the client (together with localization).

Usage

Validating a mutation's input

Here is an example usage (which you can find in tests.py as well):

import graphene
from graphene_validator.decorators import validated

class TestInput(graphene.InputObjectType):
    email = graphene.String()
    people = graphene.List(PersonalDataInput)
    numbers = graphene.List(graphene.Int)
    person = graphene.InputField(PersonalDataInput)

    @staticmethod
    def validate_email(email, info, **input):
        if "@" not in email:
            raise InvalidEmailFormat
        return email.strip(" ")

    @staticmethod
    def validate_numbers(numbers, info, **input):
        if len(numbers) < 2:
            raise LengthNotInRange(min=2)
        for n in numbers:
            if n < 0 or n > 9:
                raise NotInRange(min=0, max=9)
        return numbers

    @staticmethod
    def validate(input, info):
        if input.get("people") and input.get("email"):
            first_person_name_and_age = (
                f"{input['people'][0]['the_name']}{input['people'][0]['the_age']}"
            )
            if input["email"].split("@")[0] != first_person_name_and_age:
                raise NameAndAgeInEmail
        return input


@validated
class TestMutation(graphene.Mutation):
    class Arguments:
        input = TestInput()

    result = graphene.String()

    def mutate(self, _info, input):
        return TestMutation(result="ok"))

And this is an example output:

{
            "errors": [
                {
                    "message": "ValidationError",
                    ...
                    "extensions": {
                        "validationErrors": [
                            {
                                "code": "InvalidEmailFormat",
                                "path": [
                                    "email"
                                ]
                            },
                            {
                                "code": "LengthNotInRange",
                                "path": [
                                    "people",
                                    0,
                                    "name"
                                ],
                                "meta": {"min": 1, "max": 300}
                            }
                        ]
                    }
                }
            ],
            ...
        }

Validating a field that depends on other fields or the request's context

class TestInput(graphene.InputObjectType):
    first_field = graphene.String()
    second_field = graphene.String()

    @staticmethod
    def validate_first_field(first_field, info, **input):
        second_field = input.get("second_field")
        if second_field != "desired value":
            raise InvalidSecondField
        if info.context.user.role != "admin":
            raise Unauthorized
        return first_field

    ...

Running tests

pip install -e .

pytest tests.py

Limitations

Since errors are listed in the extensions field of a generic GraphQLError, instead of using the typical union based errors, errors aren't automatically discoverable. The ideal solution would be a hybrid that allows to decorate the mutation and obtain a union that can be used by the client for autodiscovery of the error types and metadata.

An example graphene-django query is added to schema.py to allow the client to discover error types and their metadata (the latter is a TODO).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

graphene-validator-0.0.1.tar.gz (8.3 kB view details)

Uploaded Source

File details

Details for the file graphene-validator-0.0.1.tar.gz.

File metadata

  • Download URL: graphene-validator-0.0.1.tar.gz
  • Upload date:
  • Size: 8.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.8.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.2

File hashes

Hashes for graphene-validator-0.0.1.tar.gz
Algorithm Hash digest
SHA256 4020f01aa4e0574a456755af8aabe90459691a0a0bb4b98bc27700467118f179
MD5 fe7123cc4df035f81eea4c8dfd1893f3
BLAKE2b-256 88390703b1e3e0e299601f5ec7e1c52f22e6fc4401d1d68ca53966e6f2a6abff

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page