Deno JSON: Read, Parse, and Validate JSON Files in Deno

Deno handles JSON natively at every layer — reading files, importing modules, and configuring the runtime itself. Deno.readTextFile() reads a JSON file as a string, then JSON.parse() converts it to a JavaScript object, identical to Node.js. Deno's unique addition is the native JSON import: import data from "./config.json" with { type: "json" }, which statically imports a JSON file as a typed module without any fs or readFile calls. Deno 2, released in October 2024, uses deno.json (or deno.jsonc) as its project configuration file — replacing package.json for tasks, import maps, and lint rules. The deno.json file supports JSON with Comments (JSONC) syntax for inline documentation. Deno's permission model means file reads require --allow-read unless running with --allow-all. This guide covers file reading, import assertions, deno.json configuration, import maps, and working with JSON in Deno's standard library.

Need to validate or pretty-print a JSON config or data file from your Deno project? Jsonic's formatter handles it instantly.

Open JSON Formatter

Reading JSON Files with Deno.readTextFile

The fastest way to read and parse a JSON file in Deno is a 2-line async operation: const text = await Deno.readTextFile("./data.json") followed by const data = JSON.parse(text). This requires granting read permission via --allow-read=./data.json (scoped) or --allow-read (all files). The async variant is non-blocking and returns a Promise<string>; the sync variant Deno.readTextFileSync() blocks the event loop and should be avoided in servers or any concurrent context. Wrap reads in try/catch to handle Deno.errors.NotFound when the path does not exist.

// Async read + parse (recommended)
const text = await Deno.readTextFile("./data.json")
const data = JSON.parse(text)
console.log(data)

For production code, wrap the operation in a complete async function with error handling. The function below returns a typed result or throws a descriptive error:

// deno run --allow-read=./data.json read-json.ts
async function readJsonFile<T>(path: string): Promise<T> {
  try {
    const text = await Deno.readTextFile(path)
    return JSON.parse(text) as T
  } catch (err) {
    if (err instanceof Deno.errors.NotFound) {
      throw new Error(`File not found: ${path}`)
    }
    if (err instanceof SyntaxError) {
      throw new Error(`Invalid JSON in ${path}: ${err.message}`)
    }
    throw err
  }
}

// Usage
type Config = { host: string; port: number; debug: boolean }
const config = await readJsonFile<Config>("./config.json")
console.log(`Connecting to ${config.host}:${config.port}`)

// Sync variant — blocks event loop, avoid in servers
const raw = Deno.readTextFileSync("./data.json")
const syncData = JSON.parse(raw)

3 key points: (1) scope --allow-read to specific paths for least-privilege security; (2) catch both Deno.errors.NotFound and SyntaxError — the file may exist but contain invalid JSON; (3) the sync variant is only appropriate for CLI scripts that run sequentially and do not serve concurrent requests. Use parse JSON in TypeScript for deeper coverage of type-safe JSON parsing patterns.

Native JSON Import Assertions

Deno 1.38+ supports native JSON import assertions: import config from "./config.json" with { type: "json" }. This statically imports a JSON file as a typed ES module — no Deno.readTextFile, no JSON.parse, and no --allow-read flag required. Deno resolves the import at startup as part of the module graph, making it available immediately in the module scope. TypeScript infers the type directly from the JSON shape, giving you full autocomplete on the imported object's properties.

// config.json
// { "host": "localhost", "port": 8080, "debug": true }

// main.ts — deno run main.ts (no --allow-read needed)
import config from "./config.json" with { type: "json" }

// TypeScript infers: { host: string; port: number; debug: boolean }
console.log(config.host)   // "localhost"
console.log(config.port)   // 8080
console.log(config.debug)  // true

Static analysis: Deno resolves import assertions at startup, which means the file is tree-shaken with the module graph. The JSON content is bundled into the module cache and does not require a separate file read at runtime. This is ideal for configuration, localization strings, and static data that does not change at runtime.

