← Back to Tech Journal
Developer ToolsbeginnerEditorialOct 20, 2026 · 11 min read

Build a JSON Validator in TypeScript: From Parser Errors to Typed Data

A blueprint-first tutorial: validate JSON at the boundary, return typed results instead of throwing, and verify behaviour with small reproducible tests — no extra dependencies.

By SmokyCoders Editorial · Engineering

Editorial Engine sample. Core parse/format logic smoke-tested in Node; browser UI integration not tested.

Professional cover: glowing JSON braces and typed data panels in neon green and violet on a dark background

Prerequisites

  • Basic TypeScript (types, functions, generics helpful but not required)
  • Node.js 20+ or a modern browser console to try the examples
  • Familiarity with JSON objects, arrays, strings, numbers, booleans and null

What you will learn

  • Explain the difference between JSON syntax validation and shape (schema) validation
  • Build a validateJson function that returns a typed result instead of throwing
  • Turn parser failures into actionable, user-safe error messages
  • Verify the validator with a small table of passing and failing inputs

Key takeaways

  • Parse once at the trust boundary, then pass typed data inward.
  • Syntax-valid JSON is not automatically the shape your code needs.
  • Return { ok: true, value } | { ok: false, error } so callers must handle failure.
  • Never log full untrusted payloads — log shape and position, not secrets.

Your app receives JSON from an API, a file, or a text box — and somewhere deep inside, JSON.parse throws, a field is undefined, or a number arrives as a string. This tutorial builds one small, reliable habit: validate at the boundary and hand the rest of your code typed, trustworthy data.

Two different jobs share the word “validation”. Syntax validation asks: is this valid JSON grammar? Shape validation asks: does this value have the keys and types I need? JSON.parse does only the first.

Blueprint: what we are building

Data flow — untrusted text to typed value
errorparsedmismatchmatch
Untrusted text
JSON.parse
Safe error result
Shape checks
Typed value for your code

Blueprint — har box ek component hai, har arrow data ka flow. Red path = failure result, green path = aage ka flow.

ComponentResponsibilityFailure it prevents
parseJsonSyntax only: text → unknownCrashes from malformed JSON
isRecord / guardsRuntime shape checksundefined field access
validateUser (example)Domain shape: required keys + typesBad data reaching business logic
Result typeForces callers to handle failureIgnored errors
Components and responsibilities

Project shape

text
json-validator/
  src/
    validate.ts   # parse + result type (this article)
    user.ts       # example shape guard for a User payload
  test/
    validate.test.ts  # table-driven checks (manual or your runner)
  package.json
  tsconfig.json

Prerequisites: Node.js 20+ (or a browser console) and TypeScript basics. No runtime dependencies — JSON.parse and JSON.stringify are built into every modern JavaScript engine (RFC 8259 defines the JSON grammar they implement).

Step 1 — A result type, not a throw

A discriminated union forces the caller to check ok before touching value. That single check eliminates a whole class of “forgot to handle the error” bugs.

typescript
export type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

export function parseJson(text: string): Result<unknown> {
  const trimmed = text.trim();
  if (!trimmed) return { ok: false, error: "Enter JSON to validate." };
  try {
    return { ok: true, value: JSON.parse(trimmed) as unknown };
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "Invalid JSON.";
    return { ok: false, error: `Invalid JSON: ${message}` };
  }
}

Line by line: trim rejects empty input early; try/catch converts the SyntaxError that JSON.parse throws into data; `as unknown` is honest — at this point we know it parsed, not what shape it is.

Step 2 — Shape checks with type guards

typescript
export function isRecord(
  value: unknown,
): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export type User = { id: number; name: string };

export function validateUser(value: unknown): Result<User> {
  if (!isRecord(value))
    return { ok: false, error: "Expected a JSON object." };
  if (typeof value.id !== "number" || !Number.isFinite(value.id))
    return { ok: false, error: "Field 'id' must be a finite number." };
  if (typeof value.name !== "string" || !value.name.trim())
    return { ok: false, error: "Field 'name' must be a non-empty string." };
  return { ok: true, value: { id: value.id, name: value.name } };
}

export function parseUserJson(text: string): Result<User> {
  const parsed = parseJson(text);
  if (!parsed.ok) return parsed;
  return validateUser(parsed.value);
}

Security note: return only the fields you validated ({ id, name } above). Passing the original object through lets unexpected or dangerous keys leak into later code.

Step 3 — Verify with a truth table

InputparseUserJson resultWhy
{"id":1,"name":"Ada"}ok: UserSyntax + shape pass
{"id":1,}ok: false (Invalid JSON)Trailing comma is not JSON
{"id":"1","name":"Ada"}ok: false (id must be a number)Syntax ok, shape fails
[]ok: false (Expected a JSON object)Array is not a record
ok: false (Enter JSON…)Empty input rejected early
Expected behaviour — run each row and compare
typescript
const cases: Array<[string, boolean]> = [
  ['{"id":1,"name":"Ada"}', true],
  ['{"id":1,}', false],
  ['{"id":"1","name":"Ada"}', false],
  ["[]", false],
  ["", false],
];

for (const [input, shouldPass] of cases) {
  const result = parseUserJson(input);
  console.log(input || "(empty)", "→", result.ok, result.ok ? result.value : result.error);
  if (result.ok !== shouldPass) throw new Error(`Unexpected result for: ${input}`);
}
console.log("All cases behaved as expected.");

Expected output: five lines ending with “All cases behaved as expected.” Exact parser wording after “Invalid JSON:” varies by engine — assert on ok and your own messages, never on engine text.

Common mistakes and fixes

SymptomLikely causeFix
Works in tests, fails on real API dataReal payload shape differs (nulls, string numbers)Log shape (keys + types), not full payload; extend guards
Cannot read properties of undefinedTrusted parsed JSON without shape checksValidate at the boundary; pass only typed values inward
Error message leaks payload in UI/logsDumped raw input into the errorShow position/shape only; keep raw payloads out of logs
TypeScript says value is any and stays silentJSON.parse result assigned without unknownType it as unknown immediately, then narrow with guards
Troubleshooting matrix

Try it in the Developer Lab

Paste the failing rows from the truth table into the SmokyCoders JSON tool (linked below) and watch syntax errors surface before any shape logic runs. Processing there stays in your browser — a useful model for boundary validation in your own UI.

Conclusion and next steps

You built a dependency-free JSON boundary: parse once, return a result, guard the shape, and verify with a truth table. Next steps: add guards for nested objects, collect multiple field errors instead of returning the first, and — when schemas grow — evaluate a dedicated schema library rather than hand-rolling everything.

Frequently asked questions

Is JSON.parse enough validation?

No. JSON.parse checks syntax only. You still need shape checks (required keys, types, ranges) before trusting the data in business logic.

Should I throw or return a result object?

At a UI or API boundary, returning a result object is usually clearer: callers must handle failure explicitly and you can show a safe message without a stack trace.

Can I validate huge JSON files with JSON.parse?

JSON.parse loads the whole value into memory. For very large or streamed payloads you need a streaming parser — out of scope for this beginner blueprint.

References and further reading