A native macOS JSON editor that stays fast on documents other tools choke on.
JSONBuddy is a JSON viewer, editor and comparison tool for macOS, written in plain AppKit.
There is no SwiftUI, no WebView, and no third-party runtime. The text storage, layout, drawing, syntax highlighting, JSON parser and diff are all built from scratch, which is the whole point: browser-based and Electron JSON tools stall or die on a few dozen megabytes, and the ones that don't tend not to look like Mac apps. JSONBuddy is built so that a 100 MB document still scrolls at refresh rate and still takes keystrokes in under a frame.
It is a scratchpad, not a document editor. There is no Open and no Save — you paste JSON in, work on it, and it is still there next time you launch. That is a deliberate trade: the app never touches your files, so it can never damage them.
- Editor — syntax highlighting, line numbers, bracket matching, and a status bar breadcrumb showing the JSON path at the caret.
- Soft wrap — a 50 MB string value wraps to the viewport instead of trailing off sideways. A wrapped line still shows one line number, and the continuation rows stay blank.
- Transforms — format, minify, escape, unescape and recursive key sorting, each landing as a single undoable edit.
- Exact numbers — values are re-emitted from their original literal text and never round-tripped through
Double, so12345678901234567890and1e400come out of every transform byte-for-byte unchanged. - Folding — fold any object or array from the arrow beside its key, fold to a chosen depth, or fold the document whole. A long string value folds too, showing its length in place of the text.
- Semantic comparison — both sides are formatted and key-sorted first, so a reordered key is not reported as a change, and an element inserted at the head of an array does not turn the rest of it red.
- Find and replace — plain text and regular expressions, with the total counted on a background task so a large document keeps responding.
- Repair suggestions — trailing commas, comments, single-quoted strings and Python literals are recognised in invalid JSON and can be fixed in one click.
- Escaped JSON — text that only becomes valid JSON after one unescape is recognised as such, said so in the status bar, and unescaped automatically before a structural operation.
- Native behaviour — follows the system appearance and language (English and Simplified Chinese), and restores your tabs and folds on relaunch.
- No network — the app makes no requests of any kind. There is no telemetry and no crash reporting.
Download JSONBuddy_macOS15_arm64_<version>.dmg from the latest release, open it, and drag JSONBuddy into Applications.
Intel Macs are not supported: the build is arm64-only by design.
The app is not notarized. It is signed ad-hoc rather than with a Developer ID, so macOS will refuse to open it the first time and say it cannot be verified. Any one of the following will let it through — the first is the one Apple intends:
Option 1 — System Settings (recommended)
- Double-click JSONBuddy and dismiss the warning.
- Open System Settings → Privacy & Security.
- Scroll to the Security section, where you will see a line about JSONBuddy being blocked, and press Open Anyway.
- Confirm with Touch ID or your password.
Option 2 — Right-click
- Find JSONBuddy in Finder (in Applications).
- Right-click it and choose Open — using the menu item matters, double-clicking will not do.
- Press Open in the dialog.
Option 3 — Terminal
xattr -cr /Applications/JSONBuddy.appThis strips the quarantine attribute, after which the app opens normally.
Most of the app explains itself, but three things are worth knowing up front.
There are no files. Paste JSON into a tab with ⌘V — there is no Open, no Save, and no .json association. Tabs are numbered rather than named (double-click a tab to rename it), and their contents are kept in the app's own storage between launches, so nothing is lost when you quit. Nothing is ever written to your own files.
The toolbar buttons read the document and offer whichever direction applies. The layout button says Format when the document is minified and Minify when it is not; the escape button flips the same way; the sort button alternates A–Z and Z–A. There is one button per pair rather than two, so the row stays short.
To compare, open two tabs and press ⇧⌘D. Both sides are normalized first, so the result is a semantic diff rather than a text diff.
If you just want to see what the app does, Load Sample (⌘1) drops in a document that exercises the interesting cases — big integers, 1e400, escaped Unicode, a long base64 blob, CJK prose and duplicate keys. Generate Large Document (⌘2) produces roughly 30 MB to try the scrolling on.
Keyboard shortcuts
| Action | Shortcut |
|---|---|
| New tab | ⌘T |
| Close tab | ⌘W |
| Load sample / generate large document | ⌘1 / ⌘2 |
| Undo / redo | ⌘Z / ⇧⌘Z |
| Find / find and replace | ⌘F / ⌥⌘F |
| Find next / previous | ⌘G / ⇧⌘G |
| Format / minify | ⌘K / ⌥⌘K |
| Sort keys A–Z / Z–A | ⌥⌘S / ⌥⇧⌘S |
| Escape / unescape | ⌃⌘E / ⌃⌘U |
| Fold all / unfold all | ⌥⌘← / ⌥⌘→ |
| Fold to level 1–3 | ⌥⌘1 … ⌥⌘3 |
| Compare documents | ⇧⌘D |
| Next / previous difference | ⌥⌘↓ / ⌥⌘↑ |
| Next / previous tab | ⌃⇥ / ⌃⇧⇥ |
| Settings | ⌘, |
⌘M for minify, ⌘S for save and ⌘F for format are all deliberately avoided: macOS reserves them, and taking one breaks a habit that works everywhere else.
Build instructions are given for macOS only. The app is AppKit and arm64 macOS throughout, so there is nothing to build anywhere else.
| Dependency | Version |
|---|---|
| macOS | 26.6 |
| Xcode | 26.4.1 (Swift 6.3.1) |
These are the versions on the machine this project is developed and tested on, so they are known to build and run it. Lower versions may well work — the package only requires a Swift 6 toolchain, and CI builds it with Xcode 26.3 — but anything below the versions above is untested and not guaranteed.
Everything else the build needs (SwiftPM, iconutil, codesign, lipo) ships with Xcode, so there is nothing further to install.
macOS
Check your version by running sw_vers -productVersion, or open → System Settings → General → About and read macOS.
To upgrade, go to System Settings → General → Software Update and install what it offers. This is the only route worth using; downloading installers by hand is slower and easier to get wrong.
Xcode
Check whether it is installed and which one is selected:
xcode-select -p # prints the path of the active developer directory
xcodebuild -version # prints the Xcode and build version
swift --version # prints the Swift toolchain versionIf xcodebuild is missing, install Xcode from the Mac App Store (recommended — it updates itself and needs no account), or download it from developer.apple.com/xcode if you need a specific version. Launch it once after installing so it can finish setting up.
If you have several Xcodes, point the tools at the one you want:
sudo xcode-select -s /Applications/Xcode.appThe Command Line Tools alone are not enough for the icon and packaging steps; install the full Xcode.
# 1. Get the source.
git clone https://github.com/yuman07/JSONBuddy.git
cd JSONBuddy
# 2. Run the tests. Roughly ten seconds; no app is launched.
swift test
# 3. Build the app. This compiles arm64 release, generates the icon,
# assembles build/JSONBuddy.app and signs it ad-hoc.
Scripts/build-app.sh
# 4. Run it.
open build/JSONBuddy.appTo iterate on the engine alone, skip the UI tests — the other three targets are pure Swift and run in about eight seconds:
swift test --skip JSONBuddyUITestsThe app is four SwiftPM modules with a strict dependency order, and the split is load-bearing rather than cosmetic. TextEngine, JSONCore and JSONDiff contain no AppKit at all, so the parts that are actually hard — paged storage, wrap indexing, parsing, diffing — can be tested from the command line in seconds instead of by launching an app. JSONBuddyUI is the only module allowed to import AppKit, which is what keeps view code from leaking into the algorithms.
The pipeline runs in one direction. Keystrokes land in a paged copy-on-write byte buffer; every edit produces an immutable snapshot, and a debounced background parse works on that snapshot rather than on live memory, so the parser can never read a document that is being modified underneath it. Results are only adopted if the document has not moved on since — a version comparison that is the difference between a correct reformat and silently overwriting what the user typed while it ran.
Nothing off-screen is ever laid out. The document view's height is a row count times a fixed line height, and drawing touches roughly fifty rows whether the document has a thousand lines or twenty million. Two sparse indexes sit between document lines and screen rows: folding removes lines, then soft wrap expands what is left, and both are ordered indexes with prefix sums so a lookup stays a binary search.
| Area | Choice |
|---|---|
| Language | Swift 6, strict concurrency, swiftLanguageModes: [.v6] |
| UI | AppKit only — no SwiftUI, no WebView, no NSTextView for the editor |
| Text shaping | Core Text (CTLine, CTTypesetter) directly |
| Build | SwiftPM, plus Scripts/build-app.sh to assemble the bundle |
| Tests | swift-testing, 770 tests across 113 suites |
| Runtime dependencies | none |
flowchart TD
User([User]) -->|"paste, type, click"| Editor
subgraph UI["JSONBuddyUI — the only module with AppKit"]
Workspace["WorkspaceView<br/>tabs and panes"]
Editor["EditorTextView<br/>virtualised drawing"]
Layout["VisualLayout<br/>folds x soft wrap"]
end
subgraph Engine["TextEngine — no AppKit"]
Buffer["PagedBuffer<br/>64 KiB CoW pages"]
LineIndex["ChunkedLineIndex<br/>line starts, UTF-16 sums"]
Wrap["WrapIndex<br/>sparse wide lines"]
Searcher["TextSearcher<br/>byte-level scan"]
end
subgraph Core["JSONCore — no AppKit"]
Parser["JSONParser<br/>flat node array"]
Transform["JSONTransform<br/>format, minify, sort"]
Repair["JSONDiagnostic + JSONRepair"]
end
subgraph Diff["JSONDiff — no AppKit"]
Hash["StructuralHash<br/>128-bit per subtree"]
Differ["JSONDiffer"]
Aligner["DiffAligner"]
end
Workspace -->|"active document"| Editor
Editor -->|"byte edits"| Buffer
Buffer -->|"line starts"| LineIndex
Buffer -->|"immutable DocumentSnapshot"| Parser
Buffer -->|"page sequence"| Searcher
Editor -.->|"150 ms debounce, latest wins"| Parser
LineIndex -->|"line lengths"| Wrap
Wrap -->|"visual rows"| Layout
Parser -->|"fold ranges"| Layout
Parser -->|"key ranges for highlighting"| Editor
Layout -->|"rows to draw"| Editor
Searcher -->|"match ranges"| Editor
Parser -->|"JSONNode array"| Transform
Parser -->|"parse error + offset"| Repair
Repair -.->|"one-click fix"| Transform
Transform -->|"new document bytes"| Buffer
Workspace -->|"two snapshots to compare"| Differ
Parser -->|"nodes"| Hash
Hash -->|"subtree hashes"| Differ
Differ -->|"DiffOps"| Aligner
Aligner -->|"aligned row pairs"| Workspace
- The editing path runs
EditorTextView → PagedBuffer → ChunkedLineIndex. An edit copies at most one 64 KiB page, so a keystroke costs the same in a 1 KB document as in a 100 MB one. - The parse path is deliberately detached: the buffer hands the parser an immutable
DocumentSnapshot, and the dashed edge from the editor is the 150 ms debounce that triggers it. Only one parse runs at a time and the newest wins, so continuous typing cannot pile up several multi-million-node arrays at once. - The drawing path is
ChunkedLineIndex → WrapIndex → VisualLayout → EditorTextView. Fold ranges arrive from the parser on a separate edge, which is why folding survives a reformat: it is anchored to nodes, not to line numbers. - The transform path loops back into the buffer — every transform is a whole-document replacement applied as one undoable edit.
JSONRepairhangs off it by a dashed edge because it is optional: it only contributes when the document is invalid and the user accepts a suggested fix. - The comparison path is the only consumer of
StructuralHash, which is why hashes are computed lazily when a comparison starts rather than after every parse — on a document with millions of nodes that is hundreds of megabytes not spent.
JSONBuddy/
|-- Package.swift # four library targets, one executable, four test targets
|-- SPEC.md # the design document (Chinese): decisions and why
|-- Scripts/
| |-- build-app.sh # arm64 release build, bundle assembly, ad-hoc signing
| `-- make-icon.swift # the app icon, drawn in code rather than committed as art
|-- Resources/
| |-- en.lproj/ # English strings
| `-- zh-Hans.lproj/ # Simplified Chinese strings
|-- Sources/
| |-- TextEngine/ # paged CoW storage, line index, wrapping, folding, search
| |-- JSONCore/ # order-preserving parser, transforms, escaping, diagnostics
| |-- JSONDiff/ # structural hashing, semantic diff, side-by-side alignment
| |-- JSONBuddyUI/ # AppKit views: editor, gutter, tabs, toolbar, comparison
| |-- JSONBuddyApp/ # app delegate, menus, window assembly
| `-- *Bench/ # command-line benchmarks for the engine and diff
`-- Tests/ # one test target per module
Q: macOS says the app cannot be verified. Is something wrong with it?
No. The build is signed ad-hoc rather than with a paid Developer ID, so it is not notarized and Gatekeeper blocks it on first launch. The Install section above lists three ways through it; System Settings → Privacy & Security → Open Anyway is the one Apple intends.
Q: How do I open a .json file?
You don't — that is the design. JSONBuddy has no Open, no Save and no file association, so it can never modify or damage anything on disk. Copy the JSON and paste it into a tab.
Q: If it never saves, where does my text go when I quit?
Into the app's own container at
~/Library/Application Support/JSONBuddy/, and it comes back on relaunch along with your tabs and folds. Reading position is not restored: every tab reopens at the top of the document.
Q: Does it send anything anywhere?
No. The app makes no network requests at all — no telemetry, no update check, no crash reporting. You can confirm it with
nettopor Little Snitch.
Q: Why is there no Intel build?
The project targets arm64 only, and
Scripts/build-app.shfails the build if any other slice appears. A Universal binary would double the download for a platform the project does not aim to support.
MIT.



