JSON in Bun: Read, Write, Fetch, and SQLite JSON
Last updated:
Bun is a fast JavaScript runtime — approximately 3× faster at JSON.parse than Node.js 20 in benchmarks — with native APIs for reading and writing JSON files, a built-in SQLite driver, and a faster fetch implementation backed by a native Zig HTTP client.
Bun.file("data.json").json() reads and parses JSON in one step — no fs.readFile + JSON.parse chain required. Bun's bun:sqlite module supports JSON columns natively, and SQL functions like JSON_EXTRACT() work without any npm packages because the JSON1 extension is bundled.
This guide covers all Bun JSON APIs: Bun.file, Bun.write, fetch, bun:sqlite JSON columns, and Bun.serve() for JSON REST APIs. All examples are tested on Bun 1.x and use TypeScript, which Bun runs natively without a build step.
Read JSON Files with Bun.file().json()
Bottom line: use Bun.file(path).json() for the shortest, most idiomatic read in Bun. It opens the file, reads all bytes, and parses JSON in a single awaitable call. The returned Promise resolves to the parsed JavaScript value or rejects with a SyntaxError (malformed JSON) or an ENOENT-style system error (file not found).
Under the hood, Bun.file() returns a BunFile object — a lazy handle that does not read the file until you call .json(), .text(), .arrayBuffer(), or .stream(). .text() gives you the raw string if you need to inspect it before parsing, apply a reviver, or validate the shape first. Bun.file() also works with absolute paths, relative paths, and URL objects.
// Shortest form — read + parse in one step
const config = await Bun.file("config.json").json()
// With error handling (ENOENT + SyntaxError)
async function readJson<T>(path: string): Promise<T> {
try {
return await Bun.file(path).json() as T
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`File not found: ${path}`)
}
throw new Error(`Invalid JSON in ${path}: ${(err as SyntaxError).message}`)
}
}
// Use .text() when you need the raw string first
const raw = await Bun.file("data.json").text()
console.log("Bytes:", raw.length)
const data = JSON.parse(raw)
// Read multiple JSON files in parallel
const [users, settings] = await Promise.all([
Bun.file("users.json").json(),
Bun.file("settings.json").json(),
])
// Bun.file also accepts absolute paths and URL objects
const pkg = await Bun.file(new URL("package.json", import.meta.url)).json()Write JSON Files with Bun.write()
Bottom line: Bun.write(path, content) is the Bun-native way to write any file — including JSON. Pass a JSON.stringify-serialized string as the content. Bun.write returns a Promise<number> with the number of bytes written. For human-readable output, use JSON.stringify(data, null, 2).
Bun.write is not atomic by default — if the process crashes mid-write, you may get a partial file. For production data, implement an atomic write pattern: write to a temporary path in the same directory, then rename it. On POSIX filesystems (Linux, macOS), rename is a single atomic operation. Bun exposes fs.rename via Node.js compatibility, or you can use the Bun shell ($) for shell-level operations.
// Basic write — pretty-printed
await Bun.write("output.json", JSON.stringify(data, null, 2))
// Compact write (smaller file, no indentation)
await Bun.write("cache.json", JSON.stringify(data))
// Atomic write pattern (safe on crash)
import { rename } from "node:fs/promises"
import { join, dirname } from "node:path"
async function writeJsonAtomic(path: string, data: unknown) {
const tmp = join(dirname(path), `.${Date.now()}.json.tmp`)
await Bun.write(tmp, JSON.stringify(data, null, 2))
await rename(tmp, path) // atomic on POSIX filesystems
}
// Write a BunFile directly (Bun.file as destination)
await Bun.write(Bun.file("config.json"), JSON.stringify(config, null, 2))
// Append NDJSON line (one JSON object per line)
import { appendFile } from "node:fs/promises"
async function appendNdjson(path: string, record: unknown) {
await appendFile(path, JSON.stringify(record) + "\n")
}Fetch JSON from APIs in Bun
Bottom line: Bun implements the WHATWG Fetch API natively in Zig — the same API as browsers and Node.js 18+, but faster for high-concurrency workloads. The code is identical to browser fetch: await fetch(url) then await res.json().
Always check res.ok before calling res.json(). A 404 or 500 response still has a parseable JSON body (many APIs return JSON error objects), but it is not your expected success payload. res.ok is true only for HTTP status 200–299. For POST requests, set Content-Type: application/json in headers and pass JSON.stringify(payload) as the body. Bun also supports fetch over Unix sockets via the unix option in RequestInit, useful for communicating with Docker daemons or local sockets.
// GET JSON — always check res.ok before res.json()
const res = await fetch("https://api.example.com/users/1")
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
const user = await res.json() as User
// POST with JSON body
const created = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", role: "admin" }),
})
if (!created.ok) throw new Error(`HTTP ${created.status}`)
const newUser = await created.json() as User
// With timeout (AbortController — same API as browsers)
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
try {
const res = await fetch("https://api.example.com/slow", {
signal: controller.signal,
})
const data = await res.json()
} finally {
clearTimeout(timer)
}
// Bun-specific: fetch over a Unix socket
const res = await fetch("http://localhost/info", {
// @ts-ignore — Bun-specific extension
unix: "/var/run/docker.sock",
})
const info = await res.json()SQLite JSON with bun:sqlite
Bottom line: bun:sqlite is a built-in, zero-dependency SQLite driver with the JSON1 extension enabled by default. Store JSON as a TEXT column, stringify before insert, and use JSON_EXTRACT() or json_each() directly in SQL queries — no ORM or npm package needed.
The Database class from bun:sqlite uses a synchronous API (prepared statements, .run(), .query().all()) which is idiomatic for SQLite since it is embedded in-process. For type safety, db.query<RowType, ParamType>(sql) is generic. json_each(column) is a table-valued function that expands a JSON array stored in a column into individual rows — useful for filtering by array element or counting array items.
import { Database } from "bun:sqlite"
const db = new Database("app.db")
// Create table with a JSON column (stored as TEXT)
db.run(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`)
// Insert JSON — stringify before storing
const insert = db.prepare("INSERT INTO events (data) VALUES (?)")
insert.run(JSON.stringify({ type: "login", userId: 42, tags: ["web", "mobile"] }))
// Query with JSON_EXTRACT — returns a scalar value
const query = db.query<{ type: string }, []>(
"SELECT JSON_EXTRACT(data, '$.type') AS type FROM events"
)
const rows = query.all() // [{ type: "login" }]
// json_each — expand a JSON array column into rows
const tagQuery = db.query<{ tag: string }, []>(`
SELECT value AS tag
FROM events, json_each(JSON_EXTRACT(data, '$.tags'))
WHERE JSON_EXTRACT(data, '$.userId') = 42
`)
const tags = tagQuery.all() // [{ tag: "web" }, { tag: "mobile" }]
// json_set — update a field inside a JSON column
db.run(`
UPDATE events
SET data = json_set(data, '$.processed', true)
WHERE JSON_EXTRACT(data, '$.type') = 'login'
`)
db.close()Serve a JSON API with Bun.serve()
Bottom line: Bun.serve({ fetch(req) { ... } } starts an HTTP server with a single handler function. Use Response.json(data) (a static convenience method) to return JSON — it sets Content-Type: application/json automatically and serializes the value.
Route by inspecting new URL(req.url).pathname and req.method. For CORS, add Access-Control-Allow-Origin to each response or handle OPTIONS preflight requests explicitly. Bun.serve supports TLS via the tls option, Unix socket listening via unix, and WebSocket upgrades via websocket. For production APIs, use a framework like Hono or Elysia (both Bun-native) once routing complexity grows beyond a handful of endpoints.
const PORT = 3000
Bun.serve({
port: PORT,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url)
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders })
}
// GET /api/users
if (url.pathname === "/api/users" && req.method === "GET") {
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
return Response.json(users, { headers: corsHeaders })
}
// POST /api/users
if (url.pathname === "/api/users" && req.method === "POST") {
try {
const body = await req.json() as { name: string }
if (!body.name) {
return Response.json({ error: "name is required" }, {
status: 400,
headers: corsHeaders,
})
}
return Response.json({ id: Date.now(), name: body.name }, {
status: 201,
headers: corsHeaders,
})
} catch {
return Response.json({ error: "Invalid JSON body" }, {
status: 400,
headers: corsHeaders,
})
}
}
return Response.json({ error: "Not found" }, {
status: 404,
headers: corsHeaders,
})
},
})
console.log(`Listening on http://localhost:${PORT}`)bunfig.toml vs package.json
Bottom line: package.json handles standard npm fields (name, version, scripts, dependencies, exports); bunfig.toml handles Bun-specific runtime configuration. Both files coexist in the project root — bunfig.toml is optional and does not replace package.json.
Common bunfig.toml use cases: setting a private npm registry with scoped access ([install.scopes]), configuring test runner defaults like timeout and coverage thresholds ([test]), enabling smol mode to reduce heap memory usage on constrained environments ([run]), and defining import condition overrides ([conditions]). Workspaces are still declared in package.json under "workspaces", but bunfig.toml can override peer dependency resolution behavior per workspace. JSON import maps (for aliasing bare specifiers to URLs) are declared in package.json under "imports" — Bun reads these natively.
# bunfig.toml — Bun-specific config (TOML format)
[install]
# Default registry (overrides npm)
registry = "https://registry.npmjs.org"
[install.scopes]
# Private registry for @myorg scoped packages
myorg = { url = "https://npm.myorg.com", token = "$MY_NPM_TOKEN" }
[test]
# Test runner defaults
timeout = 10000 # 10-second per-test timeout
coverage = true # enable coverage output
coverageThreshold = 80 # fail if coverage drops below 80 %
[run]
# Lower memory usage on constrained servers
smol = true
# package.json — standard fields Bun also reads
# {
# "name": "my-app",
# "imports": {
# "#config": "./src/config.ts" // import map — works in Bun natively
# },
# "workspaces": ["packages/*"],
# "scripts": {
# "dev": "bun run src/index.ts",
# "test": "bun test"
# }
# }Bun JSON Performance: Benchmarks vs Node.js and Deno
Bottom line:Bun's JSON.parse is materially faster than Node.js and Deno for medium-to-large payloads due to SIMD-accelerated parsing in JavaScriptCore (JSC). For tiny payloads (<1 KB), the difference is under 1 ms and rarely matters. For payloads above 10 KB, the throughput advantage compounds.
Bun's edge comes from two factors: JSC's SIMD-accelerated string scanning (which processes 16 bytes per CPU instruction on modern x86-64 and ARM64 hardware) and its faster string internalization for repeated keys. In independent benchmarks, Bun parses a 100 KB JSON payload approximately 3–4× faster than Node.js 20 (V8). Deno also uses V8, so it has similar performance to Node.js on JSON parsing. In Bun.file().json(), the file I/O itself is also faster than Node.js's fs.readFile because Bun uses synchronous I/O primitives on the native thread before returning to JavaScript.
When does it matter? Primarily in: (1) services that parse many small-to-medium JSON payloads at high request volume, (2) ETL pipelines that process large JSON files inline rather than streaming, and (3) edge functions where startup time and per-invocation CPU time are metered. For interactive CLI tools or low-traffic servers, the difference is invisible. For streaming very large files (>50 MB), use a streaming parser in any runtime — the all-at-once approach blocks regardless of engine speed. See the JSON performance benchmarks guide for detailed numbers.
// Benchmark: JSON.parse throughput in Bun vs Node.js
// Run with: bun bench.ts (or node bench.mjs for Node.js)
import { bench, run } from "mitata"
const small = JSON.stringify({ id: 1, name: "Alice", active: true }) // ~40 bytes
const medium = JSON.stringify(Array.from({ length: 100 }, (_, i) => ({
id: i, name: `user-${i}`, tags: ["a", "b", "c"], score: Math.random()
}))) // ~5 KB
bench("JSON.parse small (~40 B)", () => JSON.parse(small))
bench("JSON.parse medium (~5 KB)", () => JSON.parse(medium))
await run()
// Typical results on Apple M2:
// Bun 1.x: small= ~45 ns/iter medium= ~2.1 µs/iter
// Node 20: small= ~48 ns/iter medium= ~6.8 µs/iter
// (Bun ~3x faster on the medium payload)Definitions
- Bun runtime
- A fast all-in-one JavaScript runtime built in Zig, using JavaScriptCore (JSC) instead of V8. Bun is designed as a drop-in Node.js replacement with native TypeScript support, a built-in bundler, test runner, and package manager.
Bun.file(path)- Returns a lazy
BunFilehandle to a file on disk. The file is not read until you call.json(),.text(),.arrayBuffer(), or.stream(). Accepts string paths, absolute paths, orURLobjects. bun:sqlite- Bun's built-in SQLite driver module, imported as
import { Database } from "bun:sqlite". It ships with SQLite's JSON1 extension enabled, supports prepared statements, and uses a synchronous API consistent with SQLite's embedded, in-process nature. Bun.serve(options)- Starts a native HTTP server. The
fetchhandler receives a WHATWGRequestand must return aResponse. Supports TLS, WebSocket upgrades, Unix socket listening, andResponse.json()for JSON responses. - SIMD parsing
- Single Instruction, Multiple Data — a CPU technique that applies one operation to multiple data elements simultaneously. JavaScriptCore uses SIMD instructions (SSE4.2 on x86-64, NEON on ARM64) to scan JSON strings 16 bytes at a time, making JSON.parse significantly faster than scalar byte-by-byte approaches.
bunfig.toml- Bun's own TOML-format configuration file for runtime-specific settings: registry overrides, test runner configuration, memory mode, and import conditions. It complements (and does not replace)
package.json. - JSONReader (BunFile)
- Informal term for the
BunFileobject returned byBun.file(). Its.json()method is the Bun-idiomatic way to read and parse a JSON file in one async step.
FAQ
How do I read a JSON file in Bun?
Use await Bun.file("data.json").json(). This reads the file and parses JSON in one step — no separate fs.readFile call needed. Wrap in try/catch to handle ENOENT (file missing) and SyntaxError (malformed JSON). For the raw string, use await Bun.file("data.json").text() then JSON.parse(text).
How do I write a JSON file in Bun?
Use await Bun.write("output.json", JSON.stringify(data, null, 2)). Bun.write accepts a path string or a Bun.file() reference as the destination. Pass null, 2 to JSON.stringify for pretty-printed output. For atomic writes that survive a crash mid-write, write to a temp path in the same directory then rename it.
Is Bun's JSON.parse faster than Node.js?
Yes — approximately 3× faster on medium payloads (~5 KB) due to SIMD-accelerated parsing in JavaScriptCore. For tiny payloads under 1 KB the absolute difference is under 1 ms and rarely matters in practice. The same JSON.parse global is used — no code changes needed to benefit. See the JSON performance benchmarks guide for detailed numbers.
How do I fetch JSON from an API in Bun?
Bun implements the standard WHATWG Fetch API. Use const res = await fetch(url), check res.ok, then const data = await res.json(). For POST requests, set headers: { 'Content-Type': 'application/json' } and body: JSON.stringify(payload). The code is identical to browser fetch and Node.js 18+ fetch. See fetch JSON in JavaScript for a full fetch guide.
How do I use SQLite with JSON in Bun?
Import { Database } from "bun:sqlite" — no npm install needed. Store JSON as a TEXT column by inserting JSON.stringify(obj). The JSON1 extension is enabled by default, so JSON_EXTRACT(data, '$.field'), json_each(data), and json_set(data, '$.field', value) work out of the box. See SQLite JSON functions for a full reference.
Can I serve a JSON REST API with Bun without a framework?
Yes. Bun.serve({ fetch(req) { return Response.json(data) } } ) is a complete HTTP server. Use new URL(req.url).pathname and req.method to route requests. Response.json(data) is a static helper that sets Content-Type: application/json automatically. For multi-route APIs, consider Hono or Elysia — both are Bun-native and add negligible overhead.
How do I validate JSON in Bun?
Bun has no built-in JSON schema validator, but it has full npm compatibility. Zod is the most popular choice: import { z } from "zod", define a schema, and call schema.safeParse(data) after parsing. Ajv is also supported. Since Bun runs TypeScript natively, you can use TypeScript type guards for lightweight validation without any library. For incoming HTTP request bodies, call await req.json() in a try/catch, then validate the parsed object.
What is bunfig.toml and how does it relate to package.json?
bunfig.toml is Bun's optional TOML config file for Bun-specific settings: private registry overrides ([install.scopes]), test runner timeouts and coverage thresholds ([test]), and memory mode ([run] smol = true). package.json still handles all standard npm fields — name, scripts, dependencies, exports, and workspaces. The two files complement each other and coexist in the project root. You cannot replace package.json with bunfig.toml.
Further reading and primary sources
- Bun Docs — File I/O — Official Bun documentation for Bun.file(), Bun.write(), and the BunFile API including streaming and ArrayBuffer support
- Bun Docs — SQLite (bun:sqlite) — Official reference for Bun's built-in SQLite driver, prepared statements, and JSON1 extension support
- Bun Docs — HTTP server (Bun.serve) — Official Bun HTTP server documentation covering fetch handler, TLS, WebSockets, and Response.json()
- JavaScriptCore — SIMD-accelerated JSON parsing — WebKit engineering blog post on SIMD optimizations in JSC that underpin Bun's faster JSON.parse throughput
- SQLite JSON1 Extension docs — Official SQLite reference for JSON1 functions: JSON_EXTRACT, json_each, json_set, json_array, and json_object