---
name: building-and-parsing-body
description: >
  Load when creating Xincodo Body data with createBody, paragraph, heading,
  text, richText, list, table, pageLink, image, or gallery builders; when
  handling Body block shapes; or when accepting persisted JSON with
  safeMigrateBody instead of defaulting to parseBody.
type: core
library: "@ryhrm-gz/xincodo-lib"
library_version: "0.1.0"
sources:
  - "ryhrm-gz/xincodo-lib:README.md"
  - "ryhrm-gz/xincodo-lib:src/types.ts"
  - "ryhrm-gz/xincodo-lib:src/builders.ts"
  - "ryhrm-gz/xincodo-lib:src/schemas.ts"
  - "ryhrm-gz/xincodo-lib:src/migration.ts"
  - "ryhrm-gz/xincodo-lib:tests/builders.test.ts"
  - "ryhrm-gz/xincodo-lib:tests/migration.test.ts"
---

# Building and Parsing Body

Use typed builders to create `Body` data. Treat saved or external JSON as
unknown input and pass it through `safeMigrateBody` before using it.

## Setup

```ts
import { createBody, heading, paragraph, safeMigrateBody, text } from "@ryhrm-gz/xincodo-lib";

const body = createBody([
  heading(1, "Getting started", { id: "getting-started" }),
  paragraph("Create content with ", text("typed helpers", { annotations: { bold: true } })),
]);

const migrated = safeMigrateBody(JSON.parse(JSON.stringify(body)));
if (!migrated.success) {
  throw new Error(migrated.issue.message);
}

const currentBody = migrated.output;
```

## Core Patterns

### Build rich text with helper inputs

```ts
import { lineBreak, paragraph, text } from "@ryhrm-gz/xincodo-lib";

const intro = paragraph(
  "Read ",
  text("the docs", {
    annotations: { underline: true },
    link: { href: "https://example.com/docs", title: "Documentation" },
  }),
  lineBreak(),
  "before publishing.",
);
```

`paragraph`, `heading`, `callout`, `quote`, `listItem`, and similar helpers
normalize strings into `{ type: "text" }` inline values.

### Build structured blocks through builders

```ts
import {
  bulletedList,
  callout,
  createBody,
  image,
  listItem,
  table,
  tableCell,
  tableRow,
} from "@ryhrm-gz/xincodo-lib";

const body = createBody([
  bulletedList([listItem("Draft"), listItem("Review"), listItem("Publish")]),
  callout("Remember image alt text.", {
    icon: { type: "emoji", emoji: "!" },
    color: "yellow",
  }),
  table([tableRow([tableCell("Status"), tableCell("Ready")])]),
  image({ type: "file", url: "/uploads/cover.png" }, { alt: "Article cover" }),
]);
```

Prefer builders over handwritten JSON. Builders keep the serialized block
type names and nested shapes aligned with the library types.

### Accept persisted JSON with safeMigrateBody

```ts
import { safeMigrateBody } from "@ryhrm-gz/xincodo-lib";

export function readPersistedBody(value: unknown) {
  const result = safeMigrateBody(value);

  if (!result.success) {
    return { ok: false as const, issue: result.issue };
  }

  return { ok: true as const, body: result.output, migrated: result.migrated };
}
```

`parseBody` is a lower-level schema API. Use `safeMigrateBody` at saved-data
or external-input boundaries so version handling is included.

### Use parseBody only for lower-level schema checks

```ts
import { parseBody } from "@ryhrm-gz/xincodo-lib";

const result = parseBody({
  version: 1,
  content: [],
});

if (!result.success) {
  console.error(result.issues);
}
```

Use this when the calling code deliberately wants Valibot safe-parse issues
instead of migration-aware handling.

## Common Mistakes

### HIGH Defaulting to parseBody for saved JSON

Wrong:

```ts
import { parseBody } from "@ryhrm-gz/xincodo-lib";

const result = parseBody(JSON.parse(serializedBody));
if (!result.success) {
  throw new Error("Invalid Xincodo body");
}

renderPreview(result.output);
```

Correct:

```ts
import { safeMigrateBody } from "@ryhrm-gz/xincodo-lib";

const result = safeMigrateBody(JSON.parse(serializedBody));
if (!result.success) {
  throw new Error(result.issue.message);
}

renderPreview(result.output);
```

`parseBody` checks only the current schema; `safeMigrateBody` is the default
boundary for persisted or external Body JSON.

Source: maintainer interview; `src/schemas.ts`; `src/migration.ts`;
`tests/migration.test.ts`

### HIGH Handwriting display block names

Wrong:

```ts
import { safeMigrateBody } from "@ryhrm-gz/xincodo-lib";

const result = safeMigrateBody({
  version: 1,
  content: [{ type: "paragraph", richText: [{ type: "text", text: "Intro" }] }],
});
```

Correct:

```ts
import { createBody, paragraph } from "@ryhrm-gz/xincodo-lib";

const body = createBody([paragraph("Intro")]);
```

Serialized block type values are fixed snake_case literals such as `"text"`
and `"heading_1"`; builders avoid mismatching UI labels with stored shapes.

Source: maintainer interview; `src/types.ts`; `src/builders.ts`;
`tests/builders.test.ts`

### HIGH Omitting the Body version

Wrong:

```ts
import { paragraph, safeMigrateBody } from "@ryhrm-gz/xincodo-lib";

const result = safeMigrateBody({
  content: [paragraph("Intro")],
});
```

Correct:

```ts
import { createBody, paragraph, safeMigrateBody } from "@ryhrm-gz/xincodo-lib";

const body = createBody([paragraph("Intro")]);
const result = safeMigrateBody(body);
```

`Body` values must include `version: 1`; missing or non-integer versions are
reported as invalid migration input.

Source: `src/types.ts`; `src/migration.ts`; `tests/migration.test.ts`

## References

- [Body block shapes](references/body-block-shapes.md)
- [Builder signatures](references/builder-signatures.md)

See also: `validating-body/SKILL.md` — migrated or built Body values often
need lint checks before save or publish.
