vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.23k stars 429 forks source link

Django Forms and validators #631

Open liminspace opened 1 year ago

liminspace commented 1 year ago

The django-ninja is awesome, but I found something that is not supported. Django has validators that used in models and forms. It would be very helpful to support that feature in django-ninja. Of course after data-validation we can put the data into a form and validate it. But it's better to have it in a Schema class or so. It can validate data in regular way django-ninja does and then put an object into a django form and validate there. The main point of it is having single format of validation error and single interface to get validated data. And also reusing already written code in forms and validators, especially when we use 3rd party libraries. Any thought about it?

vitalik commented 1 year ago

Hi @liminspace

can you bring some pseudocode vision how would you integrate django-forms into APIs / Routes/ Vlidators ?

liminspace commented 1 year ago
class ProductSchema(ModelSchema):
    class Config:
        model = Product
        model_fields = ['name', 'created_at']

class ProductForm(ModelForm):
    class Meta:
        model = Product
        model_fields = ['name', 'created_at']

class CreateProductFormSchema(FormSchema):
    class Config:
        schema = ProductSchema
        form = ProductForm

@api.post("/product")
def create_product(request, payload: CreateProductFormSchema):
    # payload is validated by ProductSchema in a regular way
    print(payload.data)  # access to the data object
    form = payload.get_form(user=request.user)  # create a form instance with passing extra init args
    if not form.is_valid():
        raise payload.create_form_error(form.errors)  # generate an error response from form errors
    product = form.save()
    return {"id": product.pk}

It's just my first thought, I think I will have better view after more xp of using django-ninja. Usually the logic of full validation and saving objects are implemented in Django Forms. In this case we can reuse that code. It make sense if we have something that works in django admin and other parts of the project and just want to add that to API. Note, that we have Form/ModelForm and Schema/ModelSchema, so it should support any combination of them. In some general cases it can be implemented as very generic code without defining a lot of classes and manual calling a lot of methods. @vitalik

synw commented 1 year ago

It would be very nice to have a

single format of validation error and single interface to get validated data

There is a debate in my team about this: what should the schema be responsible for vs what Django forms should be? For now we are using the schemas only to validate data types, giving a 418 error to the developer when the schema does not validate, error that is not supposed to happen at runtime, and a standard 422 error on Django form validation error for business logic level errors

l-kotzur commented 1 year ago

I had a similar issue of migrating and old form with many custom validators. I solved it by writing custom pydantic validators in my ModelSchema. Such, the whole validation was integrated into the single pydantic object and there was no need to switch from the pydantic model to django forms.

Therefore, I just replaced the clean_custom_field_functions with:

from pydantic import validator

class LeadSchema(ModelSchema):
    class Config:
        model = Lead
        model_fields = ["custom_bool_field",]

    @validator('custom_bool_field')
    def my_validation_function(cls, v: bool):
            if not v:
                raise ValueError(_("Please confirm our custom bool."))
            return v

Unfortunately, the pydantic validator did not find the fields and threw me an error. Instead, I created a schema with the create_schema function and derived a subclass:

from pydantic import validator
from ninja.orm.factory import create_schema

LeadSchema = create_schema(
        Lead, 
        name="LeadSchema",
        fields=  ["custom_bool_field",]
)

class LeadValidationSchema(LeadSchema):

    @validator('custom_bool_field')
    def my_validation_function(cls, v: bool):
            if not v:
                raise ValueError(_("Please confirm our custom bool."))
            return v

This works great.

Nevertheless, the step in between with the subclass feels a bit unnecessary...

@vitalik Can I help somehow to beautify this?