JSON Validator Implementation: From Syntax Parsing to Error Location#

When debugging API responses, the most frustrating thing is JSON format errors. The console throws Unexpected token with just a character position—you have to count characters one by one to find the issue. Let’s implement a JSON validator that pinpoints errors to exact line and column numbers.

The Problem with JSON.parse Errors#

JSON.parse is a native browser method with excellent performance, but its error messages aren’t user-friendly:

const invalidJson = '{"name": "test", "age": 25,}'
JSON.parse(invalidJson)
// SyntaxError: Unexpected token } in JSON at position 26

position 26 means the 26th character from the string’s beginning. What users need is line and column numbers.

Position to Line/Column Algorithm#

The core idea: extract all characters before the error position, then split by newlines.

function getLineAndColumn(input: string, position: number) {
  // Get content before error position
  const beforeError = input.substring(0, position)
  // Split by newlines into array
  const lines = beforeError.split('\n')
  // Line number = number of lines
  const line = lines.length
  // Column number = last line's length + 1
  const column = lines[lines.length - 1].length + 1
  return { line, column }
}

Time complexity is O(n) where n is the number of characters before the error. In practice, JSON files aren’t typically huge, so this performs well.

Complete Validation Implementation#

Combine the two parts:

interface ValidationResult {
  valid: boolean
  message: string
  line?: number
  column?: number
}

function validateJson(input: string): ValidationResult {
  try {
    JSON.parse(input)
    return { valid: true, message: 'JSON format is correct' }
  } catch (e) {
    const error = e as Error
    // Extract position from error message
    const match = error.message.match(/at position (\d+)/)

    if (match) {
      const position = parseInt(match[1])
      const { line, column } = getLineAndColumn(input, position)

      // Friendly error messages
      let errorMessage = error.message
      if (error.message.includes('Unexpected token')) {
        errorMessage = 'Syntax error: Unexpected token'
      } else if (error.message.includes('Expected')) {
        errorMessage = 'Syntax error: Missing required token'
      } else if (error.message.includes('Unterminated')) {
        errorMessage = 'Syntax error: Unterminated string'
      }

      return {
        valid: false,
        message: errorMessage,
        line,
        column,
      }
    }

    return { valid: false, message: error.message }
  }
}

Common JSON Errors#

In practice, these errors are most common:

1. Trailing Comma#

{
  "name": "test",
  "age": 25,  // ← Comma after last property
}

JavaScript objects allow trailing commas, but JSON standard doesn’t. Error: Unexpected token }.

2. Missing Quotes#

{
  name: "test"  // ← Property names must use double quotes
}

JSON keys must be wrapped in double quotes—single quotes won’t work either. Error: Unexpected token n.

3. Unclosed Strings or Brackets#

{
  "name": "test  // ← Missing closing quote
}

This type of error causes the parser to keep scanning, potentially reporting an error far from the actual issue.

Performance Optimization: Real-time Validation#

For real-time validation during user input, use debounce to avoid frequent parsing:

import { useMemo, useCallback } from 'react'
import { debounce } from 'lodash-es'

function JsonValidator() {
  const [input, setInput] = useState('')
  const [result, setResult] = useState<ValidationResult | null>(null)

  // 300ms delayed validation
  const debouncedValidate = useMemo(
    () => debounce((value: string) => {
      if (value.trim()) {
        setResult(validateJson(value))
      }
    }, 300),
    []
  )

  const handleChange = useCallback((value: string) => {
    setInput(value)
    debouncedValidate(value)
  }, [debouncedValidate])

  return (
    <textarea value={input} onChange={e => handleChange(e.target.value)} />
  )
}

Handling Large Files#

When JSON files exceed several MB, parsing blocks the main thread. Use Web Worker for background processing:

// worker.ts
self.onmessage = (e) => {
  try {
    JSON.parse(e.data)
    self.postMessage({ valid: true })
  } catch (error) {
    const match = error.message.match(/at position (\d+)/)
    if (match) {
      const position = parseInt(match[1])
      const { line, column } = getLineAndColumn(e.data, position)
      self.postMessage({
        valid: false,
        message: error.message,
        line,
        column
      })
    } else {
      self.postMessage({ valid: false, message: error.message })
    }
  }
}

Main thread communication:

const worker = new Worker('worker.ts')

function validateLargeJson(json: string): Promise<ValidationResult> {
  return new Promise((resolve) => {
    worker.onmessage = (e) => resolve(e.data)
    worker.postMessage(json)
  })
}

Result#

Based on the above approach, I implemented an online JSON Validator:

  • Precise error location to line and column
  • Real-time validation with 300ms debounce
  • User-friendly error messages
  • Supports files up to 10MB

The code isn’t extensive, but getting the user experience right requires attention to these details. A JSON validator is an essential tool in every developer’s toolkit. Hope this helps!


Related Tools: JSON Formatter | JSON Diff