브라우저에서 대형 Zendesk JSON/NDJSON 내보내기 안전하게 구문 분석

작성자

카테고리:

← 피드로
DEV Community · 石云飞 · 2026-08-03 개발(SW)

石云飞

Zendesk full exports are useful for audits, migrations, and historical review, but they are not always pleasant to inspect.

A file ending in .json may actually be newline-delimited JSON (NDJSON): one complete object per line rather than one large JSON document. Exports can also be split across files, wrap tickets in different container shapes, or reference attachments that are not present in the archive.

I recently built a browser-local viewer for this workflow. This article covers the parsing decisions that mattered most, including the failure modes I would handle before trusting an export.

1. Do not assume .json means one JSON document

The first useful distinction is between standard JSON and NDJSON.

For a small file, a practical strategy is:

  1. Try to parse the complete text as JSON.
  2. If that fails, read non-empty lines individually.
  3. Keep a count of malformed lines instead of discarding the whole file.
function parseJsonOrNdjson(text, ingest) {
  const trimmed = text.trim();

  try {
    ingest(JSON.parse(trimmed));
    return;
  } catch {
    // Fall back to one JSON record per line.
  }

  let parsed = 0;
  let malformed = 0;

  for (const line of trimmed.split(/\r?\n/)) {
    if (!line.trim()) continue;

    try {
      ingest(JSON.parse(line));
      parsed += 1;
    } catch {
      malformed += 1;
    }
  }

  if (parsed === 0) throw new Error("Not valid JSON or NDJSON");
  return { parsed, malformed };
}

Enter fullscreen mode Exit fullscreen mode

This fallback is useful for ZIP entries that have already been decompressed into memory. For a large raw NDJSON file, however, reading the complete file first defeats the point.

2. Stream raw NDJSON instead of loading it all at once

The browser File API exposes a readable stream. A TextDecoder can preserve an incomplete final line between chunks:

async function readNdjson(file, onRecord) {
  const reader = file.stream().getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split(/\r?\n/);
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (!line.trim()) continue;
      onRecord(JSON.parse(line));
    }
  }

  buffer += decoder.decode();
  if (buffer.trim()) onRecord(JSON.parse(buffer));
}

Enter fullscreen mode Exit fullscreen mode

Production code should catch errors per line and report the source filename and malformed-record count. One bad record should not make every valid ticket after it invisible.

3. Move parsing off the main thread

Parsing, decompression, normalization, and search-index construction can block rendering. A module Web Worker keeps the interface responsive and provides a clean cancellation boundary:

const worker = new Worker(new URL("./parser.worker.js", import.meta.url), {
  type: "module",
});

worker.postMessage({ files });

worker.onmessage = ({ data }) => {
  if (data.type === "progress") updateProgress(data.progress);
  if (data.type === "complete") renderTickets(data.result);
};

const cancel = () => worker.terminate();

Enter fullscreen mode Exit fullscreen mode

The worker should return normalized data and warnings rather than mutate UI state directly.

4. Normalize common ticket shapes without hiding uncertainty

Not every export has the same outer shape. A parser may encounter:

  • a raw ticket object;
  • { "ticket": { ... } };
  • an array of tickets;
  • a container such as { "tickets": [...] }, { "results": [...] }, or { "data": [...] }.

Normalize the fields needed for browsing, but keep the source filename and make unsupported records visible as warnings.

For conversations, preserve at least:

  • comment ID and timestamp;
  • author ID or email when present;
  • original order;
  • the public flag, so internal notes are not presented as public replies.

If an export reports a comment count but includes no comments, the UI should show that the conversation may be incomplete. A clean-looking empty conversation is more dangerous than an explicit warning.

5. Merge duplicate tickets conservatively

Multiple selected files or overlapping exports can contain the same ticket ID more than once.

Blindly keeping the last record can replace a complete conversation with a thinner representation. The approach I used prefers the version with the richer conversation, merges tags, keeps stronger identity fields, and preserves an incomplete-data warning from either record.

This is still a heuristic. For migration or compliance work, conflicting records should remain reviewable rather than being treated as automatically resolved truth.

6. Put hard limits around ZIP processing

ZIP archives introduce a separate risk: the compressed size can be small while the declared uncompressed size is very large.

The preview I built currently applies two explicit limits:

  • 75 MB maximum compressed ZIP size;
  • 300 MB maximum declared uncompressed JSON/NDJSON data.

Larger archives are not necessarily invalid. The safer workflow is to extract them first and open the raw NDJSON files through the streaming path.

Attachments are another boundary. A ticket may reference an attachment URL without including the attachment itself. A local viewer should not automatically fetch those URLs, both for privacy and because they may require authenticated access.

7. Treat local processing as a verifiable boundary

Ticket exports can contain customer details, internal notes, and operational history. “Runs in the browser” is only meaningful if the application does not quietly send the data elsewhere.

For this project, that meant:

  • no account system, application API, or database;
  • parsing inside a Web Worker;
  • no telemetry client;
  • a production Content Security Policy that blocks browser connection requests while allowing same-origin assets and the parser worker.

Users should still review the implementation and network behavior before relying on any tool with sensitive data.

A working preview

Disclosure: I am the maker of DeskLens, a free independent preview that implements the workflow described above. It opens Zendesk ZIP, JSON, and NDJSON exports, lets you search tickets and read public/internal comments, and exports selected rows to CSV. Ticket contents stay in the browser tab.

You can try the built-in sample before opening a file:

https://desklens-viewer.whitesir520.workers.dev/

Current limits: attachments are not imported, ZIP limits are intentional, and the preview does not claim to support every historical export variant. Results should be reviewed before compliance or migration use. DeskLens is not affiliated with Zendesk.

I would be especially interested in examples of export shapes that break otherwise reasonable parsers: split archives, custom-field variants, missing comments, or something else.

Editorial disclosure: AI assistance was used to organize and edit this article. The technical claims and examples were checked against the working implementation and its tests.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다