// ✅ Static path — works with import assertions
import translations from "./i18n/en.json" with { type: "json" }

// ❌ Dynamic path — NOT supported with import assertions
const lang = "en"
// import data from `./i18n/${lang}.json` with { type: "json" }  // SyntaxError

// ✅ Dynamic path — use Deno.readTextFile instead
const langData = JSON.parse(await Deno.readTextFile(`./i18n/${lang}.json`))

The key caveat is that import assertions only work with static paths — string literals known at compile time. For dynamic paths computed at runtime (e.g., from user input, environment variables, or loop variables), fall back to Deno.readTextFile + JSON.parse. See JSON config files for patterns on managing configuration across environments.

deno.json Configuration File

Deno's project configuration lives at ./deno.json or ./deno.jsonc in the project root. It is the single source of truth for tasks, import maps, TypeScript options, linting, formatting, test patterns, and the lock file path. Deno 2 introduced full npm compatibility via deno.json, making it a genuine replacement for package.json in Deno projects. The file supports JSONC (JSON with Comments) syntax for inline documentation regardless of extension.

Key fields: "tasks" defines scripts run via deno task, "imports" is the import map, "compilerOptions" maps to TypeScript's tsconfig.json, "lock" specifies the lock file path, and "nodeModulesDir" controls npm compatibility mode.

// deno.json — complete example with 4 tasks and 3 import aliases
{
  // Tasks (run with: deno task <name>)
  "tasks": {
    "dev":   "deno run --allow-net --allow-read --allow-env src/main.ts --watch",
    "build": "deno run --allow-read --allow-write scripts/build.ts",
    "test":  "deno test --allow-read --allow-env",
    "lint":  "deno lint && deno fmt --check"
  },

  // Import map — alias bare specifiers
  "imports": {
    "std/":    "https://deno.land/std@0.224.0/",
    "zod":     "npm:zod@3",
    "express": "npm:express@4"
  },

  // TypeScript compiler options
  "compilerOptions": {
    "strict": true,
    "lib": ["deno.window"]
  },

  // Lint configuration
  "lint": {
    "rules": { "tags": ["recommended"] },
    "exclude": ["dist/"]
  },

  // Formatting
  "fmt": {
    "indentWidth": 2,
    "singleQuote": true
  },

  // Lock file (pin all dependency versions)
  "lock": "./deno.lock",

  // npm compatibility — create local node_modules
  "nodeModulesDir": "auto"
}

Run deno task build to execute the build task. Run deno task dev to start the development server with file watching. 4 tasks cover the typical development lifecycle: dev, build, test, and lint. The "nodeModulesDir": "auto" setting is required for npm packages that use the file system to locate their own dependencies. Without it, Deno resolves npm packages from its global cache instead of a local node_modules folder. Explore JSON config file patterns for more on managing per-environment config alongside deno.json.

Import Maps in deno.json

The "imports" object in deno.json is an import map that maps bare specifiers to full URLs or npm: specifiers. Import maps centralize dependency version management — there is no separate package.json or node_modules install step for Deno-native dependencies. Every module that imports "zod" resolves to exactly "npm:zod@3", ensuring a single version across the entire project. Deno generates a deno.lock file that pins the exact resolved URLs for reproducible builds.

// deno.json — import map examples
{
  "imports": {
    // npm packages via npm: specifier
    "react":       "npm:react@18",
    "react-dom/":  "npm:react-dom@18/",
    "ajv":         "npm:ajv@8",

    // Deno standard library via URL with trailing slash (directory alias)
    "$std/":       "https://deno.land/std@0.224.0/",

    // Local alias
    "$lib/":       "./src/lib/"
  }
}
// Using the import map aliases in source files
import { z } from "zod"                  // resolves to npm:zod@3
import Ajv from "ajv"                    // resolves to npm:ajv@8
import { join } from "$std/path/mod.ts"  // resolves to std@0.224.0 path module
import { db } from "$lib/database.ts"   // resolves to ./src/lib/database.ts

