跳轉到

Request Body 與 Schema#

2024 iThome 鐵人賽

這是 Django Ninja 系列教學的第 12 篇。

經過前幾篇的介紹,我們已經學習了如何處理路徑與查詢參數。但在現實世界中,我們往往還需要處理更複雜的請求資料

比如用戶提交的表單、上傳的檔案等等。對於 API 而言,最常見的就是 JSON 格式的 request body

這一篇將探討 Django Ninja 如何處理 request body,並介紹如何透過 Schema 來定義與驗證資料。

本文所有的程式碼變動,可參考這個 PR

範例專案#

👉 GitHub 範例專案


一、什麼是 Request Body?#

Request body 指的是隨著 HTTP 請求一同傳送的資料,通常用於 POSTPUT需要建立或更新「資源」的請求。

這些資料不會出現在 URL 中,而是以 JSON 或其他格式(如 XML、form-data)作為請求的主體。

例如,當用戶要發表一篇新文章時,可能會傳送以下 JSON 格式的 request body:

{
    "title": "我的第一篇文章",
    "content": "這是我在忍者論壇的第一篇文章,希望大家喜歡!"
}

這個 request body 包含了 titlecontent 兩個欄位,Django Ninja 將協助我們處理這些資料並進行驗證。


二、範例專案改動#

我們要在範例專案中建立一個接收 request body 的 API——「新增文章」。

此外,還要在 Django post app 目錄下,新增一個 Python 模組:schemas.py。這是用來放置 API 中所有用到的 Schema 的地方。

├── NinjaForum
   ├── ...
├── post
   ├── api.py
   ├── schemas.py # 新增這個模組
   ├── ...

具體程式碼,我們會在接下來的說明中介紹。

從本篇開始,分支名稱不再使用中文,因為中文分支名稱會一直被 GitHub 提醒:

The head ref may contain hidden characters: ...

而且應該也很少人使用中文來命名 git 分支!當初用中文是為了讀者比較好讀🥹

所以從這個分支開始,改成 數字+英文,比如本篇的「12-request-body」。但 PR 的標題仍維持中文。


三、使用 Schema 定義與驗證 Request Body#

與 FastAPI 相同,Django Ninja 使用 Pydantic BaseModel 來處理請求 body。

不過因為 BaseModel 這個名稱容易和 Django 的 Models 混淆,所以 Django Ninja 將其重新命名為 Schema。

Schema 繼承自 BaseModel,因此兩者的實際內涵非常接近(Django Ninja 有自己加一點料):

# Django Ninja 原始碼
class Schema(BaseModel, metaclass=ResolverMetaclass):
    ...

回到專案,讓我們來看專案中的例子,這是定義「新增文章」API 的 request body 的 Schema:

# post/schemas.py
from ninja import Schema

class CreatePostRequest(Schema):
    title: str
    content: str
    user_id: int

這個 Schema 要求 body 資料必須包含這三個欄位:titlecontentuser_id,而且資料的型別也要相符

在 View 函式中使用 Schema#

定義好了「請求」Schema,就可以在 view 函式中以「函式參數」的形式使用它:

from post.schemas import CreatePostRequest
...

@router.post(path='/posts/')
def create_post(..., payload: CreatePostRequest):  # 這裡
    ...

我們將函式 payload 參數的 type hint 設定為我們剛剛定義的 CreatePostRequest

當請求發送到這個 API 時,Django Ninja 會透過 CreatePostRequest 這個 Schema 來解析parsing)並驗證 body 中的資料。

驗證成功後,再將資料傳入 view 函式的 payload。此時函式內部payload 參數,本質上是一個 Schema(即 Pydantic BaseModel)物件。

自動資料驗證與錯誤處理#

如果請求 body 中有欄位缺少,或者資料的型別不對,Django Ninja 會自動返回 422 回應,並提供具體的錯誤資訊

