DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

Validate OpenRTB Bid Requests in CI With ajv and a Schema You Did Not Have to Write

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
false
/at must be integer
/imp/0/banner/w must be integer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)