JSON 型別:json
適用情境
當一格要放的不是一個值、而是一整份文件時,就用 json:訂單背後的原始 ERP 封包、想原樣保留的 webhook body、每列各自的功能開關、帶有自由格式 metadata 的品項清單。Cell 可以是任意 JSON 值——物件、陣列、字串、數字、布林或 null——原樣存在 record.data 裡。
json 是唯一刻意不算純量的儲存型別。這一個決定幾乎解釋了本頁所有規則:一份文件沒有全序、也沒有唯一的字串表示法,所以任何需要排序、當鍵值或做算術的地方,都會用各自的 fail-closed 錯誤拒絕 json 欄位,而不是自行猜測。
REST 與 AI agent 工具組的 JSON 詞彙並不相同。在 REST 上,json 欄位只有整格等值比對。Agent 工具組另外提供葉節點定址(json_path)、陣列長度條件、葉節點排序、葉節點分組、葉節點聚合,以及結構剖析器。不要假設兩者對等,詳見下方的 AI agent 介面。
建立 schema
以下是 columns.create 的 body。json 欄位只接受 name、type、required、description:
{
"name": "Payload",
"type": "json",
"required": false,
"description": "原始 ERP 訂單封包"
}default_value、max_length、options 一律禁止,附件上限欄位(max_count、max_file_bytes、allowed_mime_types)與所有計算欄設定欄位也一樣。json 欄位的預設值永遠是 null。
| 欄位 | 在 json 欄位上的結果 |
|---|---|
default_value | 422 json columns cannot have a default_value |
max_length | 422 json columns cannot set max_length |
options | 422 json columns cannot set options |
| 附件上限欄位 | 422 fields [...] are only valid on attachment columns |
| 計算欄設定 | 422 fields [...] are not valid on json columns |
其中兩個訊息在 dict 層的最後防線(內部呼叫與轉換過的 schema 也會經過)有第二種寫法:default_value 是 json columns cannot have a default value(三個字、沒有底線),max_length 是 Column '<name>': json columns cannot set max_length,而 options 會落到通用訊息 Column '<name>': options is only valid on select/multi_select columns。請以狀態碼與欄位名稱判斷,不要比對單一字串。
合法與不合法的值
直接送出值,不要先轉成字串:
{
"data": {
"Order number": "SO-1042",
"Payload": {
"tier": "gold",
"score": 87,
"items": [{ "sku": "A-1", "qty": 2 }, { "sku": "B-7", "qty": 1 }]
}
}
}所有寫入路徑——建立紀錄、更新紀錄、批次新增、批次更新、upsert、compose command 寫入——都跑同一個驗證器、同一組上限:
| 上限 | 限制 | 超過時的錯誤 |
|---|---|---|
| 序列化大小 | json.dumps(value, ensure_ascii=False) 的 UTF-8 長度 65536 bytes | Field '<name>' exceeds max size 65536 bytes |
| 巢狀深度 | 根之下 32 層容器 | Field '<name>' exceeds max JSON nesting depth 32 |
| 節點數 | 8192 個節點(含根節點) | Field '<name>' exceeds max 8192 JSON nodes |
| 數字 | 任何深度都只接受有限值 | Field '<name>' contains a non-finite number (NaN/Infinity) |
| 值域 | 物件、陣列、字串、數字、布林、null | Field '<name>' contains an unsupported JSON value type (<typename>) |
| 可序列化 | 必須能通過 json.dumps | Field '<name>' is not JSON-serializable |
這些訊息會包在 400 裡,形式為 Validation errors: <message>。系統不會為了塞得下而截斷資料——超過上限就整筆寫入失敗。
有兩點值得在前端先擋。NaN、Infinity、-Infinity 在樹中任何位置都會被拒絕,不只是最外層;Python 的 JSON parser 收得進來,所以這是真的會發生的輸入,而 MySQL JSON 存不了它們。另外,大小檢查是在結構走訪之後才跑,所以一份同時超大又過深的文件回報的是深度錯誤,不會是大小錯誤。
REST 不會自動 parse 字串化的 payload。"Payload": "{\"a\":1}" 存進去是一個 JSON 字串純量,不是物件。Agent 工具組的行為相反:對 json 型別的 key,只要字串的第一個非空白字元是 [ 或 { 就會 parse。同一份 body 因為送出者不同,最後存下的 cell 也會不同。
局部更新會整份取代文件。json cell 沒有 merge 或 patch 運算子——讀出來、改好、再把完整的值寫回去。
什麼算「空」
三條路徑的判定不一致,而且是刻意的:
| 路徑 | null | "" | {} / [] |
|---|---|---|---|
紀錄驗證器(required 檢查) | 空,拒絕 | 空,拒絕 | 有值,接受 |
require 規則 | 觸發 | 觸發 | 不觸發 |
規則/trigger when、簽核閘門 | 空 | 真實值 | 真實值 |
SQL is_null | 命中(key 不存在或 JSON null) | 不命中 | 不命中 |
實務後果:想用 {} 「清空」一個必填 json 欄位,會悄悄通過必填檢查;而 when <json col> is_null 也不會因為 "" 而觸發。
顯示與回傳行為
json cell 讀回來就是存進去的樣子——不展開、不加工、不包裝。
{
"data": {
"Order number": "SO-1042",
"Payload": {
"items": [{ "qty": 2, "sku": "A-1" }, { "qty": 1, "sku": "B-7" }],
"score": 87,
"tier": "gold"
}
}
}紀錄 cell 存在 MySQL 的 JSON 欄位裡,因此來回一趟不保證保留物件的 key 順序,重複 key 也會被合併。用插入順序渲染表單的前端會出錯;等值語意則不受影響,因為 MySQL JSON 的比較是正規化過的。
CSV/XLSX 匯出會把 json cell 輸出成 key 已排序的精簡 JSON,判斷依據是欄位型別而不是值的形狀——所以放著物件陣列的 cell 會匯出成真正的 JSON 文字,而不是塌成空字串。null 匯出成空字串。
{"items":[{"qty":2,"sku":"A-1"},{"qty":1,"sku":"B-7"}],"score":87,"tier":"gold"}REST 上的過濾與排序
json 欄位只能用純量做等值比對,而且永遠不能排序。
| 介面 | 接受 | 拒絕 |
|---|---|---|
舊版 filters map | 純量值,編譯成型別化的 JSON 等值比較 | dict/list → 400 Filtering json columns requires a scalar value;非有限數字 → 400 Filtering json columns requires a finite scalar value (NaN/Infinity rejected) |
stored_filters / any_of | eq、neq、in、is_null、is_not_null | 其餘全部 |
sort_by、多鍵 sort | — | 一律 400 |
records/aggregate 的 group_by 與 metric | — | 一律 400 |
全域 q 全文搜尋 | — | q 從不搜尋 json 欄位 |
比較時兩邊都會經過 JSON parser,不做型別轉換,所以數字 1 永遠不會命中字串 "1"——請用正確的 JSON 型別送出運算元。neq 會明確排除空 cell(key 不存在或明確的 JSON null),因此未設定的 cell 不會混進否定條件的結果。REST 詞彙裡完全沒有 not_in。
拒絕訊息取決於運算子先撞到哪一道閘門:
gt、gte、lt、lte、contains、between會走到json分支:400json columns do not support filter op '<op>' (supported: is_null/is_not_null/eq/neq/in)。is_empty/is_not_empty更早就被存在性閘門攔下:400filter op '<op>' is only valid on link or attachment columns。within_last/within_next/older_than更早就被相對日期閘門攔下:400filter op '<op>' is only valid on date/datetime columns。- 合法運算子搭配 dict/list 運算元:400
json columns can only be filtered by eq/neq/in with a scalar value;非有限數字運算元:400json columns can only be filtered by a finite scalar value (NaN/Infinity rejected)。
排序在 CRUD 執行點被所有路徑拒絕,訊息是 400 Sorting on json columns is not supported。若表上至少有一個計算欄,router 層的前置檢查會先觸發,訊息帶欄位名:400 Sorting on json column '<name>' is not supported。聚合的拒絕訊息是 group_by column '<display>' is type json — only stored scalar/select columns and cardinality-"one" link columns can be grouped by 與 metric column '<display>' is type json — only stored scalar/select columns can be aggregated (rollup/lookup/formula cells are computed on read; link columns have no cell value)。
儲存檢視在設定當下不做型別檢查。檢視可以把 json 欄位存進 sort_by;只有在套用檢視時才會失敗——包括公開讀取路徑,那裡會冒出同一個 400。
json 被拒絕的所有位置
json 不在「已儲存純量」允許清單裡,這一件事就解釋了下表大部分內容。每一次拒絕都是明確而且 fail-closed 的。
| 介面 | 結果 |
|---|---|
| Rollup / lookup 目標欄 | 拒絕——不是已儲存純量 |
| Rollup filter 的條件欄 | 拒絕;計算欄的 filter 檢查器另有專屬 json 分支,回報 no value (json columns are not filterable here),確保序列化文件永遠不會被當成字串比較 |
| Formula 引用(表層 formula 與 compose command formula) | 400 Formula cannot reference a json column |
unique 規則成分 | 400 column '<display>' is type json — json columns cannot be unique components (key order and number-vs-string ambiguity make uniqueness non-canonical) |
compare / check / transition / no_overlap / matches 規則欄位 | 拒絕——不是已儲存純量 |
Upsert 的 match_column | 400 Column '<match_column>' is a json column and cannot be used as match_column |
| 簽核閘門的自然鍵 | 400 Column '<match_column>' is a json column and cannot be used as an approval match_column |
非同步批次更新的 match_column | 工單失敗,訊息放在 error_message——沒有 HTTP 錯誤,輪詢的客戶端必須讀這個欄位 |
| ACL row policy 條件 | 422 row policy column '<col_hex>' is type json — row policies may only reference stored scalar columns。巢狀葉節點的前綴會帶上樹狀路徑:row policy and[0] column '<col_hex>' is type json — … |
| SCP 純量葉節點 | 400 column '<display>' is type json — policy requires one of: string, text, integer, float, boolean, date, datetime, select, user, social_client |
REST group_by / metric | 400,見上 |
| IaC record 自然鍵 | 這是 validate 階段的 plan 錯誤,會讓 plan 無法 applyable——不是 HTTP 400。詳細訊息:key column '<ref>' is a json column and cannot be a natural key (json values have no canonical bind representation and cannot uniquely match records),action 上另有簡短形式 json key column not allowed |
| CSV/XLSX 匯入——新建表 | 400 json columns cannot be created via import; create the table first, then add them via the column endpoint |
| CSV/XLSX 匯入——對應到既有欄位 | 400 Column '<target>' is a json column and cannot be an import target |
Trigger invoke_command 以 $row.<col> 餵入參數 | 400 invoke_command input '<name>': a 'json' column cannot feed a command input (json/attachment/link/computed values do not survive input validation) |
IaC 的限制只在鍵上。IaC 的 column 行可以建立 json 欄位,record 的 data 值也會原樣通過(只有 key 會做 ref 轉譯)。Plan hash 是遞迴排序過的,所以 json cell 不會造成幻覺漂移。
json 可以用的地方
require規則接受json欄位——「有沒有填」與型別無關。但要記得{}與[]都算有填。- 規則
when、triggerwhen與 trigger action 的source.filter接受json欄位,但只支援is_null、is_not_null、eq、neq。其他運算子是 400when column '<display>' is type json — only is_null/is_not_null/eq/neq are supported。這些路徑用的是 Python 深度相等,所以這裡的value可以是 dict 或 list,即使 SQL 過濾路徑只收純量。這同時也是簽核閘門的比對器,因此require_approval when { <json col> eq { ... } }真的會擋下寫入。 - Trigger 與 callback 的
$row.<col>token 會把jsoncell 渲染成精簡 JSON 文字(ensure_ascii=False),絕不是 Python repr——在訊息內容、URL 樣板(之後再做 URL 編碼)與 LINE Flex 葉節點都一樣。唯一例外:API 呼叫body 樣板裡剛好整個等於$row.<internal>的葉節點,仍然送出原始型別物件。
型別轉換
轉換時 json 有一套明確的規則:
| 變更 | 行為 |
|---|---|
string / text → json | 用 json.loads(value.strip()) 解析,再重新檢查 cell 上限 |
integer / float / boolean → json | 原值通過 |
json → string / text | 輸出 json.dumps(value, ensure_ascii=False, sort_keys=True);若會超過明訂的 max_length 就重設 |
其他任何牽涉 json 的跨族轉換 | 每個 cell 重設為欄位預設值 |
上表中每一次失敗都會把 cell 重設為欄位預設值——而 json 欄位不能宣告預設值,所以永遠是 null。把一個裝滿 JSON 字串的 text 欄位轉成 json,能解析的列會保留,不能解析的會默默被丟掉,而且沒有逐列報告。執行前務必先警告。
還原歷史版本時,快照會用目前的上限重新驗證,所以超過舊上限的文件回不來。這會在歷史版本還原路由上以 409 呈現:{"detail": "Cannot restore to version <n>: schema has changed incompatibly", "conflicts": [...]},其中每個衝突是 {"type": "type_mismatch", "field": ..., "message": "Value for column '<col_hex>' violates json column limits: ..."}。請注意這個訊息用的是內部 col_<hex> 鍵;所有寫入路徑的上限錯誤用的則是顯示名稱。
Compose command
json 在 command DSL 裡是一級宣告型別——它出現在純量型別集合、執行值型別,以及 item_schema 可接受的欄位型別中,所以 command 可以接受一個「物件陣列,其中某個成員是自由格式 json」的輸入。
$json_parse、$json_get、$json_stringify分別負責讀取、取出與重新定型文件內容。$json_get接受最長 2048 字元的 RFC-6901 pointer,以及型別化的on_error策略。- 當運算式的 kind 是
json、object、array、string、integer、float、boolean或null時,可以寫入json目標欄(null仍需要目標欄可為空)。把json/object運算式寫進非json目標欄仍然會被拒絕:JSON object cannot be written; stringify or extract it。 SELECT步驟拒絕在order_by使用json/object/array運算式(json columns cannot be sorted)、在group_by使用(json columns cannot be grouped)、以及搭配distinct使用(json columns cannot be used with distinct)。當沒有id或序號可當決勝鍵時,jsonkind 的引用也會被排除在備援決勝鍵集合之外。
Command 引擎的 JSON 樹預算是 262144 bytes(256 KiB),深度 32、節點 8192 與 cell 相同——是 65536 bytes cell 上限的四倍。因此運算式可能組出引擎接受、但寫入 cell 時被拒絕的文件。失敗會發生在寫入那一步,不是編寫當下。
AI agent 介面
以下功能只存在於 agent 工具組。REST 的 stored_filters 根本沒有 json_path 這個欄位。
先探索結構
對 json 欄位呼叫 profile_column 且不帶 json_path,回傳的是結構剖析而非值剖析:sample_size、null_ratio、shape_distribution(object / array / scalar)、array_vs_object_ratio、avg_serialized_bytes、max_serialized_bytes,以及一份依出現次數排序的 keys。每個 key 帶有 occurrences、type_distribution、sample_values、nested。另外還有 json_type_distribution,那是 SQL JSON_TYPE() 的直方圖,使用 MySQL 標籤(OBJECT、ARRAY、STRING、INTEGER、DOUBLE、BOOLEAN、NULL)——與 structure 裡的形狀標籤不同。
這份剖析是抽樣,不是普查:最多 500 列、最多 40 個排名 key(被裁掉最少見的那些時會帶 keys_truncated: true)、每個 key 最多 3 個純量樣本值、字串截斷到 60 字元並加上 …,整體輸出上限 4096 bytes。它只走訪深度 1 與深度 2 的物件 key("a.b" 記法),從不走訪陣列元素。值是物件或陣列的 key 會回報 sample_values: [] 與 nested: true。
有一個數字要小心解讀:整格的 null_count(以及由它算出的 null_ratio)只計算 SQL NULL 或 key 不存在,不含明確的 JSON null。由於建立紀錄時每個未提供的欄位都會實體化成 None,未設定的 cell 通常會存成 JSON null——所以 null_count 會低估空 cell,而同一份回應裡由 Python 端算出的 shape_distribution 則是正確的。
json_path 葉節點文法
json_path 指向單一葉節點,使用以點分隔的區段,每段符合 [A-Za-z_][A-Za-z0-9_]*,可選擇性接上 [<index>] 陣列下標(索引 0–9999)。最多 8 段、最多 128 字元。tier 與 items[0].sku 合法;萬用字元、引號、非 ASCII 名稱與運算子都不合法。其他寫法會直接回傳文法提示:
json_path '<path>' for column '<col>' is invalid. json_path grammar: dot-separated
segments matching [A-Za-z_][A-Za-z0-9_]* with optional [<index>] array subscripts
(0-9999), max 8 segments, max 128 characters, e.g. 'tier' or 'items[0].sku'.對非 json 欄位設定 json_path 會回報 json_path is only valid on json columns — '<col>' is type '<ct>'.
過濾
| 範圍 | 運算子 |
|---|---|
整格(不帶 json_path) | eq、neq、in、not_in、is_null、is_not_null、contains |
葉節點(帶 json_path) | 以上全部再加上 gt、gte、lt、lte |
整格的 contains 是對 cell 序列化文字做的定性子字串比對,所以連 key 也會命中——contains: "tier" 會命中一筆 key 叫 tier 的紀錄,即使沒有任何值包含它。它要求字串運算元。這是官方認可的「這筆紀錄有沒有提到 X」原語,不是型別化比較。
帶 json_path 時,gt/gte/lt/lte 需要數值運算元,並以葉節點執行期的 JSON 型別為數值為前提;contains 需要字串運算元,並以葉節點型別為 STRING 為前提。型別不符的葉節點會直接從結果中排除而不進行比較,因此跨型別排序永遠不會給出「看起來很篤定卻是錯的」答案。
等值比較以運算元自身的型別決定閘門:字串走 STRING 閘門加去引號,數字走數值閘門,布林走 BOOLEAN-或-INTEGER 閘門。最後一項是刻意的跨方言妥協——JSON 整數 1 或 0 的 cell 也會命中布林條件。neq 與 not_in 都以非空為前提,未設定的 cell 永遠不會命中。
json_length 是獨立欄位 {op: "eq" | "gt" | "lt", value: <int >= 0>},會取代同一個條件上的 operator/value;兩者同時給會得到 Provide either 'json_length' or 'operator'/'value' on a condition, not both.。它計算整格陣列(不帶 json_path)或葉節點陣列(帶 json_path)的元素數量,且在兩種方言上都限定 ARRAY——非陣列的 cell 或葉節點會默默排除該列而不報錯,也不像原生 MySQL JSON_LENGTH 那樣會去數物件的 key。對非 json 欄位使用會回報 json_length is only valid on json columns — '<col>' is type '<ct>'.,而且這個檢查排在 json_path 檢查之前。
兩個運算子拒絕訊息會直接列出支援集合:
Operator '<op>' cannot be applied to json column '<col>' — only eq, neq, in, not_in,
is_null, is_not_null, contains (matches the serialized JSON text) are supported on the
whole cell. Set json_path to target a leaf for gt/gte/lt/lte, or use json_length for an
array element count.
Operator '<op>' cannot be applied to json_path '<path>' on column '<col>' — supported:
<sorted ops>.排序、分組、聚合
query_records 新增了 sort_json_path 與 sort_json_type("number" 或 "text")。兩者必須同時提供,且只有在 sort_column 指向 json 欄位時才有效,並與多鍵 sort 互斥(多鍵 sort 直接拒絕 json,並用自己的訊息指回單欄形式)。"number" 依數值閘門排序葉節點,"text" 依去引號後的葉節點排序。這是所有介面中唯一能依 json cell 內部內容排序的方式——REST 沒有對應功能。
分組要用 group_by_paths: [{column, json_path}] 而不是 group_by;結果的鍵標籤是 "<column>.<json_path>",而 group_by 加上 group_by_paths 合計最多 3 個。
聚合依範圍分成兩種:
| 範圍 | 允許的函式 |
|---|---|
| 整格 | 只有 count |
葉節點(json_path) | sum、avg、min、max、median(數值閘門)以及 count、count_distinct、value_counts(去引號後的葉節點文字) |
json_path 不能搭配 column: "*",也不能搭配 weighted_avg。整格的拒絕來自三道不同閘門、三種不同訊息:sum/avg/median/weighted_avg 撞到數值閘門(Function '<fn>' requires a numeric column, but '<col>' is type 'json'. Use json_path to target a numeric leaf.)、collect 撞到字串閘門(Function 'collect' is for text or principal (user/social_client) columns, but '<col>' is type 'json'.),只有 min/max/mode/count_distinct/value_counts 會拿到「沒有全序」那則訊息。
以物件或陣列葉節點分組,等於依該葉節點的序列化 JSON 文字分組,基數可能爆炸。請對純量葉節點分組。
常見陷阱
- 65536 bytes 的 cell 上限與 262144 bytes 的 compose command 運算式預算是兩個不同的數字。Command 可能組出引擎接受、寫入 cell 卻被拒絕的文件。
- REST 會把字串化的 payload 原樣存成 JSON 字串純量,agent 工具組則會自動 parse。同一份 body 因呼叫者不同而產生不同的 cell。
{}與[]會滿足require規則。「寫{}來清空欄位」會悄悄通過必填檢查。json→string會排序 key,所以產生的文字不保留原本的 key 順序。CSV/XLSX 匯出也一樣。- MySQL JSON 不保留物件 key 順序,且會合併重複 key。
- 全域
q搜尋從不觸及json欄位。要做「有沒有提到 X」請改用 agent 工具組的整格contains。 - 匯入既不能建立也不能對應到
json欄位。支援的做法是:先匯入,再用columns.create加上json欄位,最後透過紀錄 API 寫入 cell。 - 測試資料產生器對
json欄位輸出{"sample": "a" | "b" | "c", "value": <1-100>}。
試試看
在 API Playground 用 columns.create 加一個 json 欄位,透過 records.create 寫入一份巢狀文件,再對該欄位試一次 sort_by,看看拒絕訊息。葉節點定址的完整詞彙請見 API 工具參考與查詢紀錄。