Use "scopes" for per-directory import overrides — useful when a subdirectory requires a different version of a dependency than the rest of the project:

{
  "imports": {
    "lodash": "npm:lodash@4"
  },
  "scopes": {
    "./legacy/": {
      "lodash": "npm:lodash@3"
    }
  }
}

deno.lock pins the exact resolved content hash of every import, including transitive dependencies, for fully reproducible builds. Commit deno.lock to version control. Run deno cache --reload to refresh all cached dependencies. The import map + lock file combination is the Deno equivalent of package.json + package-lock.json, but with explicit URLs instead of a registry lookup.

JSON Schema Validation with Deno Standard Library

Deno's @std/json (formerly std/json) provides JsonParseStream for streaming JSON — specifically NDJSON (newline-delimited JSON) arrays, where each line is a separate JSON object. For JSON Schema validation, use @std/json-schema from the standard library or import ajv from npm. JsonParseStream pipes from any ReadableStream<string> and emits 1 parsed object per JSON line — ideal for processing large log files, event streams, or API responses without loading everything into memory at once.

// deno run --allow-read stream-ndjson.ts
import { JsonParseStream } from "jsr:@std/json/json-parse-stream"
import { TextLineStream } from "jsr:@std/streams/text-line-stream"

// Process a large NDJSON file line by line
const file = await Deno.open("./events.ndjson", { read: true })

const readable = file.readable
  .pipeThrough(new TextDecoderStream())          // bytes → strings
  .pipeThrough(new TextLineStream())             // strings → lines
  .pipeThrough(new JsonParseStream())            // lines → parsed objects

let count = 0
for await (const event of readable) {
  // event is a parsed JSON object from one line
  console.log(event)
  count++
}
console.log(`Processed ${count} events`)
file.close()

For JSON Schema validation, use the npm: specifier to import Ajv in under 5 lines:

// deno run --allow-read validate-schema.ts
import Ajv from "npm:ajv@8"

const ajv = new Ajv()
const schema = {
  type: "object",
  properties: {
    name:  { type: "string" },
    age:   { type: "number", minimum: 0 },
    email: { type: "string", format: "email" },
  },
  required: ["name", "email"],
}

const validate = ajv.compile(schema)
const data = JSON.parse(await Deno.readTextFile("./user.json"))

if (!validate(data)) {
  console.error("Validation errors:", validate.errors)
} else {
  console.log("Valid:", data)
}

For structured runtime type validation, Deno + Zod validation is a popular alternative to Ajv. Zod infers TypeScript types from schemas automatically, eliminating the need for separate type definitions. Import Zod via "zod": "npm:zod@3" in deno.json imports and use it identically to any Node.js project. For large NDJSON files, see NDJSON streaming for format details and additional processing patterns.

Key Terms and Definitions

