Data contracts and JSON Schema
This page is about the schema you give the designer, so authors can drag fields onto the page. The data itself, what your back end sends with each render, is described with worked examples in Preparing the data.
The designer never sees your data, but authors need to know which fields exist. Give it a JSON Schema of the data you will send to the render API for that document type. The Fields tab turns it into a tree that authors drag onto the page.
import type { ReportDesigner } from '@rd/designer';
const designer = document.querySelector('report-designer') as ReportDesigner;
designer.schema = await (await fetch('/api/schemas/bill-of-lading.json')).json();
What the designer reads#
| Keyword | Effect in the Fields tab |
|---|---|
type | Icon and drag behavior: string, number, integer, boolean (drag creates a checkbox), object (expandable node) or array (drag creates a table). ["string", "null"] is treated as string |
format: "date" / "date-time" | Shown as a date field |
properties | Child fields |
items | For arrays: the item fields become the columns when dragged, bound as item.<field> |
title | Label in the tree, and the column header when a list is dragged. The root schema's title is the panel heading |
description | Tooltip |
examples[0] | Becomes the binding's preview text when dragged: {{ shipTo.city | Columbus }} |
$ref | Local references only (#/$defs/Address, #/definitions/...), resolved recursively (loops are cut) |
$defs, definitions | Targets for $ref |
allOf | Merged (properties combined) |
anyOf / oneOf | The first non-null branch is shown |
Everything else (required, enum, pattern, minimum, …) is ignored by the designer, so you can use the same schema to validate data in your back end.
Remote $refs (https://…) are not fetched. Bundle the schema first. Nesting is shown up to 8 levels deep.
Example: bill of lading contract#
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Bill of lading data",
"type": "object",
"$defs": {
"Address": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name", "examples": ["Northwind Retail #1187"] },
"address1": { "type": "string", "title": "Street", "examples": ["2250 Lakeshore Blvd"] },
"city": { "type": "string", "title": "City", "examples": ["Columbus"] },
"state": { "type": "string", "title": "State", "examples": ["OH"] },
"zip": { "type": "string", "title": "ZIP", "examples": ["43215"] }
}
}
},
"properties": {
"bolNumber": { "type": "string", "title": "BOL number", "description": "VICS BOL number, printed as Code 128", "examples": ["BOL2026102001"] },
"shipDate": { "type": "string", "format": "date", "title": "Ship date", "examples": ["2026-10-20"] },
"freightTerms": { "type": "string", "enum": ["prepaid", "collect", "thirdParty"], "title": "Freight terms", "examples": ["prepaid"] },
"hazmat": { "type": "boolean", "title": "Contains hazardous materials" },
"shipFrom": { "$ref": "#/$defs/Address", "title": "Ship from" },
"shipTo": { "$ref": "#/$defs/Address", "title": "Ship to" },
"lines": {
"type": "array",
"title": "Carrier information lines",
"items": {
"type": "object",
"properties": {
"huQty": { "type": "integer", "title": "H.U. qty", "examples": [2] },
"huType": { "type": "string", "title": "H.U. type", "examples": ["PLT"] },
"weight": { "type": "number", "title": "Weight (lbs)", "examples": [2260] },
"description": { "type": "string", "title": "Commodity description", "examples": ["Furniture, flat-pack, KD"] },
"nmfc": { "type": "string", "title": "NMFC #", "examples": ["082310"] },
"class": { "type": "string", "title": "Class", "examples": ["100"] }
}
}
}
}
}
Each of the 20 gallery templates ships a complete contract (packages/designer/templates/<id>/schema.json) and matching sample-data.json. Mapping your data
to these contracts is the fastest way to adopt a gallery template.
Designing the contract#
These rules apply to the data you send; Preparing the data shows them in five complete examples.
- Send what the document prints, already joined. Expressions can format and sum, but they can't run queries. Flatten what the document needs into one JSON document per render: the order header, its lines, the carrier and the ship-to address.
- Use stable camelCase keys. Authors type them (
shipTo.name). Keys with dashes or spaces work (shipTo['zip-code']), and the designer writes the bracket syntax for you, but they are harder to type. - Numbers as numbers. Send
2260, not"2,260", sosum()andformatNumber()work and the document language can format them. - Dates as ISO strings. Send
"2026-10-20"for dates. For date-times in warehouse local time, send"2026-10-20T15:45:00"without a zone. AZor offset makes the server convert to its own time zone. - Money as numbers plus a currency code (
{ "amount": 1250, "currency": "USD" }). Authors writeformatCurrency(total.amount, total.currency). - Booleans for checkboxes (
hazmat: true), and enumerations as strings (freightTerms: "prepaid"). - Pre-compute check digits and identifiers (SSCC, GTIN). The barcode engine verifies them but doesn't invent them.
- Images as data URIs (
"logo": "data:image/png;base64,…"), or as https URLs on hosts your administrator allows (Security). - Version the contract with your templates (Templates and gallery). Renaming a field breaks every template that binds to it.
No schema?#
Without a schema, the Fields tab lists the paths the template already uses, and authors type bindings by hand. That works, but providing a schema prevents typos and makes preview text realistic.