← Back to overview

Universal JSON Reader (PHP)

Render any JSON as tables with CSV export. Robust loader (BOM/^Z), optional root= dot-path, no external deps.

POC PHP Bootstrap 5

What it does

Scans any JSON tree and auto-builds tables for detected “array of objects”. Each table can be downloaded as CSV. A tolerant JSON loader removes UTF-8 BOM and trailing control chars (incl. 0x1A/^Z).

Usage

  • ?file=pedido.json – open a JSON file
  • ?file=pedido.json&root=Mensaje.Body.Pedidos.Detalles – focus a subtree via dot-path
  • Click “Download CSV” on any table to export

Features

  • Auto table detection (array-of-objects)
  • CSV export per table
  • root= dot-path focus
  • UTF-8 BOM / trailing control-char tolerant
  • Zero dependencies besides Bootstrap CSS (for the demo UI)
<?php
declare(strict_types=1);

// Universal JSON Reader (lite) – render JSON as tables + CSV
// Usage examples:
//   universal_tables.php?file=data.json
//   universal_tables.php?file=data.json&root=Mensaje.Body.Pedidos.Detalles

// ---- Load & decode (BOM + trailing control chars tolerant) ----
$file = $_GET['file'] ?? 'data.json';
if (!is_file($file)) { http_response_code(404); exit('File not found: ' . $file); }

$raw = file_get_contents($file);
// strip UTF-8 BOM
$raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw);
// strip trailing control chars (incl. 0x1A/^Z)
$raw = rtrim($raw, "\x00..\x1F\x7F");

$data = json_decode($raw, true, 512, JSON_BIGINT_AS_STRING | JSON_INVALID_UTF8_SUBSTITUTE);
if (json_last_error() !== JSON_ERROR_NONE) exit('JSON error: ' . json_last_error_msg());

// Optional: focus on subtree via dot path
if (!empty($_GET['root'])) {
    $node = pick_dot_path($data, (string)$_GET['root']);
    if ($node !== null) $data = $node;
}

// ---- Build tables ----
$tables = [];
scan_to_tables($data, 'root', [], $tables);

// CSV endpoint
if (isset($_GET['csv'])) {
    $k = (string)$_GET['csv'];
    if (!isset($tables[$k])) exit('Unknown table key');
    [$cols, $rows] = [$tables[$k]['cols'], $tables[$k]['rows']];
    header('Content-Type: text/csv; charset=UTF-8');
    header('Content-Disposition: attachment; filename="'.sanitize($k).'.csv"');
    $out = fopen('php://output', 'w');
    fputcsv($out, $cols);
    foreach ($rows as $r) fputcsv($out, array_map('to_scalar', $r));
    fclose($out);
    exit;
}

