noirbizarre / flask-restplus

Fully featured framework for fast, easy and documented API development with Flask
http://flask-restplus.readthedocs.org
Other
2.73k stars 507 forks source link

How can I write a custom validator? #716

Open AbdoDabbas opened 5 years ago

AbdoDabbas commented 5 years ago

Is there a way to write a validator for a custom field type that I can use in the API model (in except decorator). I mean like this:

api.model('a_name', {
    'email': MyEmailType(required=True)
})

I tried to inherit from fields.String and did it like:

EMAIL_REGEX = re.compile(r'\S+@\S+\.\S+')

class Email(fields.String):
    """
    Email field
    """
    __schema_type__ = 'string'
    __schema_format__ = 'email'
    __schema_example__ = 'email@domain.com'

    def validate(self, value):
        if not value:
            return False if self.required else True
        if not EMAIL_REGEX.match(value):
            return False
        return True
AbdoDabbas commented 5 years ago

I tried to apply this with no luck: https://aviaryan.com/blog/gsoc/restplus-validation-custom-fields

JBarrioGarcia commented 5 years ago

I don't know how to do a custom validator, but you can use "pattern" to validate the string:

api.model('a_name', { 'email': fields.String(required=True, pattern='\S+@\S+.\S+') })

I hope it helps

j5awry commented 5 years ago

@AbdoDabbas , reading what you implemented, and your described problem, did you implement the last step? In the example you posted, they created another function, validate_payload(). While the description is incomplete, I inferred that this method was then called on each endpoint after serializing the payload. something like:

@route("/test")
class MyResource(Resource):
     @api.expect(MY_MODEL, validate=False)
     def post(self):
          payload = api.payload
          validate_payload(payload, MY_MODEL)

This is based on the code in the flask_restplus Resource class: https://github.com/noirbizarre/flask-restplus/blob/master/flask_restplus/resource.py#L75

I'm not having good luck on peeking where validate_payload is called though.

AbdoDabbas commented 5 years ago

@JBarrioGarcia I tried your way, the problem now is the error message is strange, it says:

'string' does not match '\\\\S+@\\\\S+.\\\\S+'

If 'string' can be resolved using the "title", I didn't find how to replace the pattern in the message to be something I want. So the idea is, is there a way to replace the error message?

andreixk commented 5 years ago

+1. the pattern=... solution works, but it gives a really ugly message and confuses users there's a similar problem here, but it's an odd-ball solution. This thread describes exactly the expected behaviour of a custom field, but for whatever reason it's not working. Just wondering if there's been any work done on this?

AbdoDabbas commented 5 years ago

I was able somehow to work around this by doing two things:

  1. Depending on pydantic package, I defined the model I'm expecting in the request, along with the validations I need to do.
  2. I wrote a custom function to convert the validation errors that pydantic throws and throw them again using the same schema that flask uses when you define your api.model.

Here:
https://gist.github.com/AbdoDabbas/54f1f0ad2312e95e5a43da7f062ef577
you can find a snippet code of what I mean.

Maybe pydantic integration can be built inside flask-restplus, I was going to do it specifically with directives (response, expect), but didn't know how to override it or change.

andreixk commented 5 years ago

@AbdoDabbas It seems you're doing validation after flask-restplus already had a crack at it and passed it. You can definitely do validation inside the endpoints, but that pollutes your routes and creates a fractured logic - some validation happens before, some after. Besides, the @api.expect with validate=True should do exactly that already.

AbdoDabbas commented 5 years ago

@AbdoDabbas It seems you're doing validation after flask-restplus already had a crack at it and passed it. You can definitely do validation inside the endpoints, but that pollutes your routes and creates a fractured logic - some validation happens before, some after. Besides, the @api.expect with validate=True should do exactly that already.

Actually I need to do it before, but using api model validation way will not give me what I need and it's limited as you saw in the previous comments.

I think it may work better using the before_request decorator, I will try it later.

andreixk commented 5 years ago

@AbdoDabbas I created a PR addressing this issue in a more standardized way (imho). If you don't want to wait, you can just update your own copy of the flask_restplus/model.py file and use it exactly the way you described in your post. Only difference, instead of returning False, you'd have to raise Exception to fail validation.

AbdoDabbas commented 5 years ago

@andreixk Actually I'm raising an exception already:
https://gist.github.com/AbdoDabbas/54f1f0ad2312e95e5a43da7f062ef577 This link explains the idea, we can use it in flask-restplus I think.