JSON Validator Implementation: From Syntax Parsing to Error Location
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