|
| 1 | +# slack-block-kit-validator |
| 2 | + |
| 3 | +[](https://github.com/TightknitAI/slack-block-kit-validator/actions/workflows/ci.yml) |
| 4 | +[](https://www.npmjs.com/package/slack-block-kit-validator) |
| 5 | +[](https://opensource.org/licenses/MIT) |
| 6 | + |
| 7 | +JSON Schema (draft 2020-12) and validation helpers for Slack Block Kit JSON. Catches invalid block payloads before Slack silently swallows them. |
| 8 | + |
| 9 | +## Why this exists |
| 10 | + |
| 11 | +Slack's API returns `200 OK` when you send malformed Block Kit JSON — the metadata is dropped and the message renders as plain text (or a modal opens blank). The only way to find out is to eyeball a real Slack channel. Slack hasn't open-sourced their validator. |
| 12 | + |
| 13 | +This package compiles every rule in <https://docs.slack.dev/reference/block-kit> into a single JSON Schema, plus a handful of helpers for the cross-payload rules JSON Schema can't express (duplicate `block_id`, cumulative markdown length, one-table-per-message, `focus_on_load` uniqueness, surface compatibility). |
| 14 | + |
| 15 | +## Install |
| 16 | + |
| 17 | +```sh |
| 18 | +pnpm add slack-block-kit-validator |
| 19 | +``` |
| 20 | + |
| 21 | +Node 20+. No runtime peer dependencies. |
| 22 | + |
| 23 | +## Quick start |
| 24 | + |
| 25 | +```ts |
| 26 | +import { validateBlockKit } from "slack-block-kit-validator"; |
| 27 | + |
| 28 | +const blocks = [ |
| 29 | + { type: "section", text: { type: "mrkdwn", text: "Hello *world*" } }, |
| 30 | +]; |
| 31 | + |
| 32 | +const { valid, errors } = validateBlockKit(blocks); |
| 33 | +if (!valid) { |
| 34 | + console.error(errors); |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +### Validating views |
| 39 | + |
| 40 | +`validateBlockKit` takes an optional `target` for modal / home view envelopes: |
| 41 | + |
| 42 | +```ts |
| 43 | +validateBlockKit(modalView, { target: "modal" }); |
| 44 | +validateBlockKit(homeView, { target: "home" }); |
| 45 | +``` |
| 46 | + |
| 47 | +When `target` is `modal` or `home`, the surface-compatibility check is enforced automatically. For bare blocks arrays, pass `surface` explicitly: |
| 48 | + |
| 49 | +```ts |
| 50 | +validateBlockKit(blocks, { surface: "message" }); |
| 51 | +// rejects input blocks, table on non-message surfaces, etc. |
| 52 | +``` |
| 53 | + |
| 54 | +### Example output |
| 55 | + |
| 56 | +```ts |
| 57 | +validateBlockKit([ |
| 58 | + { type: "section" }, |
| 59 | + { type: "divider", block_id: "x" }, |
| 60 | + { type: "divider", block_id: "x" }, |
| 61 | +]); |
| 62 | + |
| 63 | +// { |
| 64 | +// valid: false, |
| 65 | +// errors: [ |
| 66 | +// "/0 must match a schema in anyOf", |
| 67 | +// "blocks[2].block_id must be unique — 'x' appears at index 1 and 2" |
| 68 | +// ] |
| 69 | +// } |
| 70 | +``` |
| 71 | + |
| 72 | +## Using the helpers à la carte |
| 73 | + |
| 74 | +The helpers are pure (no deps, no Ajv) and can be stacked on top of any validator — Zod, TypeBox, or a hand-rolled check: |
| 75 | + |
| 76 | +```ts |
| 77 | +import { |
| 78 | + findDuplicateBlockIds, |
| 79 | + checkCumulativeMarkdownLength, |
| 80 | + checkSingleTableBlock, |
| 81 | + checkFocusOnLoadUniqueness, |
| 82 | + checkSurfaceCompatibility, |
| 83 | +} from "slack-block-kit-validator"; |
| 84 | +``` |
| 85 | + |
| 86 | +### Using the raw JSON Schema |
| 87 | + |
| 88 | +Consuming from another validator, another language, or an OpenAPI spec: |
| 89 | + |
| 90 | +```ts |
| 91 | +import { slackBlockKitSchema } from "slack-block-kit-validator"; |
| 92 | + |
| 93 | +// With Ajv in a custom config |
| 94 | +const ajv = new Ajv2020({ strict: false, allErrors: true }); |
| 95 | +const validate = ajv.compile(slackBlockKitSchema); |
| 96 | +``` |
| 97 | + |
| 98 | +The schema uses `$defs` for every block, element, composition object, rich-text leaf, and view envelope, so non-JS consumers can also import the JSON (`slack-block-kit-validator/schema.json`) and pick the subset they need. |
| 99 | + |
| 100 | +## API reference |
| 101 | + |
| 102 | +### Wrapper |
| 103 | + |
| 104 | +| Export | Signature | Description | |
| 105 | +|---|---|---| |
| 106 | +| `validateBlockKit` | `(input, opts?) => { valid, errors[] }` | Runs schema + all caveat helpers. Defaults to validating a bare blocks array. | |
| 107 | +| `ValidationResult` | `{ valid: boolean; errors: string[] }` | Return type. | |
| 108 | +| `ValidationTarget` | `'blocks' \| 'modal' \| 'home'` | What shape `input` should match. | |
| 109 | +| `ValidateBlockKitOptions` | `{ target?, surface? }` | Options bag. | |
| 110 | +| `Surface` | `'message' \| 'modal' \| 'home'` | Surface compatibility target. | |
| 111 | + |
| 112 | +### Helpers |
| 113 | + |
| 114 | +| Helper | Signature | What it checks | |
| 115 | +|---|---|---| |
| 116 | +| `findDuplicateBlockIds` | `(blocks) => string[]` | Duplicate `block_id` values in a blocks array. | |
| 117 | +| `checkCumulativeMarkdownLength` | `(blocks) => string[]` | Sum of all `markdown` block text > 12,000 chars. | |
| 118 | +| `checkSingleTableBlock` | `(blocks) => string[]` | More than one `table` block per payload. | |
| 119 | +| `checkFocusOnLoadUniqueness` | `(blocks) => string[]` | More than one element with `focus_on_load: true` in a view (walks nested elements + accessories). | |
| 120 | +| `checkSurfaceCompatibility` | `(blocks, surface) => string[]` | Blocks not allowed on the target surface (e.g. `input` on message, `video` on modal, `file_input` outside modals). | |
| 121 | +| `checkCardActionsMax` | `(blocks) => string[]` | More than `CARD_ACTIONS_MAX` action buttons on a card block. | |
| 122 | +| `checkNumberInputBounds` | `(blocks) => string[]` | `number_input` element with `min_value > max_value`. | |
| 123 | +| `checkResponseUrlEnabledContext` | `(blocks, surface?) => string[]` | `response_url_enabled` set in contexts that don't support it. | |
| 124 | + |
| 125 | +Each returns an array of human-readable error strings — empty when valid. |
| 126 | + |
| 127 | +### Schema |
| 128 | + |
| 129 | +| Export | Description | |
| 130 | +|---|---| |
| 131 | +| `slackBlockKitSchema` | The full JSON Schema as a parsed object. `$id` is `https://tightknit.com/schemas/slack-block-kit.schema.json`. | |
| 132 | + |
| 133 | +## Coverage |
| 134 | + |
| 135 | +- **18 blocks**: actions, alert, card, carousel, context, context_actions, divider, file, header, image, input, markdown, plan, rich_text, section, table, task_card, video. |
| 136 | +- **All block elements**: button, icon_button, workflow_button, feedback_buttons, plain_text / email / url / number inputs, datepicker, datetimepicker, timepicker, file_input, rich_text_input, checkboxes, radio_buttons, image, overflow, url source, and all 5 single + 5 multi-select menu variants. |
| 137 | +- **All 9 composition objects**: text (plain_text + mrkdwn), confirm, option (3 contextual variants), option_group, slack_file, dispatch_action_config, conversation_filter, trigger, workflow. |
| 138 | +- **Rich text**: 4 container kinds (section, list, preformatted, quote) + 10 leaf kinds (text, link, user, usergroup, team, channel, emoji, broadcast, color, date) with style flags. |
| 139 | +- **View envelopes**: `modal_view` + `home_view` under `$defs`. |
| 140 | +- **Cross-payload rules** (via helpers): dup `block_id`, cumulative markdown, single-table, `focus_on_load` uniqueness, surface compatibility. |
| 141 | + |
| 142 | +Every documented `maxLength`, regex (date / time / user ID / channel ID / team ID format), enum value, and array cardinality limit is enforced structurally. |
| 143 | + |
| 144 | +## What isn't enforced |
| 145 | + |
| 146 | +Server-side rules that need app-config context or deep equality checks the schema doesn't attempt: |
| 147 | + |
| 148 | +- Slack OAuth scope requirements (e.g. `links.embed:write` for video blocks). |
| 149 | +- Initial-value matching (e.g. `radio_buttons.initial_option` must equal one of `options` by deep equality). |
| 150 | +- `video_url` must match the app's configured unfurl domains. |
| 151 | +- 10 MB per-file limit on `file_input` uploads. |
| 152 | +- Slack's `block_id`-must-not-start-with `block_` rule is folklore (not stated on any current docs page) and intentionally not enforced. |
| 153 | + |
| 154 | +## Note on `undefined` properties |
| 155 | + |
| 156 | +`validateBlockKit` strips properties whose value is `undefined` before running Ajv. `JSON.stringify` drops these before the payload reaches Slack, so common builder patterns like `value: foo ?? undefined` are no-ops on the wire but would otherwise trip the schema's `additionalProperties: false`. Explicit `null` values are preserved — they survive `JSON.stringify` and may legitimately fail the schema. |
| 157 | + |
| 158 | +## Runtime & bundle considerations |
| 159 | + |
| 160 | +Ajv v8 + ajv-formats weigh ~50–60 KB gzipped, which is fine for Node test runs but noticeable in a Cloudflare Worker bundle. If you want runtime validation inside a Worker, use the package's `compile:standalone` script to emit a self-contained validator that doesn't import Ajv at runtime: |
| 161 | + |
| 162 | +```sh |
| 163 | +pnpm dlx ajv compile --spec=draft2020 --strict=false -c ajv-formats \ |
| 164 | + -s node_modules/slack-block-kit-validator/dist/slack-block-kit.schema.json \ |
| 165 | + -o standalone-validator.js |
| 166 | +``` |
| 167 | + |
| 168 | +For most apps the recommended pattern is **validate in tests only** — use `validateBlockKit` in unit tests next to your block builders, and let CI catch regressions before they hit production. |
| 169 | + |
| 170 | +## Keeping up with Slack |
| 171 | + |
| 172 | +Slack adds new block types and fields over time. To update: |
| 173 | + |
| 174 | +1. Diff the latest at <https://docs.slack.dev/reference/block-kit> against the commit history of `src/slack-block-kit.schema.json`. |
| 175 | +2. Add or amend the relevant `$defs/*` entry, including required / optional / maxLength / enum. |
| 176 | +3. For new top-level blocks, add to `$defs/block.oneOf` and the `modal_view` / `home_view` inner arrays if applicable. |
| 177 | +4. For new elements, add to the relevant parent-container whitelist (`actions_block_element`, `section_accessory_element`, `input_block_element`, `context_block_element`, or `context_actions_block_element`). |
| 178 | +5. Add fixtures to `test/` covering both valid and invalid payloads. |
| 179 | + |
| 180 | +## License |
| 181 | + |
| 182 | +MIT. See [LICENSE](./LICENSE). |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +Maintained by the [Tightknit](https://tightknit.ai) team. |
0 commit comments