---
title: "Validating definitions — quario"
description: "Compiling a quario definition without rendering it, checking one against its JSON Schema, reading a located problem, and holding a compiled report in a service."
url: "https://getquario.com/docs/diving-deeper/validating"
---

# Validating definitions

## Intent

A definition lives outside your type system. Nothing tells you it broke until
someone renders it, which in practice means a customer opening an invoice. This
is how to compile one on purpose, read what came back, and hold the result for
as long as your service runs.

## Design decisions

**`plan` is one traversal read three ways.** A host that validates and renders
in a loop, such as an editor, would otherwise descend the document twice.
`plan` returns the compiled report, the structured problems, and the anchors
each compiled source reads, all from a single descent.

**`report` throws on the first problem.** It’s the call for a definition you
already trust. When you want every problem at once, ask `plan` or `validate`
instead of catching in a loop.

**`validate` flattens the same problems into strings.** Its output is exactly
each problem’s `message`. That makes it the right call for a build script that
prints and exits. It’s the wrong one for anything that needs the location as
data.

**A problem keeps its own offsets.** Every problem carries its own diagnostic
where an engine authenticated the fault, not merely the first. An editor
underlining the offending characters needs all of them.

**Unknown keys come before the problems beneath them.** A mistyped key has no
position in the documented shape to slot into. The engine therefore reports it
ahead of the known keys it sits among. Every schema object with fixed keys is a
closed set, which is why `visable` is an error rather than an ignored field.

**`isDiagnostic` answers for the delegated engines only.** It’s true for a
located diagnostic raised by the expression, template or query engines, and
authenticated by identity rather than by shape. An error that merely looks like
one doesn’t pass.

**quario’s own verdicts carry no code.** Examples are an unknown reducer, a
fold handed something it can’t reduce, and image bytes the engine won’t vouch
for. These are quario’s judgements about your document, and they carry structured `path`
and `source` instead of an engine’s error code. Branch on those.

**No guard separates a verdict from a registered function’s throw.** The two
carry identical locations, so the engine can’t answer the question and
declines to pretend otherwise. A host that needs the distinction throws its
own error class from its own functions.

**A warning isn’t a problem at lower severity.** A warning names a declaration
the engine will quietly drop, reported after every compile has already
succeeded. A document carrying only warnings compiles and renders exactly as
before, and a host that ignores the field notices nothing.

**The JSON Schema proves shapes. `plan` proves the document.** `quario` ships
a JSON Schema of the report document, generated from the same declarations the
engine’s types come from. It checks each value where it stands, which is what
an editor or a model can use before the engine sees anything. A rule that joins
two values, or a string that has to compile, is beyond it. `plan` stays the
authority on whether a definition renders.

**Compiling is the expensive call.** Every expression in the document parses
into a closure at compile time. Rendering walks data through closures that
already exist, which is why the compiled report is the thing worth holding.

## API walkthrough

### Checking a definition before it renders

**`q.plan(schema, funcs?)`** returns `{ report, problems, anchors, readPaths, uses, warnings, params }`. `report`
is the compiled report, or `null` while the document has problems. `params`
lists the parameters the document declares, the same list `report.params`
holds. It’s there whether or not the document compiled, so an editor can draw
a parameter form beside the problems. `anchors` maps each compiled source’s
schema path to the anchors and handles it reads, so a tool can tell whether a
node may move. A placement is safe only where every anchor that node reads binds
there.

**`plan.readPaths`** maps the same schema paths to the member paths each
source reads, such as `["@", "qty"]` or `["$", "input", "customer"]`. Check a
sample or a feed against it before you render. A missing field reads as
`null`, so a render alone never shows the mismatch. Only `@` and `$.input`
read your data. A path ends in `null` where the source reads on through a key
the definition doesn’t name, such as `@.tags[0]` or an `#each` item. The
compiled report carries the same map as `report.readPaths`.

```ts title="read-paths.ts"
const { readPaths } = q.plan(definition)

readPaths['detail.columns[0].value'] // [['@', 'qty'], ['@', 'unitPrice']]
```

**`plan.warnings`** lists declarations nothing will read, in document order,
each `{ path, message }`. They carry no `source` and no `diagnostic`, because
nothing raised. There are four today. One is a `format` on a run that never renders a
single value. Another is a `currency` on a cell not formatted as one. The third
is a table whose fixed widths total under 100, and the fourth a split whose
slot widths leave some of their share unclaimed. The list is deliberately
incomplete, since it sees the definition alone, without data. `validate()`
doesn’t change, and returns problems only.

**`validate(schema, funcs?)`** returns the problems alone, as strings, in
document order. It’s a root export rather than an instance method, because
checking a document’s shape needs no instance configuration.

**`q.report(schema, funcs?)`** compiles and throws on the first problem.

```ts title="check.ts"
import { validate } from 'quario'

const problems = validate(definition, {})

for (const message of problems) console.error(message)
process.exitCode = problems.length ? 1 : 0
```

Run this over the definitions you ship, in the same job that runs your tests.
It needs no data and no render, so it costs a second and catches the class of
breakage that otherwise waits for a customer.

### Checking against the JSON Schema

**`quario/schema.json`** is the JSON Schema of the report document, in the
2020-12 dialect. Point an editor at it with a top-level `$schema` string. The
engine checks that the value is a string and reads nothing else, so the key
costs a render nothing. Only the top level takes it. Anywhere else, `$schema`
is an unknown key. No hosted URL exists, so name the installed file.

```json title="invoice.report.json"
{
  "$schema": "./node_modules/quario/lib/schema.json",
  "data": "$.invoice.lines[*]",
  "detail": { "columns": [{ "header": "Item", "value": "{{ @.item }}" }] }
}
```

A document can pass the JSON Schema and still fail `plan`, for four reasons:

- A rule joins two or more values. Width shares that total more than 100, a
group name that isn’t unique, and a `reset` without its `break` are all
examples.
- The document uses a reserved or blocked name.
- A string has to compile, as an expression, a template, a `data` path or a
reducer does.
- A `required` name isn’t one a target reads.

With a model, give it the file as a tool’s input schema, and pass what it
returns to `plan` before anything renders.

```ts title="tool.ts"
import schema from 'quario/schema.json' with { type: 'json' }

const tool = { name: 'draft_report', input_schema: schema }

// What the model sends back is still untrusted.
const { report, problems } = q.plan(definition)
```

This works in a tool’s ordinary input schema, in editors, and with Gemini,
which doesn’t enforce the patterns. The strict structured-output modes of
OpenAI and Anthropic don’t accept the file. OpenAI’s strict mode requires every
key, and a report document leaves most keys out. The strict mode of Anthropic
allows 24 optional keys in one request, and the report document has more than
290.

### Requiring a declaration a target might withdraw

Every withdrawal is silent by design. A target that can’t honour a declaration
drops it rather than failing. That’s exactly what lets one definition serve
four document shapes, and it’s also how a document quietly loses something it
needed.

**`"required"`** at the report root names the declarations this document can’t
do without, as `{ "required": { "uppercase": true } }`. Nothing reads it at
render time: no target sees the marking, and no output changes because of it.

**`q.plan(schema, funcs?, { targets })`** checks it. `targets`
takes the `capabilities` descriptors the published targets export beside their
factories. Every required declaration one of them withdraws or leaves unread
becomes a problem of its own at `required.<name>`. That nulls the report the
way any other problem does. An approximated declaration isn’t one.
Approximation is the target doing its best with what you asked for.

**`REQUIRABLE`** exports the set a document may mark. A name outside that set
is a definition error, because a marking nothing could check would go quiet
exactly when you needed it. The paper is in the set. A landscape statement
marking `page.orientation` gets a problem from `plan` when one of the named
targets is CSV, which withdraws the paper.

```ts title="check.ts"
import { capabilities as htmlCapabilities } from '@quario/html'
import { capabilities as xlsxCapabilities } from '@quario/xlsx'

const plan = q.plan(definition, {}, { targets: [htmlCapabilities, xlsxCapabilities] })

for (const problem of plan.problems) console.error(`${problem.path}: ${problem.message}`)
if (!plan.report) process.exitCode = 1
```

Passing no descriptors checks nothing, so this is additive throughout: a build
that doesn’t name its targets behaves as it did before.

**`plan.uses`** shows every fate, not only the required ones. It maps each
name a descriptor answers for to the schema paths where the document writes it.
Join it with a descriptor to list what a target withdraws, leaves unread,
or approximates. It needs no option, adds no problem, and is there while the
document has problems.

