Skip to content

ReadableString values produce invalid JSON when used as non-first object properties #47

@hbl18

Description

@hbl18

Description

When a non-objectMode Readable stream is used as a value in an object passed to JsonStreamStringify, the resulting JSON is invalid. The opening quote (") for the stream content is placed before the object key separator (,"key":) instead of after it.

This was introduced in v3.1.0 (the complete rewrite) and affects all versions from 3.1.0 through 3.1.6.

We faced this issue in a pipeline of our project and used GitHub Copilot to find and fix the Issue.

Minimal Reproduction

const { PassThrough } = require('node:stream');
const { JsonStreamStringify } = require('json-stream-stringify');

async function test() {
  const stream = new PassThrough();
  const obj = { key: 'value', data: stream };
  const jss = new JsonStreamStringify(obj);

  setImmediate(() => {
    stream.write(Buffer.from('hello'));
    stream.end();
  });

  const chunks = [];
  for await (const chunk of jss) {
    chunks.push(chunk.toString());
  }
  const result = chunks.join('');
  console.log('Output:', result);

  try {
    JSON.parse(result);
    console.log('Valid JSON');
  } catch (e) {
    console.log('INVALID JSON:', e.message);
  }
}

test();

Expected output:

Output: {"key":"value","data":"hello"}
Valid JSON

Actual output (v3.1.6):

Output: {"key":"value"","data":hello"}
INVALID JSON: Expected ',' or '}' after property value in JSON at position 14 (line 1 column 15)

Root Cause

The bug is in the _push method in src/JsonStreamStringify.ts:

private _push(data) {
    const out = (this.objectItem ? this.objectItem.write() : '') + data;
    if (this.prePush && out.length) {
      this.buffer += this.prePush;   // ← prePush is placed BEFORE objectItem prefix + data
      this.prePush = undefined;
    }
    this.buffer += out;

When setReadableStringItem sets this.prePush = '"', and then _push is called:

  1. objectItem.write() returns the key prefix (e.g. ,"data":)
  2. These are concatenated into out = ',"data":hello'
  3. prePush (") is appended to buffer before out

Result: buffer += '"' then buffer += ',"data":hello'..."value"","data":hello"

Suggested Fix

Separate the object prefix from data, and insert prePush between them:

private _push(data) {
    const prefix = this.objectItem ? this.objectItem.write() : '';
    if (this.prePush && (prefix.length || data.length)) {
      this.buffer += prefix + this.prePush;
      this.prePush = undefined;
    } else {
      this.buffer += prefix;
    }
    this.buffer += data;

This ensures the key prefix (,"data":) is emitted first, then the opening quote ("), then the stream data.

Notes

  • ReadableString as the first property in an object is unaffected (no objectItem prefix to misorder)
  • ReadableObject values are unaffected (they use _push('[') directly, not prePush)
  • Tested on Node.js v24.x

Affected versions

3.1.0 – 3.1.6 (all versions since the v3.1.0 rewrite)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions