Django Ninja: How to dynamically return a subset of the model fields

I love Django Ninja! Nowadays, when I start a new project that requires APIs, I always choose Django Ninja over Django REST Framework (DRF). Yes, it’s a fairly new package at the time of writing, and yes the docs seem to suggest that it can only be used for really simple use-cases. But I am here to show you a common use case that is relatively complicated to implement in DRF, but a breeze to implement in Django Ninja.

The case I am talking about is fairly common: You have a bunch of fields on a large model. You want the frontend client to tell you which fields of the model it needs, so that you avoid sending the whole model. This is very common in dashboard applications where the frontend is maybe only able to show a handful of graphs or table columns and thus doesn’t need to have access to all the model fields. I call this use case “Dynamic Serialization”.

Let me illustrate this with an example: I have a model named Car. Imagine this is a big model with a lot of fields. I want my frontend application to tell me what fields it needs with a request like this:

GET https://example.com/api/cars?field=id?field=owner&field=registration_number

In that case my backend should respond with something like this:

[
  {
    "id": 1,
    "owner": "John Doe",
    "registration_number": "ABC123"
  },
  {
    "id": 2,
    "owner": "Jane Doe",
    "registration_number": "XYZ123" 
  }
]

Let’s see how we would implement this functionality in Django Ninja. Here is the implementation:

from typing import List
from ninja import NinjaAPI, ModelSchema, Query
from ninja.errors import HttpError
from ninja.orm import create_schema
from pydantic import parse_obj_as

from .models import Car


api = NinjaAPI()


class CarSchema(ModelSchema):
    class Config:
        model = Car


def validate_fields(return_fields):
    allowed_fields = [name for name, _ in CarSchema.__fields__.items()]
    for field in return_fields:
        if field not in allowed_fields:
            raise HttpError(400, f"'{field}' is not a valid field name!")


@api.get('/cars')
def cars(request, fields: List[str] = Query(...)):
    validate_fields(fields)
    OutSchema = create_schema(Car, fields=fields)
    cars = list(Car.objects.all())
    return parse_obj_as(List[OutSchema], cars)

Let me explain how this works:

First we define the schema for our model using the ModelSchema class. We set the Config.model property to point to our model Car. This will create a schema for our entire model. We’ll use this class to validate the “fields” we receive from the request. We need to make sure they are actually part of the model. We do this in the validate_fields function. If a field query parameter doesn’t exist among the fields of our CarSchema, we throw HTTP status 400 error.

Now let’s look at the view function cars. In addition to the request, it accepts the fields argument as a list of strings. That special Query(...) syntax tells Django Ninja to accept fields as a query parameter included in the URL.

The real magic happens in create_schema. This function takes the fields we give it, and uses it to dynamically create a schema out of a Django model. In our case, after validating the fields and making sure that they will exist on the model, we send the fields and the Car model to the create_schema function. The output is a new schema that only has the fields we have specified.

Now the only thing left is to serialize the objects using this new schema. We do this using the parse_obj_as function. This is a function from the pydantic package that is installed with the django-ninja package. The first argument List[OutSchema] sets how we want the data to look like. And the second argument is a list of the objects we want to send out. In our case all the car objects. Note that you cannot send in a Django ORM queryset object as the second argument. It has to be a list of objects.

Now the frontend application can use field query parameters in the GET requests and the view function will respond with a list of objects that only contain the requested fields.

Subscribe via RSS