一. 前言:JSON 很普通,直到它變成瓶頸 #
JSON 大概是現代 Python 專案裡最常見的資料格式。
API 回應是 JSON。
前端送進來的是 JSON。
快取檔可能是 JSON。
LLM 工具呼叫、設定輸出、事件紀錄,也常常繞回 JSON。
平常資料量小的時候,標準庫 json 幾乎什麼都能做。
拍拍君也不會叫你每個專案一開始就換工具。
可是當你開始遇到下面這些狀況,就可以認真看一下 orjson:
- API endpoint 每秒要吐很多筆資料。
- 背景工作一直把 Python dict 寫成 JSON 檔。
- 快取層裡塞了大量 list、datetime、dataclass。
- profiling 發現序列化時間比真正計算還久。
- 想要更嚴格一點,不想讓奇怪型別偷偷混進輸出。
orjson是用 Rust 寫的高速 JSON 函式庫。 它的目標不是提供最多客製化花招,而是把常見 JSON 序列化做得很快、很準、很明確。 今天這篇不是「跑分大賽」,拍拍君比較在意的是:怎麼安全地把orjson放進 API、快取、資料管線。 如果你之前看過 Pydantic,可以這樣分工: - Pydantic:負責資料驗證、轉型、商業規則邊界。
- orjson:負責把已經整理好的資料快速變成 JSON bytes。 今天我們會從最小範例開始,一路走到 datetime、dataclass、default 轉換與常見陷阱。
二. 安裝:一個套件就好 #
先建立一個乾淨專案:
mkdir orjson-demo
cd orjson-demo
uv init
uv add orjson
不用 uv 的話,也可以用一般虛擬環境:
python -m venv .venv
source .venv/bin/activate
pip install orjson
確認能 import:
import orjson
print(orjson.__version__)
這篇會額外示範 dataclass、datetime 和 optional 的 numpy。 如果你想跟著跑 numpy 範例,可以再裝:
uv add numpy
或:
pip install numpy
先提醒一件事:orjson 的 API 故意比標準庫小。
它主要就兩個核心函式:
orjson.dumps():Python object → JSON bytesorjson.loads():JSON bytes/string → Python object 你不需要先學一大堆設定。 真正要搞懂的是它跟標準庫json的幾個差異。
三. 第一個 dumps:注意,它回傳 bytes #
先看標準庫寫法:
import json
payload = {
"name": "拍拍君",
"score": 98,
"tags": ["python", "json", "api"],
}
text = json.dumps(payload, ensure_ascii=False)
print(type(text))
print(text)
輸出是 str。
再看 orjson:
import orjson
payload = {
"name": "拍拍君",
"score": 98,
"tags": ["python", "json", "api"],
}
raw = orjson.dumps(payload)
print(type(raw))
print(raw)
這次輸出是 bytes。
這個差異非常重要。
很多 API framework、檔案寫入、Redis client、message queue,本來就偏好 bytes。
所以 orjson 直接回傳 bytes,通常反而少一次 encode。
如果你真的需要字串,可以 decode,但先問自己後面是不是真的需要 str:
text = orjson.dumps(payload).decode("utf-8")
print(text)
如果下一步是寫檔:
from pathlib import Path
Path("payload.json").write_bytes(orjson.dumps(payload))
如果下一步是 HTTP response:
body = orjson.dumps(payload)
headers = {"content-type": "application/json; charset=utf-8"}
這些場景裡,bytes 是剛剛好。 不要急著 decode 再 encode 回去,那通常只是多做一次工。
四. loads:解析 JSON 時保持邊界乾淨 #
orjson.loads() 可以吃 bytes,也可以吃 str:
import orjson
raw = b'{"name":"Pypy","score":100,"active":true}'
data = orjson.loads(raw)
print(data)
print(type(data))
結果會是普通 Python dict:
{"name": "Pypy", "score": 100, "active": True}
這裡要注意一個架構上的分工。
loads() 只負責把合法 JSON 解析成 Python object,不負責確認 score 是不是合理分數。
這種事情應該交給驗證層。
例如你可以用簡單函式:
def parse_score_event(raw: bytes) -> dict:
data = orjson.loads(raw)
if not isinstance(data, dict):
raise ValueError("event must be an object")
if not isinstance(data.get("name"), str):
raise ValueError("name must be a string")
if not isinstance(data.get("score"), int):
raise ValueError("score must be an integer")
return data
重點是:JSON parser 不等於資料驗證器。
orjson 解析很快,但不要把所有責任丟給它。
邊界清楚,程式才不會長成一團。
五. datetime:API 最容易踩到的小坑 #
標準庫 json 不能直接處理 datetime:
from datetime import datetime, timezone
import json
event = {
"name": "deploy",
"created_at": datetime.now(timezone.utc),
}
json.dumps(event)
通常會得到:
TypeError: Object of type datetime is not JSON serializable
orjson 對 datetime 比較友善:
from datetime import datetime, timezone
import orjson
event = {
"name": "deploy",
"created_at": datetime(2026, 5, 14, 10, 30, tzinfo=timezone.utc),
}
print(orjson.dumps(event).decode("utf-8"))
會輸出類似:
{"name":"deploy","created_at":"2026-05-14T10:30:00+00:00"}
這對 API 很方便。 但 datetime 的重點不是「能不能轉」,而是「時區有沒有講清楚」。 拍拍君建議:
- 內部儲存盡量使用 timezone-aware datetime。
- API 輸出用 ISO 8601。
- 不要混用本地時間與 UTC 卻不標示。
如果你想把 UTC 輸出成
Z結尾,可以用:
body = orjson.dumps(
event,
option=orjson.OPT_UTC_Z,
)
print(body.decode("utf-8"))
輸出會像:
{"name":"deploy","created_at":"2026-05-14T10:30:00Z"}
這種格式很多前端和 API client 都讀得很順。 如果你遇到 naive datetime,也就是沒有 timezone 的 datetime,要先決定政策。 不要默默假裝它是 UTC。 明確一點比較不會害到未來的自己。
from datetime import timezone
def utc_now_payload() -> bytes:
return orjson.dumps({
"created_at": datetime.now(timezone.utc),
"source": "worker",
})
時間資料最怕曖昧。 快不是藉口,清楚才是本體。
六. dataclass:把結構化資料直接輸出 #
很多小型資料流程會用 dataclass 表示事件或設定:
from dataclasses import dataclass
@dataclass
class JobResult:
job_id: str
ok: bool
duration_ms: int
用 orjson 可以直接序列化:
result = JobResult(
job_id="job-001",
ok=True,
duration_ms=42,
)
print(orjson.dumps(result).decode("utf-8"))
輸出:
{"job_id":"job-001","ok":true,"duration_ms":42}
這對內部事件紀錄很舒服。
你不一定要先手動 asdict()。
不過要小心:能輸出不代表模型設計就完整。
dataclass 沒有自動驗證。
下面這種資料也可以被建立:
bad = JobResult(
job_id="job-002",
ok=True,
duration_ms=-999,
)
如果 duration_ms 不該是負數,你要自己檢查。
或是把資料入口交給更完整的驗證層。
orjson 在這裡扮演的是輸出引擎,不是資料警察。
一個常見封裝方式是讓 dataclass 負責結構,函式負責政策:
def serialize_job_result(result: JobResult) -> bytes:
if result.duration_ms < 0:
raise ValueError("duration_ms must be non-negative")
return orjson.dumps(result)
這樣呼叫端就不用到處記得規則。 日後要換格式,也只改一個地方。
七. default:處理自訂型別時要節制 #
如果資料裡有 orjson 不認識的型別,會丟錯:
from decimal import Decimal
payload = {
"price": Decimal("12.50"),
}
orjson.dumps(payload)
你可以用 default 指定轉換方式:
from decimal import Decimal
import orjson
def default(obj):
if isinstance(obj, Decimal):
return str(obj)
raise TypeError
payload = {"price": Decimal("12.50")}
body = orjson.dumps(payload, default=default)
print(body.decode("utf-8"))
輸出:
{"price":"12.50"}
為什麼不用 float?
因為金額轉 float 可能引入精度問題。
用字串比較保守。
default 很方便,但不要把它寫成萬能垃圾桶。
拍拍君看過最可怕的版本長這樣:
def default(obj):
return str(obj)
這會讓很多 bug 被漂亮地藏進 JSON 裡。 本來應該爆掉的物件,變成一段看似正常的字串。 然後你三天後才發現資料全歪。 比較好的做法是:
- 明確列出支援的型別。
- 不認識就
raise TypeError。 - 在測試裡覆蓋這些轉換規則。 範例:
from decimal import Decimal
from pathlib import Path
def strict_default(obj):
if isinstance(obj, Decimal):
return str(obj)
if isinstance(obj, Path):
return str(obj)
raise TypeError
嚴格一點不是龜毛。 資料邊界就是該龜毛。
八. 常見陷阱:快之外,還要可維護 #
第一個陷阱:到處 decode。
text = orjson.dumps(data).decode("utf-8")
response.write(text.encode("utf-8"))
如果 response 最後需要 bytes,中間 decode 就是多餘的。
第二個陷阱:把 default 寫太寬。
def default(obj):
return str(obj)
這會讓錯誤資料假裝正常。
第三個陷阱:把驗證交給 JSON parser。
Parser 只知道 JSON 合不合法。
它不知道你的業務規則。
第四個陷阱:API 輸出格式不一致。
某些 endpoint datetime 有 Z,某些沒有。
某些 Decimal 是字串,某些是 float。
這不是 orjson 的問題。
這是序列化政策沒有集中管理的問題。
拍拍君的建議是:建立一個小模組,例如 app/json.py。
裡面放 dumps() wrapper、loads() wrapper、default policy 和測試。
然後全專案統一從這裡用。
九. 什麼時候不需要 orjson? #
不是每個專案都需要 orjson。
如果你只是偶爾讀寫一個小設定檔,標準庫 json 很夠用。
如果你的瓶頸在資料庫、網路或模型推論,換 JSON 函式庫也不會有明顯差異。
拍拍君會在這些情況選 orjson:
- API 回應量大,而且 profiling 指向序列化。
- 背景工作大量寫 JSON 或 NDJSON。
- 需要穩定處理 datetime、dataclass、自訂型別。
- 想把 JSON 輸出統一成 bytes-first workflow。 會留在標準庫的情況:
- 小型 script。
- 一次性資料轉換。
- 教學範例要零依賴。
- 團隊部署環境非常保守。
工具不是信仰。
標準庫很穩,
orjson很快。 你要的是符合當前問題的選擇。
結語:快很好,邊界清楚更好 #
orjson 最吸引人的地方當然是快。
但真正讓它好用的,是它的邊界很清楚。
它輸出 bytes。
它嚴格處理不認識的型別。
它對 datetime、dataclass、numpy 這些常見資料工程場景很友善。
只要你把序列化政策集中起來,它就能變成很穩的一層。
今天的重點整理:
orjson.dumps()回傳 bytes,不要無腦 decode。loads()只解析 JSON,不負責商業驗證。- datetime 要明確處理 timezone。
default要嚴格,不要全部轉字串。- JSON 適合交換與事件,不適合所有大型資料。
- API 專案最好包一層自己的 JSON helper。
下一次你發現 API 明明邏輯很簡單,回應卻慢吞吞,可以把 profiler 打開看看。
如果瓶頸真的在 JSON,
orjson會是一把很俐落的小刀。 不要亂揮,但用對地方會很爽。