Skip to content

Validating untrusted JSON ​

When the JSON you're about to feed into fromJSON / importJSON came from anywhere outside your codebase — a user upload, an API you don't control, a saved file, a clipboard paste — treat it as hostile. flowgl ships a schema validator that catches every public-API-violating shape before it reaches the chart's mutation path.

What the validator catches ​

fromJSON(data) and importJSON(data, mode?) run validateChartJson internally. It rejects:

  • Missing required fields (id, x, y, width, height on nodes; id, source, target on edges)
  • Wrong types (x: "100" instead of 100, etc.)
  • Non-finite numbers (Infinity, NaN)
  • ids that aren't strings, or are empty
  • source / target referring to nodes that don't exist
  • htmlContent containing <script> tags, javascript: URLs, or inline event handlers (onclick=, onload=, etc.)
  • Cycles in parentId chains
  • parentId referencing a non-existent or non-group node

Anything that passes still goes through sanitizeHtml (if you set one) when the HTML eventually renders. The two layers compose.

Default safe usage ​

ts
import { FlowChart } from '@flowgl/core'

try {
  chart.fromJSON(untrusted)
} catch (e) {
  console.error('invalid graph payload:', e.message)
  showUserError("That file isn't a valid flowgl graph.")
}

fromJSON throws on validation failure with a descriptive message ("node a is missing required field 'width'", etc.).

When the input is yours — skip the check ​

Round-tripping data you produced yourself (toJSON → some store → fromJSON) doesn't need re-validation. The validator costs a graph walk; for huge graphs that's measurable:

ts
const snap = chart.toJSON()
saveToBackend(snap)
// later
chart.fromJSON(snap, { skipValidation: true })

Only use skipValidation: true on inputs you trust. "Came from our backend" counts only if your backend itself validates on write. "Came from localStorage" doesn't — your own user can edit localStorage in DevTools.

Merging untrusted into the current graph ​

importJSON(data, 'merge') validates by default. It will reject the entire incoming payload if any single node or edge fails — partial merges of partially-invalid payloads are not supported (intentionally; they almost always become bug bait).

ts
try {
  chart.importJSON(untrusted, 'merge')
} catch (e) {
  console.error(e.message)
  // The chart is unchanged. No half-committed merge to undo.
}

Sanitizing htmlContent ​

The validator rejects obviously-bad HTML (script tags, javascript: URLs, on*=). It does not catch every clever vector — escaped fragments, mutation XSS, dangerous attribute values. For an htmlContent field, the validator is necessary but not sufficient. Always pair it with a sanitizeHtml function:

ts
import DOMPurify from 'dompurify'

const chart = new FlowChart({
  container,
  sanitizeHtml: (html) => DOMPurify.sanitize(html),
})
chart.fromJSON(untrustedJsonFromTheNetwork)

See SECURITY.md for the full threat model.

Worked example — file upload flow ​

ts
const fileInput = document.querySelector<HTMLInputElement>('input[type=file]')!
fileInput.addEventListener('change', async () => {
  const file = fileInput.files?.[0]
  if (!file) return

  if (file.size > 5 * 1024 * 1024) {  // 5 MB cap
    showUserError('File too large.')
    return
  }
  let text: string
  try {
    text = await file.text()
  } catch {
    showUserError("Couldn't read that file.")
    return
  }
  let parsed: unknown
  try {
    parsed = JSON.parse(text)
  } catch {
    showUserError("That file isn't JSON.")
    return
  }
  try {
    chart.fromJSON(parsed)
  } catch (e) {
    showUserError((e as Error).message)
  }
})

Four error surfaces, each with a specific user message. Size cap, parse guard, schema validation, and (if you ever turn it on) a sanitizer.

See also ​

Released under the MIT License.