---
url: https://chartbuddy.io/embed/docs/quality-assurance/validation.md
---
# Validation

Pre-mount checks for `chartData`. For the look-and-export loop, see [Visual QA](/quality-assurance/visual-qa).

ChartBuddy validates chart data at runtime, not just at compile time. TypeScript
types are erased at build time, and chart data almost always arrives at runtime —
parsed JSON, a Sheets range, untyped JSON, or an untyped `setChartData()`.

Two ways in:

* `new Insight()`, `setChartData()`, `setData()`, and `update()` validate their
  input and **throw** `ChartDataValidationError` on an error-severity problem.
* `validateChartData()` **never throws** and returns every problem it found. Use
  it to check a config before you mount one.

The second is what makes a generate → check → repair loop possible without
rendering anything, which matters when configs are generated or assembled at runtime.

## Checking before you mount

```js
import { validateChartData, Insight } from '@chartbuddy.io/embed';

const { valid, errors, warnings } = validateChartData(candidate);

if (!valid) {
  // Branch on `code`, fix the value at `path`.
  for (const issue of errors) {
    console.log(issue.code, issue.path, issue.expected, issue.received);
  }
} else {
  new Insight('#chart', { chartData: candidate });
}
```

## `ValidationResult`

| Field | Type | Meaning |
|---|---|---|
| `valid` | `boolean` | True when there are no **error**-severity issues. Warnings do not invalidate. |
| `issues` | `ValidationIssue[]` | Everything found, errors and warnings, in document order. |
| `errors` | `ValidationIssue[]` | The error-severity subset — what makes `valid` false. |
| `warnings` | `ValidationIssue[]` | Rendered anyway, but probably a bug. |

## `ValidationIssue`

| Field | Type | Always present | Meaning |
|---|---|---|---|
| `path` | `string` | yes | Where the problem is, e.g. `pie.innerRadiusRatio`, `seriesData[2][0]`. Empty string for whole-object problems. |
| `code` | `ValidationIssueCode` | yes | Stable classification. **Branch on this.** |
| `severity` | `'error' \| 'warning'` | yes | Errors reject at the API boundary; warnings are logged. |
| `message` | `string` | yes | For humans. **May be reworded in any release — never parse it.** |
| `expected` | `string` | no | What the schema wanted: `'number'`, `'integer'`, `'>= 0'`, `'2D array'`. |
| `received` | `string` | no | What arrived: `'string'`, `'null'`, `'1.5'`. |
| `allowed` | `string[]` | no | The full legal set, for `not-in-enum` and `unknown-chart-type`. |
| `suggestion` | `string` | no | Nearest legal value, when one is close enough to be a likely typo. |

Optional fields are **omitted**, not set to `undefined`, so issues serialize
cleanly to JSON.

## Issue codes

`code` is the stable contract. New codes may be added in a minor release, so
treat an unrecognized code as a generic failure rather than crashing.

| Code | Severity | Cause | Repair |
|---|---|---|---|
| `not-an-object` | error | The value is not a chart-data object at all. | Pass an object. |
| `unknown-chart-type` | error | `chartType` is not a known type. | Use `suggestion`, or pick from `allowed`. |
| `empty-patch` | error | Neither `chartType` nor `seriesData` supplied. | Include at least one, or set `requireSomething: false`. |
| `wrong-type` | error | A known field holds the wrong JavaScript type. | Coerce to `expected`. Covers `NaN` / `Infinity`. |
| `out-of-range` | error | A numeric field is outside its documented range. | Clamp to the bound in `expected`. |
| `not-in-enum` | error | A string field is outside its legal set. | Use `suggestion`, or pick from `allowed`. |
| `series-data-shape` | error | `seriesData` is not a usable grid for this chart type. | Reshape — `expected` names the row/column minimum. |
| `ragged-series-data` | **warning** | Rows have unequal lengths. | Pad the short rows. The renderer pads for you, but the result is rarely what you meant. |
| `foreign-option-bag` | error | An option bag belongs to a different chart type. | Move the options to the bag in `suggestion`. |

Unknown keys are **never** an issue at any level. Extra fields are ignored.

## Repairing automatically

`suggestion` and `allowed` exist so a fix does not need a second model call:

```js
function repair(chartData) {
  const patched = structuredClone(chartData);
  for (const issue of validateChartData(patched).errors) {
    if (issue.suggestion) setAtPath(patched, issue.path, issue.suggestion);
  }
  return patched;
}
```

`'clusterdBar'` → `suggestion: 'clusteredBar'`; `orientation: 'verical'` →
`suggestion: 'vertical'`.

## Catching the throw

When you skip the pre-check, the API boundary still stops bad data. The thrown
error carries the same structured issues:

```js
import { ChartDataValidationError } from '@chartbuddy.io/embed';

try {
  insight.setChartData(candidate);
} catch (err) {
  if (err instanceof ChartDataValidationError) {
    console.log(err.errors);   // error-severity issues
    console.log(err.warnings); // logged, did not cause the throw
    console.log(err.issues);   // both
    JSON.stringify(err);       // { name, message, issues }
  }
}
```

Warnings never throw. They go to `console.warn` and the call proceeds.

## Options

```js
validateChartData(chartData, {
  form: 'auto',            // 'patch' | 'resolved' | 'auto'
  requireSomething: true,  // demand chartType and/or seriesData
  maxIssues: 20,           // cap the report
});
```

**`form`** decides whether a foreign option bag is a mistake. A hand-authored
*patch* names one chart type, so `{ chartType: 'pie', bar: {…} }` is worth
reporting. A *resolved* snapshot from `getChartData()` legitimately carries every
bag, because defaults seed them all. `auto` tells them apart by whether every bag
is present, which is right in practice — pass `form` explicitly when you know.

**`requireSomething`** should be `false` when validating a styling-only patch:

```js
validateChartData({ title: { text: 'FY26' } }, { requireSomething: false });
```

## Formatting for humans

```js
import { formatValidationIssues } from '@chartbuddy.io/embed';

console.error(formatValidationIssues(result.issues, '[my-app]'));
```

## Where else validation runs

Inside the ChartBuddy app, validation is advisory (logged, not thrown) so charts keep opening while you edit. The embed API is strict and throws, so callers can catch and fix bad configs before retrying.

## Generating valid data in the first place

The machine-readable schema ships in the package and describes every field the
validator checks:

```js
import schema from '@chartbuddy.io/embed/chart-schema.json';
```

Constrain generation with it and most of this page stops mattering. See
[chartData schema](/api/chart-data).
