DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

Your Bid Request Says secure: 1. The Protobuf Parser Wants true.

Ask an OpenRTB engineer what imp.secure holds and you get 0 or 1, because that is what the specification says. Ask someone who compiled com.iabtechlab.openrtb.v2 and you get true or false, because that is what the proto declares.

Both are right about their own transport. The payload carries no marker saying which one it is. One of the two is unparseable to the other side.

This is not a style disagreement. Protobuf JSON has no integer-to-bool coercion. "secure": 1 does not become true. The unmarshal fails, and it fails for the whole message rather than the one field.

Two encodings of the same 28 fields

The OpenRTB spec types a family of flag fields as integers with the value set {0, 1}. The IAB OpenRTB protobuf schema, which is what every gRPC bidstream integration compiles against, declares them bool. Diffing the two, object by object, produces 28 disagreements. They are not obscure corners.

Object Fields the proto declares bool
Imp secure, instl, clickbrowser, rwdd
Banner topframe, vcm
Video skip, boxingallowed
Audio stitched
Pmp private_auction
Site mobile, privacypolicy
App paid, privacypolicy
Content livestream, sourcerelationship, embeddable
Device dnt, lmt, js, geofetch
UserAgent mobile
Regs coppa, gdpr
Source fd
SupplyChain complete
SupplyChainNode hp
SeatBid group

regs.coppa and regs.gdpr are regulatory signals. imp.secure decides whether a creative can load. pmp.private_auction decides whether the auction is open. schain.complete and hp are the fields the supply-chain check sits on.

Same bytes, opposite verdicts

Here is a spec-JSON request. It is correct OpenRTB:

{
  "id": "req-1",
  "imp": [{
    "id": "imp-1",
    "secure": 1,
    "instl": 0,
    "banner": { "w": 300, "h": 250, "topframe": 1 },
    "pmp": { "private_auction": 1 }
  }],
  "site": { "id": "site-1", "domain": "news.example", "mobile": 0 },
  "regs": { "coppa": 0, "gdpr": 1 }
}
Enter fullscreen mode Exit fullscreen mode

I maintain RTBlint, an open source OpenRTB linter. Against spec JSON it is clean. Against protobuf JSON it is not a type mismatch in the ordinary sense. It is a dialect error, and the message says so:

$ rtblint validate --dialect spec-json bid.json
OK (OpenRTB 2.6-202606 bid request): no issues found.

$ rtblint validate --dialect proto-json bid.json
FAILED (OpenRTB 2.6-202606 bid request): 7 error(s), 0 warning(s).
- [error] imp[0].secure: imp[0].secure is 1, but the IAB protobuf
  schema declares Imp.secure as bool, and protobuf JSON accepts
  only true or false there. A protojson parser rejects this payload
  outright. (openrtb.dialect.integer_for_bool) ยท spec 3.2.4
Enter fullscreen mode Exit fullscreen mode

Flip every flag to a boolean and the verdicts swap. openrtb.dialect.bool_for_integer fires on spec JSON; openrtb.dialect.integer_for_bool fires on protobuf JSON. The serializer in both cases is doing what it was asked. The mistake is the assumption about who reads the next hop.

A typed JSON consumer that sees "secure": true against the spec's own types either rejects the value or binds a default and continues. The second case is worse. The reported error, when there is one, is a plain type mismatch, which sends an engineer to audit code that is correct.

The silent one is the name, not the type

Protobuf JSON emits lowerCamelCase unless the serializer sets UseProtoNames. So private_auction serialises as privateAuction, and us_privacy as usPrivacy.

A protobuf reader accepts both spellings. A protobuf-to-protobuf hop works and nothing complains. Hand that payload to a spec-JSON consumer and the field is not wrong, it is absent: a private auction reads as an open one, a privacy string goes unread, no error anywhere.

RTBlint reports the protobuf spelling as openrtb.dialect.camel_case_name (a warning, because the proto reader will still understand it) and as openrtb.field.undefined on spec JSON (an error, because that reader will not).

ARTF's own Go server sets UseProtoNames. If you serialise OpenRTB through protojson, do the same.

This is why ARTF samples disagree with each other

The Agentic Real Time Framework carries an OpenRTB payload inside a protobuf envelope and hands it to an agent. The transport is protobuf JSON by construction. The reference implementation ships five sample envelopes. Validated as protobuf JSON, two of them cannot be parsed at all: they write device.js: 1 and regs.coppa: 0. The other three write private_auction: true and coppa: false, which is correct for the envelope and wrong if you read the same file as an OpenRTB bid request.

So the corpus does not have a bug in it. It has both encodings in it, in different files, with nothing marking which is which. That is what an undeclared dialect looks like once a specification has more than one implementation. A patch for the sample encodings is PR #13 upstream.

ARTF envelopes in RTBlint do not take --dialect. --type artf-request already knows the payload is protobuf JSON, which is why the mixed-flag sample fails with openrtb.dialect.integer_for_bool on bid_request.imp[0].secure rather than with a generic type error.

What to do

Decide the dialect per hop, and write it down. A JSON exchange boundary is spec JSON. A gRPC extension point is protobuf JSON. Convert at the boundary, once.

Do not let a type error stand in for a dialect error. "Expects integer but received boolean" sends someone to the wrong file. Naming the other encoding gets the fix right on the first try.

$ cargo install rtblint
$ rtblint validate --dialect spec-json bid.json
$ rtblint validate --dialect proto-json grpc-bid.json
$ rtblint validate --type artf-request rtb-request.json
Enter fullscreen mode Exit fullscreen mode

The same checks run in the browser tester, as MCP tools an agent can call on its own output, and over gRPC on openadtech.rtblint.v1, which is the wire an ARTF host already has open.

RTBlint is independent of IAB Tech Lab. The 28-field list comes from diffing the published spec against the published proto; if a field is in one and not the other, that is a bug worth reporting.

Top comments (0)