Guides
OpenRTB validation in CI
Install the CLI with cargo install rtblint, run rtblint validate over your bid request fixtures, and fail the build on a nonzero exit code. That is the whole idea. The rest of this guide covers exit codes, JSON output, a GitHub Actions example, and why you should pin a spec snapshot.
Why validate fixtures in CI at all
Most bidder and SSP codebases carry a directory of bid request fixtures: golden files for integration tests, sample payloads for partners, request builders exercised in unit tests. Those files rot. Someone hand-edits one and typos a field name. A partner upgrades from 2.5 to a 2.6 snapshot and suddenly regs.ext.gdpr should be regs.gdpr and imp.video.placement should be plcmt. The 2.6 line itself moves: the tracked snapshots run from 2.6-202204 to 2.6-202505, and fields like regs.gpp (added in 2.6-202211) appear along the way.
The failure mode is silent. A request that drifted from the spec usually does not throw anywhere; the strictest partner just stops bidding on it, and you find out weeks later as a revenue dip. See OpenRTB no-bid reasons for how little signal comes back. A validation step in CI turns that silent drift into a red build at the pull request that introduced it.
Exit codes and JSON output
rtblint validate exits 0 when the request is valid (warnings are allowed), 1 when it has spec errors, and 2 on usage or I/O problems. That is enough for a plain shell gate. When you want to inspect or filter findings, --format json emits a machine-readable report:
{
"version": "2.6-202505",
"valid": false,
"issues": [
{
"id": "openrtb.field.moved",
"severity": "error",
"message": "regs.ext.gdpr moved in OpenRTB 2.6-202505; use regs.gdpr.",
"path": "regs.ext.gdpr"
}
]
}Each issue carries a stable rule id, a severity, a message, and the JSON path. The rule catalog documents the IDs.
GitHub Actions example
jobs:
validate-fixtures:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install rtblint
run: cargo install rtblint
- name: Validate bid request fixtures
run: |
status=0
for f in fixtures/bid-requests/*.json; do
rtblint validate "$f" --version 2.6-202505 || {
echo "FAILED: $f"
status=1
}
done
exit $statusThe loop keeps going after the first failure so one run reports every broken fixture. Cache the cargo registry and the installed binary (for example with Swatinem/rust-cache or a plain actions/cache on ~/.cargo) so the install step is only paid on cache misses. The same shape works in GitLab CI, Buildkite, or a Makefile; the contract is just the exit code.
Validating piped payloads
--stdin reads the request from the pipe instead of a file, which is handy when fixtures are generated, templated, or extracted from logs on the fly:
# validate a generated request without touching disk ./gen-request --seat 42 | rtblint validate --stdin --format json # spot-check a request pulled from a log line jq -c '.request' sampled.log | head -1 | rtblint validate --stdin
Pin a spec snapshot
Without --version, rtblint validates against the latest tracked 2.6 snapshot, which changes when a new spec release lands. In CI that means an rtblint upgrade can move the goalposts mid-week. Pin the snapshot instead:
rtblint validate request.json --version 2.6-202505
Now a spec upgrade is a deliberate one-line diff that reviewers can see, run, and discuss, exactly like bumping a dependency. When you do bump it, the new findings (a freshly deprecated field, a moved path) arrive in one reviewable batch.
Gating on rule IDs
Exit-code gating treats every error the same. With JSON output you can be more surgical, for example failing only on structural problems while tolerating a known deprecation you have scheduled for later:
rtblint validate "$f" --format json \
| jq -e '[.issues[]
| select(.severity == "error"
and .id != "openrtb.field.moved")]
| length == 0'Rule IDs such as openrtb.payload.invalid_json, openrtb.field.undefined, openrtb.field.deprecated, and openrtb.field.moved are stable, so allowlists survive upgrades. Keep the allowlist short and dated; it is a migration tool, not a mute button.
FAQ
Does rtblint need network access in CI?
No. The spec snapshots are compiled into the binary, so validation runs fully offline. The only network use is the one-time cargo install, which you can cache like any other build dependency.
How fast is it? Will it slow the pipeline down?
The validator is Rust operating on parsed JSON, and a single bid request validates in well under a millisecond of CPU time. Even a fixtures directory with thousands of requests finishes in seconds; the cargo install step dominates until you cache it.
Validate it
Try a fixture in the bid request tester first to see what the findings look like, then wire the same checks into CI with the CLI. If you are new to the request format itself, start with the bid request reference and the guide on validating a bid request.