abox.tools

語言: 繁體中文

怎麼不把 JSON 交給任何人也能格式化

格式化 JSON 該動的只是空白,別的什麼都不該動。大多數願意替你做這件事的工具動得更多,而且沒有一個會提。下面說的是該留神什麼、檔案讀不出來時那條錯誤該怎麼讀,以及你把一個配置檔案貼進去的那個框,值得先想一想。

開啟JSON 格式化 JSON、XML、HTML、CSS 和 YAML,格式化或轉換。沒有一個字被貼進別人的伺服器。

以 Markdown 檢視

簡短的回答

開啟 JSON 格式化,把 JSON 貼進框裡,讀它。排版是你打字的時候就發生的,語言是從文字本身認出來的,縮排預設兩個空格,除非你另有吩咐。什麼都不會被上傳,因為它沒有地方可去:解析器不過是幾百行 JavaScript,跑在你已經開啟的這個分頁裡。

底下所有內容,都是你在把一個配置檔案貼進那些替代品之前值得知道的:一個格式化工具可以改什麼、大多數工具無論如何都改了什麼,以及在檔案根本讀不出來時,那條錯誤該怎麼讀。

兩個窗格:左邊是一行 JSON,右邊是同一份文件以兩個空格縮排排好的樣子。
一行進去,能讀的東西出來。為此沒有把任何東西送到任何地方。

格式化是什麼,不是什麼

JSON 幾乎沒有語法。一個物件、一個陣列、一個字串、一個數,再加上 true、false、null 這三個詞。在這些零件之間,空白什麼也不表示:下面這個檔案

{"name":"thing","tags":["local","offline"]}

和下面這個檔案

{
  "name": "thing",
  "tags": [
    "local",
    "offline"
  ]
}

是同一份文件。格式化就是從前一個走到後一個這件事,整個工作就這麼多。一個格式化工具對你的檔案做的其他任何事——重排、取整、丟掉——都是在沒人要求的情況下,改動了這份文件說的話。

其中三種改動常見到值得點名,因為它們是無聲的,而且正是一個用一下午寫出來的格式化工具預設會做的事。

格式化工具不該改的三樣東西

你的鍵的順序

這是最容易把人絆住的一條。用 JavaScript 寫 JSON 格式化最順手的做法,是先調 JSON.parse,再帶上縮排調 JSON.stringify,而這一對並不保留那些長得像整數的鍵的順序:

Object.keys(JSON.parse('{"10":"a","2":"b","x":"c"}'))
// ['2', '10', 'x']

這不是誰的程式碼有毛病。JavaScript 的物件在規範裡就規定了要把類整數的鍵排在前面、按數值升序排,而凡是經過 JSON.parse 的值都會變成 JavaScript 物件。這麼造出來的格式化工具,會把一份按 id、埠號、年份或者 HTTP 狀態碼作鍵的檔案重排一遍,而且一聲不吭。

這要不要緊,看檔案。JSON 的物件原則上是無序的,所以嚴格說沒壞什麼——但和你儲存庫裡那一版的 diff 會大得離譜,評審根本沒法看,而如果下游有什麼東西是按順序讀這個檔案的,行為就變了。

你的數的位數

JSON 沒說一個數能有多大,JavaScript 說了:每個數都是雙精度。所以一個把數解析成雙精度再列印回去的格式化工具,會丟掉一切雙精度裝不下的東西。

JSON.stringify(JSON.parse('{"id":123456789012345678901}'))
// {"id":123456789012345680000}

JSON.stringify(JSON.parse('{"size":1e999}'))
// {"size":null}

一個二十一位的 id——一個 Twitter id、一個 Snowflake id、一個銀行參考號——回來的時候成了另一個數;而一個大到雙精度裝不下的值,回來的時候成了 null。兩個檔案照樣解析得過,但沒有一個還是你出發時的那個檔案。

出路是根本不去解析數。格式化工具為了把文件排出來,只需要知道一個數從哪裡開始、到哪裡結束,它從來不需要那個值,所以穩妥的做法是把那些數字原樣照抄過去。這裡的工具就是這麼做的。

你重複的鍵

{"a": 1, "a": 2} 是合法的 JSON,而標準拒絕說明這兩個哪個算數。實際上各家解析器意見不一:多數留最後一個,有些留第一個,少數乾脆拒收這份文件。一個悄悄輸出其中之一的格式化工具,替你做了這個決定,還把「這裡本來有兩個」這個有用得多的事實藏了起來——而那幾乎總是檔案裡的一個錯誤,一個你會想看見的錯誤。

檔案讀不出來的時候

讀不出來的 JSON,絕大多數並不稀奇。它是大約六件事裡的某一件,而只要錯誤資訊用你找得到的方式說清了位置,它就已經告訴你是哪一件了。像 位置 4193 這樣的偏移量不算;行號和列號才算。