// ---- Render minimal HTML ----
?><!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Universal JSON Tables – <?=htmlspecialchars($file)?></title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body{font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;background:#f7f8fb}
.container{max-width:1200px;margin:24px auto 56px;padding:0 16px}
.tbl{margin:18px 0}
.scroll{overflow:auto;border:1px solid #e4e7ef;background:#fff;border-radius:10px}
thead th{position:sticky;top:0;background:#eef2f9}
</style></head><body>
<div class="container">
  <h1 class="h4">Universal JSON Tables</h1>
  <div class="text-muted mb-2">File: <strong><?=htmlspecialchars($file)?></strong> • Tables: <span class="badge bg-secondary"><?=count($tables)?></span></div>

  <?php foreach ($tables as $key => $tab): ?>
    <section class="tbl" id="<?=htmlspecialchars($key)?>">
      <h2 class="h6 d-flex align-items-center gap-2">
        <span><?=htmlspecialchars($key)?></span>
        <a class="btn btn-sm btn-outline-primary" href="?file=<?=rawurlencode($file)?>&csv=<?=rawurlencode($key)?>">Download CSV</a>
        <span class="badge bg-secondary"><?=count($tab['cols'])?> cols</span>
        <span class="badge bg-secondary"><?=count($tab['rows'])?> rows</span>
      </h2>

      <div class="scroll">
        <table class="table table-sm mb-0">
          <thead><tr>
            <?php foreach ($tab['cols'] as $c): ?><th><?=htmlspecialchars($c)?></th><?php endforeach; ?>
          </tr></thead>
          <tbody>
          <?php foreach ($tab['rows'] as $r): ?>
            <tr>
              <?php foreach ($tab['cols'] as $i => $_): $cell = $r[$i] ?? ''; ?>
                <td><?=htmlspecialchars(to_scalar($cell))?></td>
              <?php endforeach; ?>
            </tr>
          <?php endforeach; ?>
          </tbody>
        </table>
      </div>
    </section>
  <?php endforeach; ?>

  <?php if (!$tables): ?>
    <p class="text-muted">No array-of-objects found. Use <code>root</code> to focus on a subtree.</p>
  <?php endif; ?>
</div>
</body></html>
<?php
// ----------------- Helpers -----------------
function scan_to_tables($x, string $path, array $ctx, array &$tables): void {
    if (is_array($x) && is_assoc($x)) {
        foreach ($x as $k => $v) {
            $child = $path.'.'.$k;
            if (is_array($v)) {
                if (is_array_of_objects($v)) build_from_aoo($v, $child, $ctx, $tables);
                else build_from_scalars($v, $child, $ctx, $tables);
            } else {
                $ctx[$child] = $v;
            }
        }
    } elseif (is_array($x)) {
        if (is_array_of_objects($x)) build_from_aoo($x, $path, $ctx, $tables);
        else build_from_scalars($x, $path, $ctx, $tables);
    }
}
function build_from_aoo(array $arr, string $name, array $ctx, array &$tables): void {
    $cols = merge_cols(array_keys($ctx), keys_union($arr));
    $tab = &ensure_table($tables, $name);
    $tab['cols'] = merge_cols($tab['cols'], $cols);
    foreach ($arr as $obj) {
        $row = array_fill(0, count($tab['cols']), '');
        foreach ($tab['cols'] as $i => $c) if (array_key_exists($c, $ctx)) $row[$i] = $ctx[$c];
        if (is_array($obj) && is_assoc($obj)) {
            foreach ($obj as $k => $v) {
                $idx = array_search($k, $tab['cols'], true);
                if ($idx !== false && $row[$idx] === '') $row[$idx] = $v;
            }
        }
        foreach ($row as $i => $v) $row[$i] = to_cell($v);
        $tab['rows'][] = $row;
    }
}
function build_from_scalars(array $arr, string $name, array $ctx, array &$tables): void {
    $tab = &ensure_table($tables, $name);
    $one = $name.'[]';
    $tab['cols'] = merge_cols($tab['cols'], array_merge(array_keys($ctx), [$one]));
    foreach ($arr as $v) {
        $row = array_fill(0, count($tab['cols']), '');
        foreach ($tab['cols'] as $i => $c) if (array_key_exists($c, $ctx)) $row[$i] = to_cell($ctx[$c]);
        $idx = array_search($one, $tab['cols'], true);
        if ($idx !== false) $row[$idx] = to_cell($v);
        $tab['rows'][] = $row;
    }
}
function &ensure_table(array &$tables, string $name): array {
    if (!isset($tables[$name])) $tables[$name] = ['cols'=>[], 'rows'=>[]];
    return $tables[$name];
}
function is_assoc(array $a): bool { foreach (array_keys($a) as $k) if (is_string($k)) return true; return false; }
function is_array_of_objects(array $a): bool { if ($a===[]) return false; foreach ($a as $v) if (is_array($v) && is_assoc($v)) return true; return false; }
function keys_union(array $arr): array { $seen=[]; foreach ($arr as $o) if (is_array($o)&&is_assoc($o)) foreach ($o as $k=>$_) $seen[$k]=1; return array_keys($seen); }
function merge_cols(array $old, array $new): array { $m=[]; foreach ($old as $c) $m[$c]=1; $out=$old; foreach ($new as $c) if (!isset($m[$c])) {$m[$c]=1;$out[]=$c;} return $out; }
function to_cell($v): string {
    if ($v===null) return '';
    if (is_string($v)) return $v;
    if (is_int($v)||is_float($v)) return (string)$v;
    if (is_bool($v)) return $v?'true':'false';
    if (is_array($v)) return json_encode($v, JSON_UNESCAPED_UNICODE);
    return (string)$v;
}
function to_scalar($v): string { return to_cell($v); }
function sanitize(string $s): string { return preg_replace('/[^A-Za-z0-9._-]/','_', $s); }
function pick_dot_path($node, string $dot) {
    foreach (explode('.', $dot) as $seg) {
        if ($seg==='') continue;
        if (is_array($node) && array_key_exists($seg, $node)) $node=$node[$seg]; else return null;
    }
    return $node;
}
?>
{
  "Mensaje": {
    "Body": {
      "Pedidos": {
        "Detalles": [
          { "pedaux": 1, "pedcpr": "0007019", "pedctd": 12 },
          { "pedaux": 2, "pedcpr": "0007020", "pedctd": 8 }
        ]
      }
    }
  }
}