{
    "detail": [
        {
            "type": "missing",
            "loc": [
                "body",
                "payload",
                "content"
            ],
            "msg": "Field required"
        }
    ]
}

錯誤訊息表示:body 中缺少了 content 這個 field。


四、可選的(Optional)欄位與預設值#

在實際 API 開發中,並不是所有請求欄位都是必須的。

我們可以透過 Pydantic 與 type hints 來定義可選欄位。假設,現在文章的內容是完全可選的:(留意 content 欄位)

class CreatePostRequest(Schema):
    title: str
    content: str | None = None
    user_id: int

使用 = 運算子,將 content 欄位的預設值設定為 None,該欄位就會變成可選欄位。此時 content 的 type hints 也要改為 str | None

值得一提的是,如果 Schema 用在請求,這樣設定即使能通過驗證,你也要注意後續對應的 Django Model 欄位(也就是 db 欄位)是否允許 NULL。不然還是會出錯:

django.db.utils.IntegrityError: NOT NULL constraint failed: post_post.content

除了將欄位設為可選,也可以直接給定預設值,比如這裡的空字串。在使用者未輸入時,就會直接填入預設值

class CreatePostRequest(Schema):
    title: str
    content: str = ''
    user_id: int

然而,除了預設值為 None,在 Schema 中給定預設值的行為要「非常慎用」。這部分我們在〈卷 18:Pydantic Field 設定範例與預設值〉還會再次討論。


五、Django Ninja 判斷參數的順序#

你是否想過,一個 view 函式參數這麼多種,Django Ninja 怎麼知道誰要對應誰

事實上,Django Ninja 確實會根據 view 函式的參數簽名,自動判斷參數的來源(究竟是路徑參數、查詢參數或請求 body)。其判斷順序如下:

  1. 路徑參數:任何定義在 URL path 中的變數(比如 /items/{id} 中的 id)會優先被識別為路徑參數
  2. 查詢參數:函式中的其他單數類型參數(比如 intfloatboolstr,而不是 listdict),若未標註為路徑參數,則會被識別為查詢參數。
  3. Request body:Schema 型別參數,才會被視為請求 body。

原則上,view 函式只能有一個 Schema 參數。畢竟一個請求就只有一個 body 而已。


第二節尾聲#

本節的內容已差不多結束。

在這一節中,我們學習了如何使用 Django Ninja 處理 HTTP 請求,並介紹了 Schema 的基本用法。

Schema 的用法與變化還很多,這裡只是「牛刀小試」而已。在第三節「HTTP 回應」中,你將看到更多關於 Schema 的設定。

進入下一節之前,我們先進行中場休息——和一些準備


中場休息與準備#

下一節,我們要讓專案的 API 真正運作起來,還記得前面提到為何目前無法使用嗎?

  1. 沒有 db 資料。
  2. 沒有建立 Schema。

我們已經學到怎麼使用 Schema 了——雖然還不全面。那「db 資料」問題也需要獲得解決。

Django Fixtures#

我們固然可以透過呼叫 POST API 去手動新增用戶與文章資料,但太麻煩了!更別說,專案目前還沒有「新增使用者」這個 API。

所以,不用麻煩了。

我們直接透過 Django fixtures 來匯入由我預先定義好的假資料

有關 Django fixtures 的介紹,可參考文章〈用 Django Fixture 匯入與導出資料〉。

在下一篇 13-response 分支進度底下,你已經可以看到我導出的 fixtures 資料:

  1. users.json
  2. posts.json

想要使用它們,直接依序匯入即可:

python manage.py loaddata users.json
python manage.py loaddata posts.json

一定要先匯入 users,否則文章沒有作者會關聯失敗

匯入完成後,你會獲得 2 個使用者——Alice 和 Bob,還有他們各發表的 30 篇文章。

呃,夾雜了第一篇我的測試文章,請多包涵😅

成功匯入後,我們就可以繼續了。