還有一樣不是錯誤、卻長得像錯誤的東西:以位元組序標記開頭的檔案。它在多數編輯器裡看不見,它也不是空白,它讓文件的第一個字元變得出人意料。要是一份看上去毫無問題的檔案報錯在第 1 行第 1 列,那就是它。

同一個工具遇到一份壞掉的文件:錯誤提示指出多餘逗號所在的行和列,輸入窗格裡正顯示那一行。
讀不動的時候,提示會說清在哪裡。多餘的逗號是最常見的原因,也是肉眼最難發現的原因。

壓縮,以及它通常省下的那一點點

把空白擠出去是同一個操作反過來做,而它到底買來了什麼,值得實實在在地看。空白是高度重複的東西,而你和讀者之間的每一臺伺服器、每一個瀏覽器,早就用 gzip 或者 Brotli 把回應壓過一遍了,而它們正好最擅長這種重複。

所以壓過的 JSON 作為檔案常常小三成,線上路上卻只小几個百分點。它真正值回票價的地方,是前面沒有壓縮的那些:資料庫某一列裡的值、日誌一行裡的一個欄位、塞進 QR Code 的內容,或者你正打算 Base64 之後放進請求頭的文件。

它的代價是可讀性,而要是這個檔案進了儲存庫,代價裡還要加上 diff——一個只有一行的檔案,只要裡面有任何東西變了,整行就全變了。壓縮要放在從編輯器出去的路上,不是進來的路上。

給鍵排序,以及什麼時候別排

給每個物件的鍵排序,在這裡是一個選項,而不是預設就做的事,因為那是對檔案的真實改動,而它值不值得,完全取決於你接下來要做什麼。

當你在比較兩份本該說同一件事的文件時它有用——兩個環境的配置、一次改動前後的 API 回應——而其中一份把鍵排成了另一個順序。先把兩份都排一遍,就把「哪裡都不一樣」的 diff 變成了「真正不同的那兩行」的 diff。

當順序本身在承擔某種意義時它有害。package.json 對什麼放在前面是有慣例的;手寫的配置常常把相關的設定擺在一起;而一份被工具排過序又提交上去的檔案,會產出一個巨大而毫無意義的提交。排一份副本,別排原件。

有個細節值得知道:這裡的排序按的是鍵讀起來的樣子,而不是它們的碼位,所以 item2 排在 item10 前面,而不是後面。按碼位排,正是把 item10 塞到一堆 1 開頭的裡頭去的那種排法:技術上沒錯,對讀的人毫無用處。

比較兩個 JSON 檔案

可靠的做法是先把兩份用同樣的方式格式化一遍。兩份說著同一件事的文件,只要一份壓過、另一份沒壓,就能每一行都不一樣,而沒有哪個 diff 能看穿這一層。

所以:格式化第一份,格式化第二份,然後比較這兩個結果。這三步在這裡都在同一個頁面上——比較標籤和格式化共用同一個框,正是為了這個。要是這兩份還把鍵排成了不同的順序,那就在格式化的時候把兩邊都排一遍,比較就會收縮成你本來要找的那點差別。

沒人寫在頁面上的那部分

搜一個 JSON 格式化工具,你會找到幾十個帶輸入框的網站。往那個框裡一貼,就是一次上傳。你剪貼簿裡裝的是什麼都一樣——一份帶著客戶地址的 API 回應、一個帶著連線串的配置檔案、一枚你正在除錯的令牌——它已經被送到一臺不歸你管的機器上,現在它是人家的日誌、人家的錯誤報告、人家的備份。

這不是在假設誰心懷惡意。一個完全出於好意的網站,照樣留訪問日誌,照樣跑統計,照樣有一家託管商。最安全的資料是從來沒離開過的資料,而對一件純粹是字串操作的工作來說,它壓根沒有離開的理由。

兩條檢查,而且它們對任何做出這種宣告的網站都管用,不只是這一個:

  1. 開啟 DevTools,盯著 Network 標籤,格式化點東西。要是你的文字正在被送出去,就一定有一條請求馱著它。同一時刻不可能有別的解釋。
  2. 斷開網路再試一次。在你瀏覽器裡運作的工具毫無影響。把你的文字送去別處的工具會立刻、徹底地停擺。

這兩條還有更長的版本,外加另外兩條檢查,都在把檔案傳上去安全嗎那一頁。

那 YAML、XML 和其餘的呢

同一個頁面也讀 XML、HTML、CSS 和 YAML,並且能在 JSON 和其中的第一種與最後一種之間互轉。有兩件事值得從上面帶過來,因為它們不過是同一個道理換了身衣服: