2024 iThome 鐵人賽2024 iThome 鐵人賽

上一篇文章中,我們探討了 Django Ninja 影響 API 文件呈現的一些重要設定。它們是自動化 API 文件的基本功,不容忽視。

但這樣還不夠!我們想要讓這份文件更加生動,讀起來清晰易懂。

其中的關鍵在於 API 文件上的資料範例。好的範例讓人一讀就懂,能有效縮短理解和思考的時間。

本文將介紹如何運用 Pydantic 的Field設定,全方位提升 API 文件的清晰與可讀性。我們會探討如何為自動生成的文件加上栩栩如生的範例,讓文件更貼近真實

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


Pydantic 在 Django Ninja 中的角色

Pydantic 是一個實現資料驗證、序列化的套件,廣泛應用於 FastAPI 和 Django Ninja 等框架。

在 Django Ninja 中,Pydantic 被用來定義 Schema,這些 Schema 決定了 API 如何處理 HTTP 請求和回應中的資料,並自動轉換成符合 OpenAPI 標準的文件

Pydantic 的強大之處在於,它不僅能驗證資料,還可以通過Field設定,為文件欄位提供額外的說明、範例和預設值

這些細節設定會自動反映在轉換後的 API 文件中,幫助開發者更好地理解 API 的行為與內涵。

Pydantic Field

Pydantic 的Field是一個強大的工具,可以用來為每個資料欄位提供更多細節資訊,如標題、描述、範例和預設值等。

這些設定不僅有助於數據驗證,也大大提升了 API 文件的可讀性。以下是一些常見的Field參數:

  • title:為欄位設定標題,幫助開發者快速理解該欄位的作用。
  • description:提供欄位描述,讓人更清楚地了解這個欄位的用途與限制。
  • examples:設定範例值,幫助開發者直觀理解 API 的輸入、輸出格式。
  • default第一位置參數,提供欄位的預設值。Input 未提供該欄位值時,將自動使用預設值

善用這些參數,可以產生高品質的 API 文件。

程式與文件的平衡點

不過!我們還是要稍稍向「現實」靠攏,如果每一個 API 都要你寫這麼多內容,可能會讓開發者感到負擔過重

而且,使用大量參數,文件確實變好看了,但產生文件的程式碼不免會落落長

我們要找到一個平衡點,既能提供足夠資訊,又不會讓程式變得過於冗長。

從這個角度考慮,我覺得其中最重要的兩個參數,是defaultexamples——尤其是後者

所以本文會專注介紹這兩者,這樣不僅學習上更聚焦,也符合我的開發日常。


官方文件與原始碼

如果你想多了解 Pydantic Field 的參數與用法,那就要看 Pydantic 的官方文件——而不是 Django Ninja。

Django Ninja 的文件中,並沒有專門的章節介紹Field的使用。這是因為Field實際上是 Pydantic 的功能,而不是 Django Ninja 特有的。

不過如果你真的去看這份文件,可能會發現,它對Field全部參數的解說,也不算是非常詳盡。

想要知道所有可用的參數,我覺得看原始碼是最快的。然後從函式簽名(對,Field是一個函式)的 type hints 去揣摩它的用法,也不失為一個好的方式。


以下,我們開始講述如何使用 Pydantic Field 的examplesdefault參數,讓 API 文件更加生動且嚴謹。

為 API 文件加入「範例」

上一篇我們提到目前 API 文件的不足,其中「缺乏真實範例」這個問題還未解決。

以下是「取得單一文章資訊」在文件中的回應範例:

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 0,
"title": "string",
"content": "string",
"author": {
"id": 0,
"username": "string",
"email": "string"
},
"created_at": "2024-09-22T08:58:55.960Z",
"updated_at": "2024-09-22T08:58:55.960Z"
}

無論0"string",都稱不上是好的文件範例——它們都不夠真實

現在,我們要為回應的 Schema 加上範例,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class _AuthorInfo(Schema):
id: int = Field(examples=[1])
username: str = Field(examples=['Alice'])
email: str = Field(examples=['alice@exapmple.com'])

