Skip to content

parseSpecStreamLine silently drops malformed JSON with no recovery attempt #196

@arielkark

Description

@arielkark

Problem

parseSpecStreamLine in @json-render/core silently drops any line where JSON.parse fails, with no attempt at recovery:

function parseSpecStreamLine(line) {
  const trimmed = line.trim();
  if (!trimmed || !trimmed.startsWith("{")) return null;
  try {
    const patch = JSON.parse(trimmed);
    if (patch.op && patch.path !== void 0) {
      return patch;
    }
    return null;
  } catch {
    return null;  // ← silently dropped, no recovery
  }
}

Since this package is specifically designed to parse AI-generated JSONL output, it should be resilient to the most common LLM generation error: brace miscounting.

Real-world example

When asking Claude Sonnet 4.6 to generate a dashboard with MetricTile, BarChart, PieChart, and DataTable components (all using dataSource with a nested {sql: "..."} object), 7 out of 12 spec lines were generated with exactly one extra trailing }:

// What the model generated (INVALID — extra closing brace):
{"op":"add","path":"/elements/metric-payments","value":{"type":"MetricTile","props":{"title":"Payments Collected","format":"currency","dataSource":{"sql":"SELECT ..."}}},"children":[]}}

// What it should have generated (VALID):
{"op":"add","path":"/elements/metric-payments","value":{"type":"MetricTile","props":{"title":"Payments Collected","format":"currency","dataSource":{"sql":"SELECT ..."}},"children":[]}}

The pattern is consistent: every element with a dataSource property (which adds an extra level of {} nesting inside props) confuses the model's brace tracking. The children field ends up outside the value object with an extra } appended.

Result: Only the page header card rendered. All 4 metric tiles, both charts, and the data table were silently dropped — making the dashboard appear completely broken with no error feedback.

Suggested fix

Add a simple recovery step when JSON.parse fails — try trimming trailing } characters one at a time:

function parseSpecStreamLine(line) {
  const trimmed = line.trim();
  if (!trimmed || !trimmed.startsWith("{")) return null;
  try {
    const patch = JSON.parse(trimmed);
    if (patch.op && patch.path !== void 0) return patch;
    return null;
  } catch {
    // Recovery: try removing extra trailing braces (common LLM error)
    let attempt = trimmed;
    while (attempt.endsWith("}")) {
      attempt = attempt.slice(0, -1);
      try {
        const patch = JSON.parse(attempt);
        if (patch.op && patch.path !== void 0) return patch;
      } catch {
        continue;
      }
    }
    return null;
  }
}

This would have recovered all 7 broken lines in the example above. A more comprehensive approach could also handle missing trailing braces (try appending }) or other common malformations.

Additional suggestion

Consider emitting a warning or callback when recovery is needed (or when a line is ultimately dropped), so developers can observe generation quality and debug rendering issues more easily. Currently the silent return null makes these failures invisible.

Environment

  • @json-render/core@0.12.0
  • Model: Claude Sonnet 4.6 via Cloudflare AI Gateway
  • Streaming mode with createMixedStreamParser

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions