Skip to content

Validation

There are two questions worth asking about a report. They have different answers and different owners, and mixing them into one number tells you neither.

Is the template sound? validateTemplate answers that without any data at all, so what it reports is true for every report the template will ever produce and is the designer's to fix.

Does this client's data fill it in? checkOutput answers that against one data set. A template can be perfectly valid and still print Values as at . for a client whose review has no end date.

The editor runs both and shows them as separate lists in the toolbar. Click any issue to jump to the block it belongs to.

Template validation

Templates are analysable before any data exists. The editor validates continuously; servers can validate before accepting a template for storage.

ts
import { validateTemplate } from "@broadpaper/core";
import { createRegistry } from "@broadpaper/blocks";

const result = validateTemplate({ template, registry: createRegistry([riskProfileBlock]), dataSources, theme, availableFonts: ["Inter"] });
result.ok;          // no errors
result.errors;      // Issue[] — will not render correctly
result.warnings;    // Issue[] — renders, but probably not what you meant

Each Issue has severity, code, message, and where relevant nodeId, prop, region and source.

CodeMeaning
schema-invalidThe JSON does not match the document schema.
unknown-blockA block type is not registered (custom block unavailable).
license-requiredA block needs a tier the host has not enabled.
unknown-bindingAn expression references a field that no data source declares.
invalid-expression, invalid-conditionSyntax errors.
type-mismatchclient.firstName | currency, or a number bound where a date is expected.
missing-collectionA table, chart or repeater without a list, or bound to a non-list.
missing-requiredA block-specific required property is empty.
missing-theme-token, missing-theme-value$colors.brandX does not exist / the theme lacks a required value.
unsupported-fontA font is used that the host did not declare as available.
invalid-url, invalid-imageNon-http(s)/data:image URLs.
layout-invalidRow layouts not summing to 12, blocks in the wrong container, unusable margins.
region-overflowHeader/footer content taller than its region (reported at layout time).
scope-shadowingA repeater alias hides a data source of the same name.
empty-contentInformational.

Custom blocks add their own checks through validate(ctx) and can type-check expressions with ctx.checkExpression(expr, prop).

Output readiness

checkOutput walks a document resolved in preview mode and reports what the data does to it: bound values that come back blank, tokens that leave a gap in the middle of a sentence, blocks and sections that render nothing at all.

ts
import { checkOutput, resolveTemplate } from "@broadpaper/core";

const resolved = resolveTemplate({ template, registry, dataSources, data: client, mode: "preview" });
const issues = checkOutput({ resolved, registry, dataLabel: "Eleanor Whitfield" });

Resolve in preview mode against the data you intend to ship. Design mode fills gaps in sample data with synthesised values, which would hide exactly the blanks this is looking for.

Every issue it produces carries source: "output". Issues from validateTemplate carry source: "template", or no source, which means the same thing.

CodeMeaning
empty-valueA bound field, or a token inside a sentence, is empty in this data.
empty-contentA block that carries text renders none of it, or a section has nothing visible in it.

Three things it deliberately says nothing about, because a check people learn to ignore is worse than no check:

  • Content a false condition already removed. A preview resolution has dropped those nodes, so they cannot be reached from here at all.
  • Zero, false, and lists with a designed empty message. 0 is an answer, and a table that prints "No holdings in this portfolio" has handled its case.
  • An expression that already handles the empty case — a default: fallback, a conditional, or coalesce. Warning there would mean firing precisely when someone has fixed the problem.

Writing around optional values

That last exclusion is also the advice. Where a field is optional, say so in the template rather than letting the sentence break around it.

{{ review.nextReview | date:"MMMM yyyy" | default:"To be arranged" }}
{{ review.periodEnd ? "Values as at " + (review.periodEnd | date:"d MMMM yyyy") + ". " : "" }}
{{ adviser.qualifications ? ", " + adviser.qualifications : "" }}

The first substitutes a fallback when the value is missing. The second and third take the surrounding words with them, which is what you want when the clause only makes sense with a value in it — otherwise a missing phone number leaves contact Daniel Okafor on or in a document a client reads.