Deals API 1.0 JSON Schema Unofficial

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.

The pipeline: the specification is extracted into an intermediate representation, mapped with the value domains and the decision records, resolved into a model with the constructors and the variant switches, and emitted as the schemas, rules, SDK types, and examples that the site and the downloads serve. A verification step checks every output against the extracted text. Specificationmarkdown at a pinnedcommit and blob1 ExtractIR: each element withan ID, a quote, a digest3 Mapone entry per element,by rule or by decision4 Resolvethe model: layers,modules, named copies5 EmitJSON Schema, rules,SDK types, examplesSite and filesthe type map, theschema files2 Value domainsconcepts, primitives,reviewed firstDecision recordsgaps and departures,each quoting its linesConstructors andthe as-writtenswitches6 VerifyThe fidelity oracle reads the IR alone and checks every output. Every gate rejects its known-bad fixtures,and the specification's examples run against the schemas and the rules. generatedreviewed by handchecks every output The pipeline: the specification is extracted into an intermediate representation, mapped with the value domains and the decision records, resolved into a model with the constructors and the variant switches, and emitted as the schemas, rules, SDK types, and examples that the site and the downloads serve. A verification step checks every output against the extracted text. Specificationmarkdown at a pinnedcommit and blob1 ExtractIR: each element withan ID, a quote, a digest3 Mapone entry per element,by rule or by decision4 Resolvethe model: layers,modules, named copies5 EmitJSON Schema, rules,SDK types, examplesSite and filesthe type map, theschema files2 Value domainsconcepts, primitives,reviewed firstDecision recordsgaps and departures,each quoting its linesConstructors andthe as-writtenswitches6 VerifyThe fidelity oracle reads the IR aloneand checks every output. Every gaterejects its known-bad fixtures, and thespecification's examples run againstthe schemas and the rules. generatedreviewed by hand
Each step is a function of the steps before it and of the inputs we review by hand. The verification step reads the extracted text, not the mapping, so it can disagree with every step in between.

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: Deal and Object: BuyerSeat are the two payloads; terms holds Object: 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 domain Domain, a UTC timestamp UtcTimestamp (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 enddate blank (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 integer beside int (D-033), and guar'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

  1. The specification row

    Object: Deal, the sellerstatus row, L88 at 3028ddc:

    sellerstatus | int, default 0, recommended | Status of the deal in the sellers system where: 0 = deal is active 1 = deal is paused 2 = deal is pending 3 = deal is complete (ie. selling scomplete (ie. selling system shows deal has completed) 4 = deal is archived (cannot be active again)
  2. The element

    The extractor records the row as field:object-deal:sellerstatus, with the digest 0c8cf640c2946620c962742b4ddebacad40140806db8b1e55a56ac71589d4d9f. Every mapping entry and decision that rests on the row stores that digest, and goes stale when the row changes.

  3. The vocabulary

    The row's code list becomes SellerDealStatus, in the deal module, and the property takes its default, 0, from the Type cell; recommended leaves it optional (D-029).

  4. 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"
    }
  5. 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.