技術筆記 / Python

🐍 Python 精簡教學:用 jsonpickle 儲存與還原多型物件!

在 Python 中,我們常需要把自訂物件轉成 JSON 儲存或傳輸。不過標準的 json 模組只能處理 dict、list、str、int 等基本型別,遇到類別(尤其是多型)時就 GG 😵。

這時候,jsonpickle 就是你的好幫手!


✅ jsonpickle 是什麼?

jsonpickle 是一個 Python 套件,可以將 任意 Python 物件(包含類別、巢狀、多型)序列化成 JSON,並支援還原。

它的效果很像 Java 的 @JsonTypeInfo(use = CLASS) 或 Python 的 pickle,但是 JSON 格式,更通用!


🚀 快速上手:一分鐘搞懂 jsonpickle

✨ 安裝

pip install jsonpickle

🐶 假設我們有以下類別結構:

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, bark=True):
        super().__init__(name)
        self.bark = bark

🔁 序列化 + 反序列化

import jsonpickle

dog = Dog("Buddy")

# 序列化成 JSON 字串
json_str = jsonpickle.encode(dog)
print(json_str)

# 反序列化回原本的 Dog 物件
obj = jsonpickle.decode(json_str)
print(type(obj), obj.__dict__)

📦 輸出 JSON 範例

{
  "py/object": "__main__.Dog",
  "name": "Buddy",
  "bark": true
}

這裡的 "py/object" 就是類似 Java 的 "@class",自動包含完整類型資訊,還原時會用 import 方式動態載入類別。


🧠 使用情境

使用場景 是否適合
儲存/還原動態型別資料 ✅ 非常適合
序列化巢狀資料或循環引用 ✅ 處理得很好
跨語言(送 JSON 給前端) ❌ 不推薦,格式太特殊
高安全性場景(接受用戶輸入) ⚠️ 謹慎使用,勿 decode 不可信 JSON

🛡️ 安全提醒!

因為 jsonpickle.decode() 會依照 py/object 動態建立類別實例,千萬不要用來反序列化不可信來源的 JSON!

如需與外部系統互動,建議使用 dict() + json 手動序列化或用 pydantic。


🏁 結語

你要的功能 用 jsonpickle ✅
序列化/反序列化自訂類別 ✅
保留類型資訊 ✅ 自動加 "py/object"
快速還原 Python 多型物件 ✅ 一行搞定

簡單一句話:jsonpickle 是 Python 版的 @JsonTypeInfo(use = CLASS),超適合處理多型、巢狀與類別封裝物件!

📝jsonpickle 與 @dataclass 交互行為

1. 核心概念

2. 關鍵行為測試

測試場景

我們定義一個包含以下特性的 Dataclass:

  1. 一般欄位:id, name

  2. default_factory:tags (預設為空 list)

  3. init=False:created_at (不透過 init 傳入,而是內部產生)

import jsonpickle
from dataclasses import dataclass, field
from datetime import datetime
import time

@dataclass
class Task:
    id: int
    name: str

    # 特性 A: default_factory (預設值工廠)
    tags: list[str] = field(default_factory=list)

    # 特性 B: init=False (非建構子參數,通常由 __post_init__ 產生)
    created_at: float = field(init=False)

    def __post_init__(self):
        self.created_at = time.time()

# --- 1. 建立物件 ---
task = Task(id=1, name="Data Cleanup")
task.tags.append("urgent")
#此時 task.created_at 已經由 __post_init__ 自動生成
print(f"原始物件: {task}")

# --- 2. 序列化 (Encode) ---
json_str = jsonpickle.encode(task, indent=2)
print(f"\nJSON (儲存狀態):\n{json_str}")

# --- 3. 反序列化 (Decode) ---
restored_task = jsonpickle.decode(json_str)
print(f"\n還原物件: {restored_task}")

觀察結果

JSON 輸出會包含所有欄位,包括 init=False 的欄位:

{
  "py/object": "__main__.Task",
  "id": 1,
  "name": "Data Cleanup",
  "tags": ["urgent"],
  "created_at": 1708320000.123  <-- 狀態被完整保存
}

3. 重要機制詳解 (The "Gotchas")

Q1: field(init=False) 的欄位會被儲存嗎?

Q2: 反序列化時,會重新執行 __init__ 或 __post_init__ 嗎?

Q3: default_factory 的行為?


4. 最佳實踐建議

  1. 適合場景:

    • 複雜設定檔:例如你的 ETL Flow 設定,包含巢狀結構。

    • 暫存/快取 (Caching):將物件狀態存入 Redis 或檔案,稍後讀取繼續執行。

    • 除錯 (Debugging):將出錯的物件 Dump 下來分析。

  2. 不適合場景:

    • 跨語言傳輸:因為 JSON 中包含 Python 特有的 py/object 標籤,Java 或 JS 讀取時會覺得怪怪的(雖然可以設 unpicklable=False 來拿掉標籤,但就無法自動還原回物件了)。
  3. 安全性提醒:

    • 永遠不要對 不可信來源 的 JSON 字串執行 jsonpickle.decode(),這與 pickle 模組有相同的安全風險(可能導致惡意程式碼執行)。

5. 總結圖表

Dataclass 特性 jsonpickle 行為 備註
一般欄位 ✅ 保存 & 還原 最基本功能
field(default=...) ✅ 保存 & 還原 如果值與預設值不同,存當下的值
field(default_factory=...) ✅ 保存 & 還原 List/Dict 內容會被完整保留
field(init=False) ✅ 保存 & 還原 關鍵:還原時不重新計算,保留最後狀態
ClassVar ❌ 忽略 屬於類別屬性,不屬於實例狀態

在 GitHub 看原文