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

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

[開啟JSON 格式化](https://abox.tools/zh-TW/json-formatter/): JSON、XML、HTML、CSS 和 YAML，格式化或轉換。沒有一個字被貼進別人的伺服器。

最後更新 2026年8月26日

## 簡短的回答

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

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

![兩個窗格：左邊是一行 JSON，右邊是同一份文件以兩個空格縮排排好的樣子。](https://abox.tools/screens/format-json-without-uploading-it/panes.webp)

一行進去，能讀的東西出來。為此沒有把任何東西送到任何地方。

## 格式化是什麼，不是什麼

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` 這樣的偏移量不算；行號和列號才算。

- **末尾多出來的逗號。**`{"a": 1,}` 在 JavaScript 裡合法，在 JSON 裡不合法。單一原因裡最常見的一個，通常是刪掉列表最後一項之後留下的。
- **單引號。**`{'a': 1}` 是 JavaScript 的物件字面量，不是 JSON。字串和鍵都用雙引號，而且鍵永遠要帶引號。
- **沒加引號的鍵。**`{a: 1}`，同一個錯誤從另一頭犯起——通常是因為從程式碼裡、而不是從檔案裡貼出來的。
- **註釋。**`// 像這樣` 也不是 JSON。那是 JSONC，VS Code 的設定和 `tsconfig.json` 用它，別處一概讀不了。要是一條註釋非留不可，慣例是拿一個鍵來放：`"_comment": "..."`。
- **字串裡真正的換行或製表符。**它們必須寫成 `\n` 和 `\t`。手工把一條 shell 命令或者一份證書貼進某個值裡時，通常就是這裡出差錯。
- **JSON 不允許的數。**前導零（`01`）、光禿禿的小數點（`.5`）、`NaN`、`Infinity` 和 `+1`，這些人們都寫，而它們沒有一個是 JSON。

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

![同一個工具遇到一份壞掉的文件：錯誤提示指出多餘逗號所在的行和列，輸入窗格裡正顯示那一行。](https://abox.tools/screens/format-json-without-uploading-it/error.webp)

讀不動的時候，提示會說清在哪裡。多餘的逗號是最常見的原因，也是肉眼最難發現的原因。

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

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

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

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

## 給鍵排序，以及什麼時候別排

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

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

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

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

## 比較兩個 JSON 檔案

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

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

## 沒人寫在頁面上的那部分

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

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

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

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

這兩條還有更長的版本，外加另外兩條檢查，都在[把檔案傳上去安全嗎](https://abox.tools/zh-TW/guides/is-it-safe-to-upload-files/)那一頁。

## 那 YAML、XML 和其餘的呢

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

- **把 YAML 轉成 JSON 會丟掉註釋**，因為 JSON 沒有地方放註釋。錨點和別名——YAML 用來說「同一個節點出現兩次」的辦法——也沒法表達，所以這裡選擇直接拒絕，而不是替你猜。
- **`no` 是一個字串。**在 YAML 1.1 裡，`yes`、`no`、`on` 和 `off` 是布林值，所以一份含挪威的國家程式碼列表，從前回來時裡面會多出一個 `false`。YAML 1.2 把這條去掉了，這裡也一樣——但這些詞寫回去時仍然帶引號，因為下一個開啟這份檔案的，可能是個 1.1 的讀者。
