---
title: "Validate data in CI"
description: "Run aontu vet in a pipeline so a document that does not hold fails the build, with the reason attached."
source: "https://aontu.dev/how-to/validate-in-ci/"
---

# Validate data in CI

Run aontu vet in a pipeline so a document that does not hold fails the build, with the reason attached.

Rendered from [`docs/how-to/validate-in-ci.md`](https://github.com/aontu-lang/aontu/blob/main/docs/how-to/validate-in-ci.md) in the engine repository, where a correction belongs, and where the test suite executes every example on this page.

A schema nobody runs is a comment. The `vet` verb turns yours into a gate: it validates data files against a schema document, prints located findings, and exits with the verdict class, which is everything a pipeline needs to go red for a reason.

Write the schema as `schema.aon`:

```aontu
user: { id:integer name:string admin:boolean }
```

and the data your pipeline produced as `user.json`:

```json
{"user": {"id": "seven", "name": "ada", "admin": true}}
```

Now vet it:

```sh
$ aontu vet schema.aon user.json
verdict: invalid

$.user.id: no_scalar_unify [conflict]
  [aontu/no_scalar_unify]: Cannot unify values at path $.user.id
  data: user.json:1:17 ("seven")
  schema: schema.aon:1:12 (integer)
$ echo $?
1
```

The finding names both sides, data site first, because the data is the side to edit. (To refuse _extra_ keys too, wrap the schema in `close`: see [Forbid unexpected keys](https://aontu.dev/how-to/forbid-unexpected-keys).)

## The exit code is the verdict class

There is more than one kind of “no”, and a gate that collapses them throws information away. Drop `id` from the data instead of mistyping it, as `user2.json` does:

```json
{"user": {"name": "ada", "admin": true}}
```

```sh
$ aontu vet schema.aon user2.json
verdict: incomplete

$.user.id: mapval_no_gen [incomplete]
  [aontu/mapval_no_gen]: Cannot resolve value at path $.user.id
  schema: schema.aon:1:12 (integer)
$ echo $?
3
```

Nothing contradicts here; the truth is simply not met yet, and the exit code says so:

| Exit | Verdict | What the pipeline should do |
| --- | --- | --- |
| 0 | `valid` | pass |
| 1 | `invalid` | fail: the data contradicts the schema; repair the data |
| 3 | `incomplete` | fail: no contradiction, but required truth is unmet |
| 4 | `error` | fail loudly: the _schema_ side is unusable, never the data’s fault |

(Exit 2 is usage: a bad flag or an unreadable file.) A blanket “non-zero is red” still gates correctly, but keeping 1 and 3 apart lets an emit → validate → repair loop route the failure: exit 1 means repair what was emitted, exit 3 means finish it, exit 4 means stop and page whoever owns the schema. The full classes are specified under [`aontu vet`](https://aontu.dev/docs/reference-api#aontu-vet).

## Reports for machines

`--format json` emits the same findings as one object; `--format sarif` emits SARIF 2.1.0, the form GitHub code scanning ingests:

```sh
$ aontu vet --format sarif schema.aon user.json
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "runs": [
    {
      "invocations": [
        {
          "executionSuccessful": true
        }
      ],
...
$ echo $?
1
```

The exit code still carries the verdict, so a job can upload the report and stay red.

## Wire it in

In GitHub Actions, the repository ships an action wrapping the verb, [`vet-action/`](https://github.com/aontu-lang/aontu/blob/main/vet-action/README.md), which fails the job by verdict class and can hand the SARIF to code scanning:

```yaml
steps:
  - uses: actions/checkout@v4
  - uses: aontu-lang/aontu/vet-action@main
    with:
      schema: schema.aon
      data: user.json
```

As a pre-commit hook, the verb is one line, and the verdict classes mean a half-finished document blocks the commit too:

```sh
#!/bin/sh
# .git/hooks/pre-commit
exec aontu vet schema.aon user.json
```

While editing, `--watch` re-runs the whole vet whenever the schema or a data file changes, streaming one report per run:

```sh
$ aontu vet --watch schema.aon user.json
```

Every run is a full re-parse and re-unify, so what you see on each save is exactly what CI will say.

## Make the gate prove it checked something

A gate that passes because it examined **nothing** is worse than no gate: it reports success. The usual cause is one construct, and it is silent. A schema written with the wildcard other tools use is a key **named** `*` in aontu, so it meets no data key and constrains nothing. Here it is as `wild.aon`:

```aontu
entity: { "*": { table: string } }
```

`rows.aon` is the data it was meant to check:

```aontu
entity: planet: table: 42
```

```sh
$ aontu vet --partial --strict-coverage wild.aon rows.aon
verdict: valid
...
coverage: VACUOUS — no data leaf was constrained by the schema; this run checked nothing
coverage: 0/1 data leaves checked, 3 schema declarations
  unchecked: $.entity.planet
  unused: $.entity.*
aontu: no data leaf was constrained by the schema: this run checked nothing
...
$ echo $?
1
```

`--strict-coverage` is the CI form: it measures what the run examined and exits 1 when the answer is nothing. The verdict word stays `valid`, because the unification really did hold; what failed is the gate’s claim to have checked anything. Written with the template `&:` the same run checks the field and refuses the `42`.

Use `--coverage` alone while writing a schema, to see which declarations no data meets and which data paths nothing constrains, and `--coverage-at` to measure one subtree.

Vetting gates the data; the schema itself changes too, and that gate is [`aontu breaking`](https://aontu.dev/how-to/gate-schema-changes). For decoding an individual finding, see [Read a conflict error](https://aontu.dev/how-to/read-a-conflict-error).