```ts title="fates.ts"
import { capabilities as csvCapabilities } from '@quario/csv'

const { uses } = q.plan(definition)

for (const [name, paths] of Object.entries(uses)) {
  const fate = csvCapabilities.declarations[name]
  if (fate !== 'resolved') console.log(name, fate, paths)
}
// uppercase withdrawn ['detail.columns[0].style.uppercase']
```

### Reading a problem

**`problem.path`** is the schema path the fault sits at, such as
`detail.columns[3].value`. It tells you which node to open.

**`problem.source`** is the author source behind it, present when the problem
came from compiling one.

**`problem.message`** is the whole located sentence, and it’s what `validate`
returns for that problem.

**`problem.diagnostic`** is the engine’s own located error, present when an
engine authenticated the fault. Its offsets are zero-based into the source
shown in brackets, with `end` exclusive.

```ts title="check.ts"
const { report, problems, warnings } = q.plan(definition, {})

for (const problem of problems) {
  console.error(`${problem.path}: ${problem.message}`)
  if (problem.source) console.error(`  in ${problem.source}`)
}
for (const warning of warnings) console.warn(`${warning.path}: ${warning.message}`)

if (!report) process.exitCode = 1
```

The engine freezes problems, so a reporting pass can’t change what it’s
describing.

Decide your own severity. This site’s own build fails on a warning, because a
documented declaration that does nothing is documentation that’s wrong.

### Telling an engine’s fault from your own

**`isDiagnostic(e)`** is true for a located diagnostic and nothing else. What
it buys you is trust in the `code` and the offsets on that error. They’re an
engine’s, not a lookalike’s.

**`code`, `start` and `end`** come from whichever engine decided the fault.
Expression errors preserve the expression engine’s, and template errors use
absolute offsets into the displayed template. A traversal budget exceeded at
render time carries `limit` and `actual` instead of offsets.

**A registered function’s throw keeps its class** behind the path it failed at.
`isDiagnostic` is false for it, and so is any test for quario’s own verdicts.

**Your own error class** is the way to mark your own failures. It must be the
class that carries the marker. The engine rebuilds a located copy from the
constructor and the message, so a field you set on the instance doesn’t
survive.

**`error.cause` carries the throw as it happened.** Because that copy is
rebuilt, the stack saying which line of your function threw used to go with the
original. The copy now carries it: read `cause` for the throw itself, and the
message for where quario was when it happened. The viewer’s and the editor’s
own property checks do the same. A `page` or `fonts` value either element
refuses throws a `TypeError` naming the property, carrying the layout’s account
of the value behind it.

```ts title="render.ts"
import { isDiagnostic } from 'quario'

try {
  await report.render(target, data)
} catch (error) {
  if (isDiagnostic(error)) console.error(error.code, error.start, error.end)
  else throw error
}
```

Diagnostics sit outside the frozen authoring surface, so their codes may change
with the engines behind them. Log them, and don’t build control flow on a
particular code.

### Holding a compiled report

A compiled report is the object your service keeps between requests. Compile it
at startup, or the first time something asks for a definition, and give
`render` a fresh dataset per request.

**`report.stream(data)`** is the raw event seam, one generator of report events
per call. It’s what a custom target consumes and what the official targets sit
behind.

**`report.names`** is the union of free variables across every expression in
the document. It excludes engine anchors and group handles, so what’s left
is the set of names your registry has to satisfy.

**`report.functions`** lists the registry functions the document calls, in
call-first-seen order. Each carries a name, an arity, and its own doc string
where it has one.

**`report.paths`** is the query topology behind `data`. Metadata read through
`$.input` doesn’t change it.

**What invalidates a held report** is the definition and the registry behind
it. Change either and compile again. Nothing invalidates it on its own, and
no data you render through it can.

The instance matters as much as the definition. Presentation reads the
instance’s locale, currency and timezone, so the same definition compiled by an
`en-IE` instance and a `de-DE` one renders `€1,234.50` and `1.234,50 $`. A
service presenting in several locales holds an instance, and a compiled report,
for each.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Validating definitions","item":"https://getquario.com/docs/diving-deeper/validating"}]}
```
