請求(四)Request Body 與 Schema 介紹
文章目錄
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 12 篇。
經過前幾篇的介紹,我們已經學習了如何處理路徑與查詢參數。但在現實世界中,我們往往還需要處理更複雜的請求資料。
比如用戶提交的表單、上傳的檔案等等。對於 API 而言,最常見的就是 JSON 格式的 request body。
這一篇將探討 Django Ninja 如何處理 request body,並介紹如何透過 Schema 來定義與驗證資料。
本文所有的程式碼變動,可參考這個 PR。
快速導覽
👉 完整系列目錄:點此查看
👉 程式碼範例:GitHub 範例專案
一、什麼是 Request Body?
Request body 指的是隨著 HTTP 請求一同傳送的資料,通常用於POST
、PUT
等需要建立或更新「資源」的請求。
這些資料不會出現在 URL 中,而是以 JSON 或其他格式(如 XML、form-data)作為請求的主體。
例如,當用戶要發表一篇新文章時,可能會傳送以下 JSON 格式的 request body:
1 | { |
這個 request body 包含了title
和content
兩個欄位,Django Ninja 將協助我們處理這些資料並進行驗證。
二、範例專案改動
我們要在範例專案中建立一個接收 request body 的 API——「新增文章」。
此外,還要在 Django post app 目錄下,新增一個 Python 模組:schemas.py
。這是用來放置 API 中所有用到的 Schema 的地方。
1 | ├── NinjaForum |
具體程式碼,我們會在接下來的說明中介紹。
從本篇開始,分支名稱不再使用中文,因為中文分支名稱會一直被 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 有自己加一點料):
1 | # Django Ninja 原始碼 |
回到專案,讓我們來看專案中的例子,這是定義「新增文章」API 的 request body 的 Schema:
1 | # post/schemas.py |
這個 Schema 要求 body 資料必須包含這三個欄位:title
、content
和user_id
,而且資料的型別也要相符。
在 View 函式中使用 Schema
定義好了「請求」Schema,就可以在 view 函式中以「函式參數」的形式使用它:
1 | from post.schemas import CreatePostRequest |
我們將函式payload
參數的 type hint 設定為我們剛剛定義的CreatePostRequest
。
當請求發送到這個 API 時,Django Ninja 會透過CreatePostRequest
這個 Schema 來解析(parsing)並驗證 body 中的資料。
驗證成功後,再將資料傳入 view 函式的payload
。此時函式內部的payload
參數,本質上是一個 Schema(即 Pydantic BaseModel)物件。
自動資料驗證與錯誤處理
如果請求 body 中有欄位缺少,或者資料的型別不對,Django Ninja 會自動返回 422 回應,並提供具體的錯誤資訊:
1 | { |
錯誤訊息表示:body 中缺少了content
這個 field。
四、可選的(Optional)欄位與預設值
在實際 API 開發中,並不是所有請求欄位都是必須的。
我們可以透過 Pydantic 與 type hints 來定義可選欄位。假設,現在文章的內容是完全可選的:(留意content
欄位)
1 | class CreatePostRequest(Schema): |
使用=
運算子,將content
欄位的預設值設定為None
,該欄位就會變成可選欄位。此時content
的 type hints 也要改為str | None
。
值得一提的是,如果 Schema 用在請求,這樣設定即使能通過驗證,你也要注意後續對應的 Django Model 欄位(也就是 db 欄位)是否允許 NULL。不然還是會出錯:
django.db.utils.IntegrityError: NOT NULL constraint failed: post_post.content
除了將欄位設為可選,也可以直接給定預設值,比如這裡的空字串。在使用者未輸入時,就會直接填入預設值:
1 | class CreatePostRequest(Schema): |
然而,除了預設值為None
,在 Schema 中給定預設值的行為要「非常慎用」。這部分我們在〈卷 18:Pydantic Field 設定範例與預設值〉還會再次討論。
五、Django Ninja 判斷參數的順序
你是否想過,一個 view 函式參數這麼多種,Django Ninja 怎麼知道誰要對應誰?
事實上,Django Ninja 確實會根據 view 函式的參數簽名,自動判斷參數的來源(究竟是路徑參數、查詢參數或請求 body)。其判斷順序如下:
- 路徑參數:任何定義在 URL path 中的變數(比如
/items/{id}
中的id
)會優先被識別為路徑參數。 - 查詢參數:函式中的其他單數類型參數(比如
int
、float
、bool
、str
,而不是list
、dict
),若未標註為路徑參數,則會被識別為查詢參數。 - Request body:Schema 型別參數,才會被視為請求 body。
原則上,view 函式只能有一個 Schema 參數。畢竟一個請求就只有一個 body 而已。
第二節尾聲
本節的內容已差不多結束。
在這一節中,我們學習了如何使用 Django Ninja 處理 HTTP 請求,並介紹了 Schema 的基本用法。
Schema 的用法與變化還很多,這裡只是「牛刀小試」而已。在第三節「HTTP 回應」中,你將看到更多關於 Schema 的設定。
進入下一節之前,我們先進行中場休息——和一些準備。
中場休息與準備
下一節,我們要讓專案的 API 真正運作起來,還記得前面提到為何目前無法使用嗎?
- 沒有 db 資料。
- 沒有建立 Schema。
我們已經學到怎麼使用 Schema 了——雖然還不全面。那「db 資料」問題也需要獲得解決。
Django Fixtures
我們固然可以透過呼叫 POST API 去手動新增用戶與文章資料,但太麻煩了!更別說,專案目前還沒有「新增使用者」這個 API。
所以,不用麻煩了。
我們直接透過 Django fixtures 來匯入由我預先定義好的假資料。
有關 Django fixtures 的介紹,可參考文章〈用 Django Fixture 匯入與導出資料〉。
在下一篇13-response
分支進度底下,你已經可以看到我導出的 fixtures 資料:
users.json
。posts.json
。
想要使用它們,直接依序匯入即可:
1 | python manage.py loaddata users.json |
一定要先匯入 users,否則文章沒有作者會關聯失敗。
匯入完成後,你會獲得 2 個使用者——Alice 和 Bob,還有他們各發表的 30 篇文章。
呃,夾雜了第一篇我的測試文章,請多包涵😅
成功匯入後,我們就可以繼續了。