On this page
Schemas and types
import { z } from 'zod'
import { variables } from '@react-markdown-kit/variables'
const ReportSchema = z.object({
customer: z.object({ name: z.string() }),
revenue: z.number(),
})
variables({ schema: ReportSchema, data: { customer: { name: 'Acme' }, revenue: 50_000 } })variables() accepts any validator implementing
Standard Schema, the shared interface designed by
contributors from Zod, Valibot and ArkType. When you pass schema, data is
typed from it, so TypeScript will reject a call that leaves out revenue or
misspells the customer key. Without a schema, data is whatever you pass, and
you can type it with a generic parameter: variables<ReportData>({ data }).
Generics are compile time, schemas are runtime#
| Generic type parameter | Standard Schema | |
|---|---|---|
| When it runs | tsc, and your editor | Every compilation |
| Catches | A wrong literal in your own code | A wrong value from an API, a database or a user |
| Cost at runtime | None | One validation pass |
| Failure mode | A type error | An error diagnostic, and nothing rendered |
Data that comes over the network is really unknown, whatever the type says,
so I'd recommend validating it.
Valibot and ArkType work the same way, through the same property, with no adapter:
import * as v from 'valibot'
const ReportSchema = v.object({ customer: v.object({ name: v.string() }), revenue: v.number() })import { type } from 'arktype'
const ReportSchema = type({ customer: { name: 'string' }, revenue: 'number' })@react-markdown-kit/variables doesn't depend on any of these libraries. It just
reads the ~standard property, so the only validator installed is the one you
picked.
What a rejection looks like#
If validation fails, resolution stops there. Resolving against rejected data
would give you a document that looks fine to publish but is built on bad data,
so the document becomes the fallback (nothing by default) and the diagnostics
explain why:
// [{ code: 'VARIABLE_SCHEMA_INVALID', severity: 'error', path: 'revenue', message: ... }]Each validator issue becomes one diagnostic with the data path it's about. Messages include the path but not the value, so they're safe to log.
Resolution is synchronous. A validator that returns a Promise produces
VARIABLE_SCHEMA_ASYNC instead of a half-resolved document, so do any async
work before you compile.
Schemas stay optional#
Without a schema, a missing value still produces VARIABLE_REQUIRED_VALUE and
an empty document. Adding a schema moves that check earlier, so bad data is
rejected when it comes in instead of being caught while the document resolves.
## Statement for {{customer.name}}
Balance: {{balance | currency:"EUR"}}
Statement for Northwind
Balance: €1,240.50
The second dataset fails, so the pane shows diagnostics instead of a document with a blank where the balance should be.
Presentation metadata is separate#
Runtime validation and editor UX are separate concerns, so labels go in
variables instead of the schema. That way your validator doesn't need to know
about any kit-specific annotations.
variables({
data,
schema: ReportSchema,
variables: {
'customer.name': { label: 'Customer name', group: 'Customer' },
revenue: { label: 'Revenue', group: 'Financials' },
},
})The same map is used by variableChips({ variables }) in the editor (see
Authoring). Two of the fields affect resolution:
variables: {
'customer.vatNumber': { required: false },
'report.title': { default: 'Monthly report' },
}required: false lets a missing value resolve to empty text with a warning.
default provides a value when the data doesn't have one, and makes the
variable optional. Every other variable is required by default.
Next#
- Formatting and localization for the built-in formatters.
- Variable basics for options and diagnostics.
Read next#
The overview, from placeholder syntax to schemas: Markdown variables.
Last updated on