OpenRTB is a JSON protocol with no official JSON Schema. The IAB publishes the specification as prose and tables, plus a protobuf definition, and that is it. If you want to validate a bid request structurally, you write the schema yourself.
So most teams have a partial one. It covers the objects they touch, it was correct against whichever version was current when someone wrote it, and it silently accepts everything it does not know about. It works right up until a partner sends a field type you never modelled.
I generate these from the spec as part of building an OpenRTB linter, and they are published as static files. You can use them without the linter, without Rust, and without me.
What is there
Thirty-four schemas, request and response, across seventeen versions:
2.0 2.1 2.2 2.3 2.3.1 2.4 2.5
2.6-202210 2.6-202211 2.6-202303 2.6-202309 2.6-202402
2.6-202409 2.6-202501 2.6-202505 2.6-202606
3.0
Each one is JSON Schema draft 2020-12 with a resolvable $id:
$ curl -s https://rtblint.org/schemas/openrtb-2.6-202606-bid-request.schema.json | jq '{$schema, $id, title, required}'
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://rtblint.org/schemas/openrtb-2.6-202606-bid-request.schema.json",
"title": "OpenRTB bid request (2.6-202606)",
"required": ["id", "imp"]
}
32 named definitions in that one (App, Audio, Banner, Channel, Content, DOOH, Data, Deal, Device, DurFloors, and so on), so you can $ref individual objects rather than pulling the whole request in.
The version matters more than you might expect. OpenRTB 2.6 ships as dated snapshots that add and occasionally rename fields, so "2.6" alone does not pin a field set. The schemas are per snapshot for that reason.
With ajv
Nothing special required. Point ajv at it and go:
import Ajv from 'ajv/dist/2020.js';
const schema = await (
await fetch('https://rtblint.org/schemas/openrtb-2.6-202606-bid-request.schema.json')
).json();
const ajv = new Ajv({ strict: false, allErrors: true });
const validate = ajv.compile(schema);
const req = {
id: '1',
imp: [{ id: '1', banner: { w: '300', h: 250 } }],
site: { id: 's' },
at: '1',
};
console.log(validate(req));
for (const e of validate.errors ?? []) console.log(e.instancePath, e.message);
false
/at must be integer
/imp/0/banner/w must be integer
Both of those are the classic OpenRTB failure: a numeric field arriving as a string. It is the single most common type error in real bid traffic, it is invisible to a lenient parser, and any partner enforcing the type drops you from the auction without telling you why.
Use strict: false. The schemas use constructs ajv's strict mode complains about, and the complaints are about the schema's style rather than its correctness.
Vendor the file rather than fetching it at runtime if this is on a hot path. It is 43 KB and it does not change unless you change versions.
In CI without writing any JavaScript
If all you want is a check on fixture files, check-jsonschema is a single step:
- run: pipx install check-jsonschema
- run: |
check-jsonschema \
--schemafile https://rtblint.org/schemas/openrtb-2.6-202606-bid-request.schema.json \
fixtures/requests/*.json
That catches drift in your own test fixtures, which is worth more than it sounds. Fixtures rot: someone hand-edits one to reproduce a bug, gets the type wrong, and the fixture now encodes an invalid request that your tests happily assert against forever.
What a schema cannot do
Being straight about the ceiling here, because pointing a schema at OpenRTB and declaring the problem solved is how you end up with the partial-schema situation again.
A schema checks one document's structure. It does not check:
Cross-field rules. site, app and dooh are mutually exclusive. A schema can express that with oneOf, awkwardly, but most of these rules are conditional in ways that get unreadable fast.
Cross-document rules. Whether a bid response's impid matches an imp in the request that produced it. The other document is not in scope, so it is unreachable in principle.
Enum membership across 500-plus documented values. Technically expressible, genuinely unpleasant to maintain by hand, and this is where AdCOM lists and vendor ranges live.
Deprecation and version drift. A field can be structurally perfect and removed three snapshots ago. Schemas say valid or invalid, not "valid but you should stop."
String contents. bid.adm is typed string, so a schema is finished with it. Whether the string contains the markup type that bid.mtype declares, or whether it got JSON-encoded twice on the way through your stack, is beyond the type system.
Those are the checks rtblint exists for, and it is the same catalog underneath, so the schema and the linter agree about what a version contains:
$ cargo install rtblint
$ rtblint validate --version 2.6-202606 request.json
The rules reference lists every check with its id and severity, and there is a browser tester if you would rather paste one payload than install anything.
Take the schemas
They are static files under rtblint.org/schemas/, no key and no rate limit. Use them in whatever validator you already have. If you find a field the spec has and the schema does not, that is a bug worth reporting, since both come out of the same extraction and a gap in one is usually a gap in both.
Top comments (0)