JSON Validator — Python source
Validate JSON and pinpoint errors with line and column numbers. Clear valid/invalid verdict plus the exact error location - runs entirely in your browser.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""json-validator — Python polyglot showcase port.
Language: Python
CosmoDev polyglot showcase port of the json-validator tool.
Ported from src/lib/jsonValidate.ts (canonical TypeScript logic).
This is display source — part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
Pure JSON validation logic. Uses the standard-library json module. Python's
JSONDecodeError carries a 0-based char offset (.pos), so position recovery is
exact here: we feed .pos through the shared offset_to_line_col algorithm rather
than scanning the message string, giving cross-port-consistent line/column.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from typing import Optional
# Match the "at position N" marker some engines embed in error messages.
_POSITION_RE = re.compile(r"at position (\d+)")
@dataclass
class ValidationResult:
"""Mirrors the TypeScript result shape. None means 'not applicable'."""
valid: bool
error: Optional[str]
line: Optional[int]
column: Optional[int]
def extract_offset(message: str) -> Optional[int]:
"""Extract the 0-based offset from an engine 'at position N' message, or None."""
match = _POSITION_RE.search(message)
return int(match.group(1)) if match else None
def error_message(value: object) -> str:
"""Coerce a caught value into a human-readable message."""
if isinstance(value, BaseException):
# Exceptions expose their primary message as args[0] when present.
return str(value.args[0]) if value.args else str(value)
return str(value)
def normalize_error(message: str) -> str:
"""Strip JavaScriptCore's 'JSON Parse error: ' prefix for consistent display."""
return re.sub(r"^JSON Parse error:\s*", "", message, flags=re.IGNORECASE)
def offset_to_line_col(text: str, offset: int) -> tuple[int, int]:
"""Convert a 0-based char offset into a 1-based (line, column).
Counts '\\n', a lone '\\r', and '\\r\\n' each as a single line break.
"""
line = 1
column = 1
limit = min(offset, len(text))
i = 0
while i < limit:
ch = text[i]
if ch == "\n":
line += 1
column = 1
elif ch == "\r":
line += 1
column = 1
if i + 1 < limit and text[i + 1] == "\n":
i += 1 # treat CRLF as a single break
else:
column += 1
i += 1
return line, column
def locate_error(text: str, message: str) -> tuple[Optional[int], Optional[int]]:
"""Map a parse error message to (line, column) (or (None, None)).
Used for foreign-engine messages that embed 'at position N'.
"""
offset = extract_offset(message)
if offset is None:
return None, None
return offset_to_line_col(text, offset)
def validate_json(text: object) -> ValidationResult:
"""Validate a JSON string. Never raises.
- empty / whitespace-only / non-string -> invalid, 'Input is empty'
- valid JSON -> valid=True
- invalid JSON -> valid=False, cleaned message, and a
1-based line/column derived from
the parser's char offset
"""
if not isinstance(text, str) or not text.strip():
return ValidationResult(False, "Input is empty", None, None)
try:
json.loads(text)
return ValidationResult(True, None, None, None)
except json.JSONDecodeError as exc:
raw = error_message(exc)
# Python reports a precise 0-based char offset; route it through the
# shared line/column algorithm so every port agrees on the location.
line, column = offset_to_line_col(text, exc.pos)
return ValidationResult(False, normalize_error(raw), line, column)
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →