Skip to content

JSON Validator — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
// json-validator — PHP polyglot showcase port
// Language: PHP
// 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 ext-json (always bundled with PHP since 8.0).
// PHP's json_decode does not report an error offset in JsonException::getMessage,
// so position recovery gracefully returns null line/column — the same fallback
// the TypeScript port uses for engines that omit "at position N". The
// offset_to_line_col helper is still provided for parity and for foreign engines.

declare(strict_types=1);

namespace CosmoDev\JsonValidator;

/**
 * Extract the 0-based offset from an engine message of the form "at position N".
 * PHP's own error messages never include this marker, so this returns null for
 * PHP-produced errors; kept for parity with the other ports.
 */
function extract_offset(string $message): ?int
{
    if (preg_match('/at position (\d+)/', $message, $m) === 1) {
        return (int) $m[1];
    }
    return null;
}

/** Coerce a caught value into a human-readable message. */
function error_message(mixed $value): string
{
    if ($value instanceof \Throwable) {
        return $value->getMessage();
    }
    // Non-throwable: stringify only what is sensibly stringifiable.
    return is_scalar($value) ? (string) $value : '';
}

/**
 * Normalize an engine error message for display.
 * Strips JavaScriptCore's "JSON Parse error: " prefix (case-insensitive).
 */
function normalize_error(string $message): string
{
    return preg_replace('/^JSON Parse error:\s*/i', '', $message);
}

/**
 * Convert a 0-based byte offset into a 1-based [line, column].
 * Counts "\n", a lone "\r", and "\r\n" each as one line break. Columns are
 * measured in multibyte characters, so a UTF-8 rune counts once.
 */
function offset_to_line_col(string $text, int $offset): array
{
    $line = 1;
    $column = 1;
    $len = min($offset, strlen($text));
    for ($i = 0; $i < $len; $i++) {
        $byte = $text[$i];
        // Skip UTF-8 continuation bytes so multibyte runes count as one column.
        if ((ord($byte) & 0xC0) === 0x80) {
            continue;
        }
        if ($byte === "\n") {
            $line++;
            $column = 1;
        } elseif ($byte === "\r") {
            $line++;
            $column = 1;
            if (($i + 1) < $len && $text[$i + 1] === "\n") {
                $i++; // treat CRLF as a single break
            }
        } else {
            $column++;
        }
    }
    return [$line, $column];
}

/** Map a parse error message to ['line' => ?, 'column' => ?] (or nulls) using the input text. */
function locate_error(string $text, string $message): array
{
    $offset = extract_offset($message);
    if ($offset === null) {
        return ['line' => null, 'column' => null];
    }
    [$line, $column] = offset_to_line_col($text, $offset);
    return ['line' => $line, 'column' => $column];
}

/**
 * Validate a JSON string. Never throws.
 *  - empty / whitespace-only / non-string → invalid, "Input is empty"
 *  - valid JSON                          → valid: true
 *  - invalid JSON                        → valid: false, cleaned message,
 *                                         null line/column (PHP reports no offset)
 */
function validate_json(mixed $text): array
{
    if (!is_string($text) || trim($text) === '') {
        return ['valid' => false, 'error' => 'Input is empty', 'line' => null, 'column' => null];
    }

    try {
        // depth 512 mirrors PHP's documented default stack depth. The decoded
        // value is discarded — we only care about well-formedness.
        json_decode($text, true, 512, JSON_THROW_ON_ERROR);
        return ['valid' => true, 'error' => null, 'line' => null, 'column' => null];
    } catch (\JsonException $e) {
        $raw = error_message($e);
        return array_merge(
            ['valid' => false, 'error' => normalize_error($raw)],
            locate_error($text, $raw),
        );
    }
}

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 →