strawberry icon indicating copy to clipboard operation
strawberry copied to clipboard

Field level data validators and pre/post processors

Open la4de opened this issue 4 years ago • 14 comments
trafficstars

It would be very useful to have possibility to define field level validators especially for those fields which do not have resolvers. My proposal is to add validators parameter to field to strawberry.field and validator decorator. This idea came to my mind when I was designing API for strawberry-graphql-django package (https://github.com/strawberry-graphql/strawberry-graphql-django/issues/10). Soon we realized that this could be core feature of strawberry library.

I implemented prototype code and made draft pull request #809.

Validators could be used for following purposes

  • field data validation
  • data pre processing (input types)
  • data post processing (output types)

Option 1

I'd propose following syntax for validators

def permission_validator(value, info):
    if not info.context.request.user.has_permission():
        raise Exception('Permission denied')
    return value

@strawberry.type
class User:
    name: str = strawberry.field(validators=[permission_validator])

Option 2

I'd also propose decorator syntax, which is quite similar with existing field resolver syntax. Strawberry field would provide validator decorator which could be then used to tie the field together with validator function.

@strawberry.input
class UserInput:
    name: str = strawberry.field()

    @name.validator
    def name_validator(value, info):
        if 'bad' in value:
            raise ValueError('name contains bad word')
      return value.title()

Option 3 (not doable)

def permission_validator(value, info):
    if not info.context.request.user.has_permission():
        raise Exception('Permission denied')
    return value

@strawberry.type
class User:
    # this does not work
    @strawberry.validator(permission_validator)
    name: str = strawberry.field()

la4de avatar Mar 23 '21 18:03 la4de

I still think that maybe it's possible do add this functionality without first-classing support for validation, through other techniques such as user-defined function decorators (this gets a bit tricky with input types w/o resolvers, so maybe not?).

However, if we do want to first-class validation, I wonder if we should take a similar approach that Python's @property decorator does the getter and setter functions have the same symbol name:

####### Property

class SomeClass:
    @property                     # implicitly my_field.getter
    def my_field() -> bool:
        return True

    @my_field.setter
    def my_field(value: bool):
        print(f"Set my_field={value}"

####### Strawberry

@strawberry.type
class User:
    @strawberry.field             # implicitly name.resolver
    def name() -> str:
        return "First Last"

    @name.validator
    def name(value):
        if 'bad' in value:
            raise ValueError('name contains bad word')
        return value.title()

Here, the same function symbol name is used for both the resolver and validator functions for the name field.

I think this might actually just be the standard practice in Python, and not actually a strict requirement with the property system; but it could be something for us to follow precedent

BryceBeagle avatar Mar 23 '21 20:03 BryceBeagle

I added 3rd option. I think I personally would like implement both 2nd and 3rd. Any opinions?

la4de avatar Mar 30 '21 05:03 la4de

Unfortunately, I don't think you can use decorators around assignments/expressions, only function/class definitions.

BryceBeagle avatar Mar 30 '21 06:03 BryceBeagle

Unfortunately, I don't think you can use decorators around assignments/expressions, only function/class definitions.

~Decorator can add validator functions to the internal field definition structure. Generic field resolver can then iterate validators and execute them later. See: https://github.com/la4de/strawberry/commit/04b0ca2a0fe3e6b0a2dc65cd835846bc743ade67. I think this should be doable.~ 

You are right, it is not possible to do that. :)

la4de avatar Mar 30 '21 06:03 la4de

don't know if this helps, but attrs has a concept of validators: https://www.attrs.org/en/stable/init.html#validators

patrick91 avatar Mar 30 '21 09:03 patrick91

don't know if this helps, but attrs has a concept of validators: https://www.attrs.org/en/stable/init.html#validators

I like this API. I think it would be a nice feature if Strawberry implemented it as well. (It's option 1 and 2 in the examples)

jkimbo avatar Mar 30 '21 16:03 jkimbo

I implemented prototype code and made draft pull request #809.

la4de avatar Apr 03 '21 12:04 la4de

It's really important feature. Any evolution ?

stygmate avatar Apr 02 '25 16:04 stygmate

@stygmate I was not around during the original discussion, but I think this is already possible with field extensions.

Or is there any case where you think it would not work?

bellini666 avatar Apr 04 '25 16:04 bellini666

@bellini666 It’s more a quality of life problem for developers. It doesn’t seems possible to have declarations of validators as methods in input types by using extensions, does it ?

stygmate avatar Apr 05 '25 13:04 stygmate

@stygmate would using Pydantic work for your use case?

patrick91 avatar Apr 06 '25 10:04 patrick91

@patrick91 I use a mix of strawberry-django and strawberry inputs. Sometimes needing to validate fields using subtypes not existing in django model. Can it work ?

stygmate avatar Apr 06 '25 11:04 stygmate

@stygmate so you want to use django built-in validation? can you show me an example? 😊

patrick91 avatar Apr 06 '25 11:04 patrick91

@patrick91

not only.

a example (in code/pseudo-code 😅 ):

from django.db import models


class ExampleModel(models.Model):
    name = models.CharField(max_length=255)
    data = models.JSONField()

    def __str__(self):
        return self.name
import strawberry
import strawberry_django


@strawberry.input
class DataInput:
    key: str
    value: str


@strawberry_django.input(models...)
class ExampleModelInput:
    name: str
    data: list[DataInput]

here i transform the list of DataInput into a resolver but i want to be able to add validator directly into DataInput

something like that would be great:

@strawberry.input
class DataInput:
    key: str
    value: str

    def validate_value(self, value):
        if value .... some conditions:
            raise ...

In reality i have more complex case with nested types and not only for transforming data to json.

stygmate avatar Apr 06 '25 12:04 stygmate