Skip to content

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 →