How it's built
Postindustria, an IAB Tech Lab member, generates these schemas from the Deals API 1.0 specification text. Nobody writes a schema file by hand. Every schema is a function of two things: the specification at a pinned commit, and a mapping and a set of decision records that we review by hand. This page explains each step for the Deals API, and what it shapes in the type map and the files.
The pipeline
The same pipeline generates every specification on this site. The Deals API has no SDK types yet, since the schemas come first.
Extraction
The text writes each object as an Attribute | Type | Description table under a heading Object: Deal, and every section heading of the body follows an anchor, such as object-deal, which is the section's ID. A Type cell states the base type and, after it, the presence and the default, as int, default 0, recommended. A Description cell lists a code's meanings as n = label runs, most of them with the code in a code span. The text adopts no RFC 2119 convention and writes its keywords in lowercase, so a keyword in any case makes a statement (D-001).
On main, the IR holds 46 attribute rows in 7 tables, 10 code lists, and 54 statements. Each element has an ID, such as field:object-deal:sellerstatus: an attribute row (field) in the section object-deal.
Mapping and decisions
Every element has exactly one entry in the mapping, on each input. Of the 130 entries across both inputs, 53 come from mapping rules and 77 cite a decision record.
- An object no attribute holds is a document.
Object: DealandObject: BuyerSeatare the two payloads;termsholdsObject: Terms, so Terms is an object, named by its heading. - A row's concept types it. The kernel binding cites each row for a concept of the domain model, and the concept's type is the row's: an identifier is
ExternalId, a domainDomain, a UTC timestampUtcTimestamp(D-002). - A code list is a vocabulary. Each own list becomes one, named for what its codes are (D-031), each code labeled with its row's text.
- Decisions fill the gaps. 32 decision records settle what the text leaves open, from how an evergreen deal leaves
enddateblank (D-003) to which field "Price attribute" names (D-007). Read them in the decision records.
What a row becomes
An attribute is a property of its object: a $ref to its type, with its row's description and, where the Type cell states one, its default. A code list is type: integer and a oneOf of titled const branches, one per code (see the design notes); pricetype's list also takes an exchange's code from 500 up. 8 rules of the text that JSON Schema can't express, such as "at most one of wseat and bseat", are in the rules file beside the kernel's, each with valid and invalid vectors and its level: a should is a warning, a must an error.
Verification
- The fidelity oracle. A separate program reads each attribute row from the IR alone, never the mapping, and checks the emitted schemas: the presence, the type, and the default its Type cell states, and every code its Description cell lists.
- The round trip. Each attribute table is rendered back from the model and compared with the text, cell by cell. The recorded differences are Type cells whose wording the schemas don't keep, such as
integerbesideint(D-033), andguar's codes (D-018). - The registry agreement. Every list the text cites is AdCOM's, at the commit the registry pins, and no schema copies its codes.
- Determinism and drift. Every generated file is written twice in separate processes and compared with the committed one, byte for byte.
- Known-bad fixtures. Every gate has fixtures it must reject, such as a code list that lost a code, or a default the schema dropped.
The two inputs
The Deals API calls every revision "1.0", and its errata changed sellerstatus after the tag. So the schemas are built twice: from main, the default, and from the tag 1.0-202602. The lock comparison shows that they differ in that one list, where the tag drops code 3 and gives code 4 both meanings, and the release differ classes the change as breaking.
One attribute, end to end
The specification row
Object: Deal, the
sellerstatusrow, L88 at 3028ddc:sellerstatus| int, default 0, recommended | Status of the deal in the sellers system where:0= deal is active1= deal is paused2= deal is pending3= deal is complete (ie. selling scomplete (ie. selling system shows deal has completed)4= deal is archived (cannot be active again)The element
The extractor records the row as
field:object-deal:sellerstatus, with the digest0c8cf640c2946620c962742b4ddebacad40140806db8b1e55a56ac71589d4d9f. Every mapping entry and decision that rests on the row stores that digest, and goes stale when the row changes.The vocabulary
The row's code list becomes
SellerDealStatus, in thedealmodule, and the property takes its default, 0, from the Type cell;recommendedleaves it optional (D-029).The emitted schema
In
schemas/as-written/modules/deal/1/deal.json, under$defs. Open it in the type map.{ "description": "The codes of `Deal.sellerstatus`, each with the label the text gives it", "oneOf": [ { "const": 0, "title": "deal is active" }, { "const": 1, "title": "deal is paused" }, { "const": 2, "title": "deal is pending" }, { "const": 3, "description": "deal is complete (ie. selling scomplete (ie. selling system shows deal has completed)", "title": "deal is complete" }, { "const": 4, "title": "deal is archived (cannot be active again)" } ], "title": "Seller Deal Status", "type": "integer" }The example value
A vocabulary's example is one of its codes. See it in the type map.
0
Design notes: codes and flags
Integer codes carry their names
The text writes its own lists as integer codes, such as pricetype's 3 for Fixed Price, and cites two lists from AdCOM 1.0. A bare enum of the integers says which numbers are valid, but not what they mean. So every integer code list on this site is type: integer and a oneOf of one const branch per code, titled with the name its row gives, as in List: Device Types:
{
"type": "integer",
"oneOf": [
{
"const": 1,
"title": "Mobile/Tablet - General"
},
{
"const": 2,
"title": "Personal Computer"
},
{
"const": 3,
"title": "Connected TV"
}
]
}
It validates exactly as the enum would: no two branches match the same value. A reader of the schema alone sees what each code means, and a generator can name SDK constants from the titles deterministically, such as CONNECTED_TV = 3, with no hand-kept table. Where the text opens a vendor range, one more branch takes it: { "type": "integer", "minimum": 500, "title": "Vendor-specific" }. String vocabularies keep their enum, since a string term already names itself.
Flags keep the encoding of their source
A yes-or-no field keeps the encoding its text gives it, and a field copied from another IAB standard keeps that standard's. OpenRTB 2.6 and its object model write flags as 0 (no) or 1 (yes), and such a field is IntegerFlag: two titled branches, 0 "No" and 1 "Yes". The text writes guar as 0, not a guaranteed deal, or 1, a guaranteed deal, so it is IntegerFlag.
The trade-off: a JSON boolean reads more naturally and can't take a third value. But a copied key that turns into a boolean must be converted at every handoff, such as when a seller builds a bid request from a proposal, and the schema then contradicts the text it cites. Following the source keeps each value as the standard it comes from writes it.
How to reproduce it
The tooling is public at KonstantinMirin/openproposal-schema. It needs Node 22, and the gates also need Python 3.12 and Go 1.25 or later. npm run ci runs every gate on every specification, as CI does; npm run extract -- --lock specs/dealsapi/spec-input/spec-lock.yaml and the other steps run on one.