Skip to Content
核心概念欄位型別JSON

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 欄位只接受 nametyperequireddescription

{ "name": "Payload", "type": "json", "required": false, "description": "原始 ERP 訂單封包" }

default_valuemax_lengthoptions 一律禁止,附件上限欄位(max_countmax_file_bytesallowed_mime_types)與所有計算欄設定欄位也一樣。json 欄位的預設值永遠是 null

欄位json 欄位上的結果
default_value422 json columns cannot have a default_value
max_length422 json columns cannot set max_length
options422 json columns cannot set options
附件上限欄位422 fields [...] are only valid on attachment columns
計算欄設定422 fields [...] are not valid on json columns

其中兩個訊息在 dict 層的最後防線(內部呼叫與轉換過的 schema 也會經過)有第二種寫法:default_valuejson columns cannot have a default value(三個字、沒有底線),max_lengthColumn '<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 bytesField '<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)
值域物件、陣列、字串、數字、布林、nullField '<name>' contains an unsupported JSON value type (<typename>)
可序列化必須能通過 json.dumpsField '<name>' is not JSON-serializable

這些訊息會包在 400 裡,形式為 Validation errors: <message>。系統不會為了塞得下而截斷資料——超過上限就整筆寫入失敗。

有兩點值得在前端先擋。NaNInfinity-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_ofeqneqinis_nullis_not_null其餘全部
sort_by、多鍵 sort一律 400
records/aggregategroup_by 與 metric一律 400
全域 q 全文搜尋q 從不搜尋 json 欄位

比較時兩邊都會經過 JSON parser,不做型別轉換,所以數字 1 永遠不會命中字串 "1"——請用正確的 JSON 型別送出運算元。neq 會明確排除空 cell(key 不存在或明確的 JSON null),因此未設定的 cell 不會混進否定條件的結果。REST 詞彙裡完全沒有 not_in

拒絕訊息取決於運算子先撞到哪一道閘門:

  • gtgteltltecontainsbetween 會走到 json 分支:400 json columns do not support filter op '<op>' (supported: is_null/is_not_null/eq/neq/in)
  • is_empty / is_not_empty 更早就被存在性閘門攔下:400 filter op '<op>' is only valid on link or attachment columns
  • within_last / within_next / older_than 更早就被相對日期閘門攔下:400 filter 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;非有限數字運算元:400 json 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 bymetric 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_column400 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 / metric400,見上
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、trigger when 與 trigger action 的 source.filter 接受 json 欄位,但只支援 is_nullis_not_nulleqneq。其他運算子是 400 when 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 { ... } } 真的會擋下寫入。
  • Triggercallback$row.<col> token 會把 json cell 渲染成精簡 JSON 文字(ensure_ascii=False),絕不是 Python repr——在訊息內容、URL 樣板(之後再做 URL 編碼)與 LINE Flex 葉節點都一樣。唯一例外:API 呼叫body 樣板裡剛好整個等於 $row.<internal> 的葉節點,仍然送出原始型別物件。

型別轉換

轉換時 json 有一套明確的規則:

變更行為
string / textjsonjson.loads(value.strip()) 解析,再重新檢查 cell 上限
integer / float / booleanjson原值通過
jsonstring / 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 是 jsonobjectarraystringintegerfloatbooleannull 時,可以寫入 json 目標欄(null 仍需要目標欄可為空)。把 jsonobject 運算式寫進 json 目標欄仍然會被拒絕:JSON object cannot be written; stringify or extract it
  • SELECT 步驟拒絕在 order_by 使用 jsonobjectarray 運算式(json columns cannot be sorted)、在 group_by 使用(json columns cannot be grouped)、以及搭配 distinct 使用(json columns cannot be used with distinct)。當沒有 id 或序號可當決勝鍵時,json kind 的引用也會被排除在備援決勝鍵集合之外。

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_sizenull_ratioshape_distributionobject / array / scalar)、array_vs_object_ratioavg_serialized_bytesmax_serialized_bytes,以及一份依出現次數排序的 keys。每個 key 帶有 occurrencestype_distributionsample_valuesnested。另外還有 json_type_distribution,那是 SQL JSON_TYPE() 的直方圖,使用 MySQL 標籤(OBJECTARRAYSTRINGINTEGERDOUBLEBOOLEANNULL)——與 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 字元。tieritems[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_patheqneqinnot_inis_nullis_not_nullcontains
葉節點(帶 json_path以上全部再加上 gtgteltlte

整格的 contains 是對 cell 序列化文字做的定性子字串比對,所以連 key 也會命中——contains: "tier" 會命中一筆 key 叫 tier 的紀錄,即使沒有任何值包含它。它要求字串運算元。這是官方認可的「這筆紀錄有沒有提到 X」原語,不是型別化比較。

json_path 時,gtgteltlte 需要數值運算元,並以葉節點執行期的 JSON 型別為數值為前提;contains 需要字串運算元,並以葉節點型別為 STRING 為前提。型別不符的葉節點會直接從結果中排除而不進行比較,因此跨型別排序永遠不會給出「看起來很篤定卻是錯的」答案。

等值比較以運算元自身的型別決定閘門:字串走 STRING 閘門加去引號,數字走數值閘門,布林走 BOOLEAN-或-INTEGER 閘門。最後一項是刻意的跨方言妥協——JSON 整數 10 的 cell 也會命中布林條件。neqnot_in 都以非空為前提,未設定的 cell 永遠不會命中。

json_length 是獨立欄位 {op: "eq" | "gt" | "lt", value: <int >= 0>},會取代同一個條件上的 operatorvalue;兩者同時給會得到 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_pathsort_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_pathsumavgminmaxmedian(數值閘門)以及 countcount_distinctvalue_counts(去引號後的葉節點文字)

json_path 不能搭配 column: "*",也不能搭配 weighted_avg。整格的拒絕來自三道不同閘門、三種不同訊息:sumavgmedianweighted_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'.),只有 minmaxmodecount_distinctvalue_counts 會拿到「沒有全序」那則訊息。

物件或陣列葉節點分組,等於依該葉節點的序列化 JSON 文字分組,基數可能爆炸。請對純量葉節點分組。

常見陷阱

  • 65536 bytes 的 cell 上限與 262144 bytes 的 compose command 運算式預算是兩個不同的數字。Command 可能組出引擎接受、寫入 cell 卻被拒絕的文件。
  • REST 會把字串化的 payload 原樣存成 JSON 字串純量,agent 工具組則會自動 parse。同一份 body 因呼叫者不同而產生不同的 cell。
  • {}[] 會滿足 require 規則。「寫 {} 來清空欄位」會悄悄通過必填檢查。
  • jsonstring 會排序 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 Playgroundcolumns.create 加一個 json 欄位,透過 records.create 寫入一份巢狀文件,再對該欄位試一次 sort_by,看看拒絕訊息。葉節點定址的完整詞彙請見 API 工具參考查詢紀錄

Last updated on