Deno.readTextFile()
An async Deno built-in that reads a file at the given path and returns its contents as a UTF-8 string wrapped in a Promise, requiring the --allow-read permission flag at runtime.
JSON import assertion
A static ES module import syntax using with { type: "json" } that instructs the runtime to parse the imported file as JSON and expose it as a typed module, resolved at startup without requiring runtime file-read permissions.
deno.json
Deno's project configuration file that centralizes task definitions, import maps, TypeScript compiler options, lint rules, format rules, test patterns, and the lock file path in a single JSONC-compatible file at the project root.
Import map
A JSON object under the "imports" key in deno.json that maps bare module specifiers (like "react") to full URLs or npm: specifiers, centralizing dependency version management across all source files.
JSONC (JSON with Comments)
A superset of JSON that allows single-line (//) and multi-line (/* */) comments, supported by Deno's configuration file parser for both deno.json and deno.jsonc files.
JsonParseStream
A TransformStream from Deno's @std/json standard library that accepts a stream of text lines and emits one parsed JavaScript object per line, enabling memory-efficient processing of NDJSON (newline-delimited JSON) files.
deno.lock
A lock file automatically generated by Deno that records the exact resolved content hashes of all imported modules and their transitive dependencies, ensuring reproducible builds across different machines and environments.

Frequently asked questions

How do I read a JSON file in Deno?

Read a JSON file in Deno with const data = JSON.parse(await Deno.readTextFile("./file.json")). Run with deno run --allow-read script.ts to grant read permission. For sync reads, Deno.readTextFileSync("./file.json") works but blocks the event loop — avoid it in servers or async contexts. Wrap in try/catch to handle Deno.errors.NotFound when the path does not exist. Deno 2 also supports import file from "./file.json" with { type: "json" } for static imports, which requires no --allow-read flag. Scope the permission to a specific path for least-privilege security: --allow-read=./config.json instead of --allow-read.

What is deno.json and what can I configure in it?

deno.json is Deno's project configuration file, replacing package.json for Deno projects. It configures tasks (deno task, equivalent to npm scripts), import maps ("imports" object for aliasing bare specifiers), TypeScript compiler options, linting rules, formatting rules, test file patterns, and the lock file path. It supports JSONC syntax for comments — use deno.jsonc for explicit editor support, or deno.json (Deno's parser accepts comments in both). In Deno 2, setting "nodeModulesDir": "auto" enables a local node_modules folder for full npm package compatibility. Explore JSON config files for multi-environment configuration patterns.

Do I need --allow-read to import a JSON file with import assertions?

No. Files in the module graph (statically imported) are resolved at startup before the permission check. --allow-read is only required for runtime file reads via Deno.readTextFile(). JSON import assertions — import x from "./f.json" with { type: "json" } — do not require --allow-read because Deno treats them as static module dependencies resolved at load time, not as runtime I/O operations. The key constraint is that import assertions only work with static path literals known at compile time. For dynamic paths computed at runtime, use Deno.readTextFile + JSON.parse, which does require --allow-read.

How do I use npm packages in Deno for JSON processing?

Use the npm: specifier: import Ajv from "npm:ajv@8". No install step is needed — Deno downloads and caches the package automatically. Add it to deno.json imports: "ajv": "npm:ajv@8", then import Ajv from "ajv" throughout your project. Deno 2 has full npm compatibility including node_modules support when "nodeModulesDir": "auto" is set in deno.json. Popular JSON processing packages available via npm:: ajv for JSON Schema validation, zod for runtime type validation, and fast-json-stringify for high-performance serialization. See Zod validation for a full guide on using Zod in Deno.

What is the difference between deno.json and deno.jsonc?

They are functionally identical; deno.jsonc enables JSONC (JSON with Comments) linting and editor support explicitly. deno.json also supports comments in practice — Deno's parser accepts them in both file types — but deno.jsonc makes the intent clear and improves editor tooling for comment highlighting and validation. Both work as Deno config files, are discovered automatically by the Deno runtime in the project root, and support the same configuration keys: tasks, imports, lint, fmt, test, compilerOptions, lock, and nodeModulesDir. Choose deno.jsonc when you want to add explanatory comments to your configuration.

How do I write JSON to a file in Deno?

Write JSON to a file with await Deno.writeTextFile("output.json", JSON.stringify(data, null, 2)). Requires --allow-write. The third argument to JSON.stringify (2) adds 2-space indentation for readable output. For atomic writes, write to a temporary file and rename with Deno.rename()— this avoids partial writes on process crash. To append instead of overwrite, pass { append: true } as the third argument to writeTextFile. The sync variant Deno.writeTextFileSync() is available but blocks the event loop. Scope the permission: --allow-write=./output.json is safer than --allow-write.

Ready to work with JSON in Deno?

Use Jsonic's JSON Formatter to validate and pretty-print JSON files from your Deno project. You can also diff two JSON responses to compare config changes across environments.

Open JSON Formatter