vitalik / django-ninja

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

Pass serialization context (similar to validation context) with request to `model_dump` #1233

Closed scorpp closed 2 months ago

scorpp commented 2 months ago

Is your feature request related to a problem? Please describe. We're trying to post-process response entities to erase data from responses based on user permissions.

We're using custom framework to do this (somewhat similar to ninja permissions, but applied at field level). Based on model class and user permissions it generates list of fields to exclude to use with model_dump.

excludes = get_excludes_by_permissions(api_response_schema, request.user)
json_response = api_response_schema.model_dump_json(exclude=excludes)

Describe the solution you'd like Pydantic starting from 2.7 added serialization context support (https://github.com/pydantic/pydantic/pull/8965)

Ninja already passes similar context to validation. Operation._result_to_response https://github.com/vitalik/django-ninja/blob/master/ninja/operation.py#L258-L269

It would be useful to have request in serialization context as well.

vitalik commented 2 months ago

Hi @scorpp

Can you do some pseudocode on how you think it should look/work ?

I think currently you can achive it like this:


class MyResponse(Schema):
   public_field: str
   secret_field: Optional[str] = None

   @staticmethod
   def resolve_secret_field(obj, context):
       request = context['request']
       if not request.user.is_superuser:
             return None
       return obj.secret_field

@api.get('/some', response=MyResponse)
def some(request, ...
scorpp commented 2 months ago

I'm seeing like this

class ResponseSchema(Schema):
    field_available_for_everyone: str
    field_under_permission: str

    # this is part of custom framework
    class Permissions:
        field_under_permission = UserPermission('can_view_secret_field')

    @model_serializer(mode="wrap")
    def serialize(self, handler, info):
        serialized = handler(self)

        if info.context is not None and info.context.request is not None:
            # ---> This is where i need request
            excludes = get_excludes_by_permissions(self, info.context.request.user)
            for e in excludes:
               serialized.pop(e)

        return serialized

@api.get('/some', response=ResponseSchema)
def some(request, ...) -> QuerySet:
    ...

the part with alternating b/w custom serilization and default one is not 100% clear yet.

scorpp commented 2 months ago

I have finally managed to put it to work, but in quite an ugly way, by subclassing Operation, PathView, Router, APIController, api_controller decorator and NinjaExtraAPI for only add 1 line of code to Operation :-D

I've also updated my example above ^^ to demonstrate the usage scenario better.