快轉到主要內容
  1. 教學文章/

Python orjson 實戰:高速 JSON 序列化與 API 資料處理

·7 分鐘· loading · loading · ·
Python Orjson Json Serialization Api Performance
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 59: 本文

featured

一. 前言: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 bytes
  • orjson.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 會是一把很俐落的小刀。 不要亂揮,但用對地方會很爽。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 59: 本文

相關文章

Python Profiling:cProfile + line_profiler 效能分析完全指南
·8 分鐘· loading · loading
Python Profiling CProfile Line_profiler Performance Optimization
FastAPI:Python 最潮的 Web API 框架
·5 分鐘· loading · loading
Python Fastapi Web Api Async
Python multiprocessing:突破 GIL 的平行運算完全指南
·9 分鐘· loading · loading
Python Multiprocessing Parallel Concurrency Performance
Python marimo 實戰:可重現的 Reactive Notebook 與資料小工具
·7 分鐘· loading · loading
Python Marimo Notebook Data-App Developer-Tools Reactive
Python APScheduler 實戰:讓程式定時執行背景工作
·9 分鐘· loading · loading
Python Apscheduler Scheduler Cron Automation Background-Jobs
Python DuckDB 實戰:用 SQL 快速分析 CSV 與 Parquet
·6 分鐘· loading · loading
Python Duckdb SQL Parquet Data-Analysis Developer-Tools