> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Serializable spec

The builders you've met so far — `createSpec`, `pipe`, `geom.*`, `scale.*`, `config` — are convenience, not substance. Everything they produce is a plain, immutable object made only of objects, arrays, strings, numbers and booleans. A spec holds no functions, no class instances, no DOM references. That means it round-trips through `JSON.stringify` / `JSON.parse` unchanged: you can store a spec in a database, send it over the wire, generate it from another language, or write one by hand.

## The builder is just sugar

`pipe(...)` and the `geom.*` / `scale.*` / `config` helpers just assemble and merge a plain object. This call:

```tsx theme={null}
import { createSpec, pipe, geom, scale, config } from '@graphysdk/react';

const spec = pipe(
  createSpec({ x: 'month', y: 'revenue', color: 'product' }),
  geom.line(),
  scale.x(),
  scale.y.continuous({ domainMin: 0 }),
  scale.color.palette(),
  config({ legend: { position: 'bottom' } })
);
```

produces exactly this object — and `JSON.stringify(spec)` gives you exactly this JSON (fields left `undefined` drop out):

```json theme={null}
{
  "mapping": { "x": "month", "y": "revenue", "color": "product" },
  "layers": [{ "type": "layer", "geom": "line" }],
  "scales": [
    { "type": "scale", "scaledAesthetic": "x", "scaleType": "inferred" },
    {
      "type": "scale",
      "scaledAesthetic": "y",
      "scaleType": "continuous",
      "domainMin": 0,
      "domainMax": null,
      "scheme": null,
      "domainMid": null
    },
    { "type": "scale", "scaledAesthetic": "color", "scaleType": "palette" }
  ],
  "transforms": [],
  "highlights": [],
  "config": { "legend": { "position": "bottom" } }
}
```

You could paste that JSON into your source and feed it straight to the renderer with no builder in sight. What the builders add is **types, defaults and guardrails** — autocomplete for aesthetics and params, sensible fallbacks, and a compile-time check that the shape is valid.

## Every top-level key

`createSpec` seeds six keys — `mapping`, `layers`, `scales`, `transforms`, `highlights` and `config` — so they are present even when empty. The other three appear only once something is piped in:

| Key           | Holds                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------- |
| `mapping`     | The spec-level [aesthetic mapping](/sdk-next/concepts/mappings)                              |
| `layers`      | One entry per [geom](/sdk-next/concepts/geoms)                                               |
| `scales`      | One entry per declared [scale](/sdk-next/concepts/scales)                                    |
| `transforms`  | Spec-level [transforms](/sdk-next/advanced/transforms)                                       |
| `highlights`  | [Highlights](/sdk-next/emphasis/highlights)                                                  |
| `config`      | [Chart chrome](/sdk-next/config/index)                                                       |
| `coords`      | The [coordinate system](/sdk-next/concepts/coordinate-systems) — a single object, not a list |
| `styles`      | The [stylesheet](/sdk-next/config/styling)                                                   |
| `annotations` | [Annotations](/sdk-next/emphasis/annotations), grouped by kind                               |

## A styled spec is still JSON

The stylesheet is the largest thing a spec carries, and it round-trips as plainly as the rest. A `token('brand')` reference is an ordinary object the compile stage inlines, and a light/dark colour pair is an ordinary object the renderer picks from at read time — so neither needs a function to survive the trip:

```tsx theme={null}
import {
  createSpec,
  pipe,
  geom,
  scale,
  styles,
  style,
  token,
} from '@graphysdk/react';

const spec = pipe(
  createSpec({ x: 'month', y: 'revenue' }),
  geom.bar(),
  scale.x(),
  scale.y(),
  styles({
    tokens: { brand: { light: '#0B5FFF', dark: '#6AA1FF' } },
    defaults: [style.geom({ color: token('brand') })],
  })
);
```

```json theme={null}
{
  "styles": {
    "type": "styles",
    "tokens": { "brand": { "light": "#0B5FFF", "dark": "#6AA1FF" } },
    "defaults": [
      {
        "select": { "target": "geom" },
        "declarations": { "color": { "token": "brand" } }
      }
    ]
  }
}
```

## Writing the JSON by hand

Two things to know before you do.

A layer's aesthetic override is called `aes` on the builder and **`mapping`** in the object: `geom.line({ aes: { y: 'profit' } })` serializes as `{ "type": "layer", "geom": "line", "mapping": { "y": "profit" } }`.

And a spec that uses a [plugin](/sdk-next/extending/index) geom, stat or transform still serializes — a custom layer is just `{ "geom": "candlestick", "params": { … } }` — but the plugin itself is code, and it reaches the chart through the `plugins` array on `GraphProvider` rather than through the spec. Store the spec; ship the plugins with your app.

## Next

* [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the compile → render pipeline that consumes a spec
* [Data structure](/sdk-next/data-structure) — the table a spec references by column name
* [Extending](/sdk-next/extending/index) — custom geoms, stats and transforms, and the `plugins` they ship in
