vitalik / django-ninja

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

How to create an alias for a field created via annotation #1272

Open tobi-or-not opened 2 months ago

tobi-or-not commented 2 months ago

I have an ORM query that creates annotations. While hour makes sense within the context of the query, as a property in the API response, I'd like to call the field time. In the WaterLevelSchema, I can specify hour: datetime, which works but if I only specify time: datetime = Field(..., alias='hour') I get an error:

  Field required [type=missing, input_value=<DjangoGetter: WaterLevel...o.ZoneInfo(key='UTC')))>, input_type=DjangoGetter]
    For further information visit https://errors.pydantic.dev/2.8/v/missing

How can I create an alias for a field that was created via annotation in a Django ORM query? When I specify both hour and time in the WaterLevelSchema, there is no error, but then I have both properties in the API response, which I do not want. Do I have my thinking backwards? How can this be done?

class WaterLevelSchema(Schema):
    hour: datetime  # --> works
    time: datetime = Field(..., alias='hour')   # --> raises an error unless hour is specified as above

class DashboardSchema(Schema):
    ...
    historic_water_levels_m3: list[WaterLevelSchema]

@router.get('/dashboard/{location_id}', response=DashboardSchema)
def dashboard(request, location_id):
   latest_water_levels = (
      ExecutionLog.objects
      .filter(forecast_id__in=latest_forecasts_ids)
      .annotate(day=TruncDay('created_at'), hour=TruncHour('created_at'))  # Truncate timestamp to day and hour
      .annotate(avg_water_level_m3=Avg('water_level_m3'))  # Calculate average water level per hour per day
      .values('day', 'hour')  # Group by day and hour
   )

   return DashboardSchema(
            ...
            historic_water_levels_m3=latest_water_levels
   )
lapinvert commented 2 months ago

It would seem to me to be pretty normal but I may be mistaken.

You're sending hour attribute to your WaterLevelSchema when it's expecting time.

The syntax for alias is good, but in this case you don't want an alias, you just want a different schema.

So you remove hour from WaterLevelSchema, you define time as time: datetime (not an alias), and you annotate your object to output time instead of hour.

Btw I don't think you need to return DashboardSchema, as it's defined in response=DashboardSchema, that's the whole point, you can return the object as-is, as long as it's in the format you defined.

class WaterLevelSchema(Schema):
    time: datetime

# (...)

@router.get('/dashboard/{location_id}', response=DashboardSchema)
def dashboard(request, location_id):
   latest_water_levels = (
      # (...)
      .annotate(day=TruncDay('created_at'), time=TruncHour('created_at'))  # Truncate timestamp to day and hour
      # (...)
   )

   return {
        # (...)
        historic_water_levels_m3=latest_water_levels
   }
tobi-or-not commented 2 months ago

Thanks @lapinvert!

Upon review, the name change works. I am still confused, though, why I hour is accessible as a 'regular' schema field but not via an alias.

The reason I am returning the DashboardSchema is that the data for it comes from a number of different queries. I guess I could

Thanks for your thoughts!