django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

Pydantic context in response

Open kingychiu opened this issue 1 year ago • 4 comments

I am trying to use Pydantic context to add different fields to the response dynamically. For example:

Based on the include_admin_part flag, this schema will include the admin_part


class _JobResponseAdminPartSchema(ModelSchema):
    """
    The optional admin part of the response schema for job api
    """

    class Meta:
        model = Job
        fields = [...]

class JobResponseSchema(ModelSchema):
    admin_part: Optional[_JobResponseAdminPartSchema] = Field(default=None)
    
    @staticmethod
    def resolve_admin_part(
        job: Job, context: dict[str, bool]
    ) -> Optional[_JobResponseAdminPartSchema]:
        if context.get("include_admin_part"):
            return _JobResponseAdminPartSchema.from_orm(job)
        return None

    class Meta:
        model = Job
        fields = [...]

The method above works when I call this:

JobResponseSchema.from_orm(job_obj, {"include_admin_part": True}) # Works!

However, when I added the response option in @router.get, the response data was passed into JobResponseSchema again without the include_admin_part context!:

@router.get(
    "/",
    response={
        200: JobResponseSchema,
        400: ErrorSchema,
        401: ErrorSchema,
        403: ErrorSchema,
    },
)

One solution is to remove response entirely, and then it works. However I need the response typing in Swagger for typescript generation.

Can I either

  1. Disable the auto response serializing of an API?, like this:
@router.get(
    "/",
    response={
        200: JobResponseSchema,
        400: ErrorSchema,
        401: ErrorSchema,
        403: ErrorSchema,
    },
    parse_response=False,
)
  1. Or, when I return from the API function, I can provide my context, like this.
return job, {"include_admin_part": True}

Thank you!

kingychiu avatar Dec 04 '23 18:12 kingychiu

@kingychiu there are no mechanics to pass context yet from view function...

maybe something like could work in future:

@api.get("/some", response=SchemaWithContext)
def some(request...)
    return WithContext({"some": "data"}, context={"some": "context"})

but as for now you can use request object to pass extra context to Schema:


class JobResponseSchema(ModelSchema):
    admin_part: Optional[_JobResponseAdminPartSchema] = Field(default=None)
    
    @staticmethod
    def resolve_admin_part(job, context):
           request = context["request"].   # <------- !
           if request.include_admin_part:
                 return {extra}
    
@api.get("/some", response=JobResponseSchema)
def some(request...)
    request.include_admin_part = True # <------- !
    return job

vitalik avatar Dec 04 '23 21:12 vitalik

Thanks @vitalik; yeah, I am looking forward to official support!

For now, I found another workaround that solved the issue.

# from django.http import JsonResponse
response_dict = JobResponseSchema.from_orm(job_obj, {"include_admin_part": True}).dict()
return JsonResponse(data=response_dict)

By doing this, I can trigger the schema in my view function + having response defined in my @router.get. It seems like JsonResponse will be ignored (Not parsing again) as a return type.

For your SchemaWithContext suggestion, do you think we can have inferred types in the resolve_admin_part? I think now the context of the resolver__ functions is set to Any type.

kingychiu avatar Dec 04 '23 23:12 kingychiu

@kingychiu

the WithContext can be just a special django-ninja wrapper that will just notify renderer to include passed context to a desired response

vitalik avatar Dec 05 '23 18:12 vitalik

I would just like to add that I'm currently looking for this exact functionality. So I'm supporting adding it and thanks for giving some pointers on how to work around it for the time being.

stvdrsch avatar Dec 14 '23 12:12 stvdrsch