class PostResponse(Schema):
id: int = Field(examples=[1])
title: str = Field(examples=['Ninja is awesome!'])
content: str = Field(examples=['This is my first post.'])
author: _AuthorInfo
created_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
updated_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
...

這裡有兩個重點

重點一:examples 參數

我只有使用examples參數,這樣最簡單,而且範例確實是文件中相當重要的一環

此外,這個examples其實大有文章,如果你寫成example,比如:

1
2
class PostResponse(Schema):
id: int = Field(example=1)

實際上也能正常運作,但 Mypy 卻會提醒你:

Unexpected keyword argument “example” for “Field”; did you mean “examples”?

沒錯,因為現在的 Pydantic v2,Field只有examples這個參數。example應該是 Pydantic v1 的做法,而 Django Ninja 還保持了對二者的相容

考慮到未來,建議還是使用examples,不僅可以避免 Mypy 的警告,而且與最新版本的 Pydantic 保持一致。

重點二:巢狀 Schema 的範例

巢狀 Schema 的範例,只要在底層 Schema 加上Field即可。引用層不必聲明:

1
2
3
4
5
6
7
8
9
class _AuthorInfo(Schema):  # 這是巢狀底層,要寫 Field
id: int = Field(examples=[1])
username: str = Field(examples=['Alice'])
email: str = Field(examples=['alice@exapmple.com'])

class PostResponse(Schema):
...
author: _AuthorInfo # 無須再寫 Field
...

實際效果

看一下實際的 API 文件回應,我直接截取頁面中的 JSON 值:

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 1,
"title": "Ninja is awesome!",
"content": "This is my first post.",
"author": {
"id": 1,
"username": "Alice",
"email": "alice@exapmple.com"
},
"created_at": "2021-01-01T00:00:00Z",
"updated_at": "2021-01-01T00:00:00Z"
}

相比於之前的0"string",是不是更生動、好讀了呢?


default 參數的正確使用時機

在我看來,大部分的時候,我們並不需要定義預設值。

我建議你也不要這樣寫:

1
2
3
class PostResponse(Schema):
id: int = 1
...

雖然文件上一樣也會顯示範例值為 1,但其實這個寫法與下面這個寫法等價

1
2
3
class PostResponse(Schema):
id: int = Field(default=1)
...

這實際上是在定義預設值。如前所述,如果 Schema 用在 HTTP 請求,且客戶端未提供該欄位的值時,Django Ninja 將自動使用預設值。

這很可能會造成出乎意料的結果。

正確的流程是:前端沒有提供值的時候,Django Ninja 應該要給出 422 回應。

所以你根本不需要(也不應該)定義預設值——除了下列情況

在可選欄位使用預設值 None

我個人推薦,只在請求欄位為「可選(optional)」時,使用default參數。

而且此時的預設值應為None

為了示範,我們建立一個新 API——「新增使用者」(也就是用戶註冊)。這個 API 在後續教學中,還會被反覆提及與改進。

還記得我們的User模型中,bio欄位是可選的嗎?

1
2
3
4
class User(AbstractUser):
email = models.EmailField(unique=True) # 強制唯一的 email
bio = models.TextField(null=True) # 個人簡介欄位(可選)
...

所以,我們的 API 請求 Schema 如下——直接看bio欄位設定:

1
2
3
4
5
6
class CreateUserRequest(Schema):
...
bio: str | None = Field(
default=None,
examples=['Hello, I am Alice.']
)

Field 中的default=None設定讓你在客戶端沒有填入值時,API 也不會出錯。

另外留意bio: str | None這個 type hint,千萬不要少了None,會影響文件的渲染結果:(這是有None的結果)

None,API 文件才會顯示欄位值為可選(string | null)。


小結與下一步

經過本章的學習與改進,我們的 API 文件已經達到 80 分水準!在大多數開發專案中,這樣的文件品質可以說相當出色了。

接下來我們要進入第五章——資料驗證錯誤處理

這個章節將涵蓋如何在 Django Ninja 中實現有效的資料驗證,以及如何優雅地處理和回應各種可能的錯誤情況。

通過這些技巧,我們將能夠建立更穩健、可靠的 API。