AdCOM 1.0 JSON Schema Unofficial

How it's built

Postindustria, an IAB Tech Lab member, generates these schemas from the AdCOM 1.0 specification text. Nobody writes a schema file by hand. Every vocabulary 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 AdCOM's lists, and what it shapes in the type map and the files.

The pipeline

The same pipeline generates every specification on this site. For AdCOM's lists it runs every step but two: the lists need no constructors, and AdCOM 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

AdCOM writes its tables in HTML, not Markdown. The extractor reads each HTML table into the same rows a Markdown table gives, keeping each row's source bytes, and fails on any tag a table doesn't use, so no row is skipped silently. Each heading carries its own anchor, such as list_devicetypes, which every link to it names, so each section takes that anchor as its ID. The text adopts no RFC 2119 convention and writes its keywords in either case, so a keyword in any case makes a statement (D-001).

On main, the IR holds 303 list rows in 42 tables, 411 object attribute rows, and 266 statements. Each element has an ID, such as def:list_devicetypes:3: a value-definition row (def), in the section list_devicetypes, for the code 3.

Mapping and decisions

Every element has exactly one entry in the mapping, on each input. Of the 1013 entries across both inputs, 953 come from mapping rules and 60 cite a decision record.

  • A list is a vocabulary. A table of codes that a List: heading alone names becomes one vocabulary, named after the list's title in PascalCase with its head noun in the singular (D-002), and each code row becomes one of its terms, labeled with its row's text.
  • A vendor range is an open branch. A 500+ row opens its list to a vendor's code of 500 or more; a list without one stays closed (D-008).
  • The objects wait. Every element of the Object: sections is informative until a later phase maps the objects (D-003).
  • Decisions fill the gaps. 18 decision records settle what the text leaves open, from the start delays' >0 row (D-004) to the revision of record (D-016). Read them in the decision records.

What a list becomes

A list is type: integer and a oneOf of one const branch per code, titled with the code's name, so a reader of the schema alone sees what each code means. An open list adds one branch for its extension: an integer of at least 500 for a vendor range, as VendorCode is, or, for the start delay, a whole number of seconds other than 0, as MediaOffset is. The design notes say why.

Verification

  • The fidelity oracle. A separate program reads each list's rows from the IR alone, never the mapping, and checks the emitted schemas: every code valid, a code no row names invalid, the first vendor code valid and the code below it invalid. On main it checks every list with codes.
  • The round trip. Each list's table is rendered back from the model and compared with the text, cell by cell. The one recorded difference is the start delays' >0 row, which no single term can render.
  • Determinism and drift. Every generated file is written twice in separate processes and compared with the committed one, byte for byte, and each release manifest's digests are recomputed.
  • Known-bad fixtures. Every gate has fixtures it must reject, such as an HTML cell with a tag the reader doesn't know, or a vocabulary that lost a code.

The two inputs

AdCOM has changed its lists many times while calling every revision "1.0", and its last tag isn't main. So the registry is built twice: from main, the default, and from the last tag. The lock comparison shows that their lists are the same, code for code, and that they differ only in the Content object's table.

One list, end to end

  1. The specification row

    List: Device Types, the row of 3, L3401 at 26c59ba:

    3 | Connected TV
  2. The element

    The extractor records the row as def:list_devicetypes:3, with the digest 3263e9d33977c01ee1c4209782b35d2c93364d54fa588ecf61167c8def677da4. Every mapping entry and decision that rests on the row stores that digest, and goes stale when the row changes.

  3. The vocabulary

    The rule that maps a list's code rows makes the row a term of DeviceType, in the lists module, with the label "Connected TV".

  4. The emitted schema

    In schemas/modules/lists/1/lists.json, under $defs. Open it in the type map.

    {
      "description": "The codes of List: Device Types",
      "oneOf": [
        {
          "const": 1,
          "title": "Mobile/Tablet - General"
        },
        {
          "const": 2,
          "title": "Personal Computer"
        },
        {
          "const": 3,
          "title": "Connected TV"
        },
        {
          "const": 4,
          "title": "Phone"
        },
        {
          "const": 5,
          "title": "Tablet"
        },
        {
          "const": 6,
          "title": "Connected Device"
        },
        {
          "const": 7,
          "title": "Set Top Box"
        },
        {
          "const": 8,
          "title": "OOH Device"
        }
      ],
      "title": "List: Device Types",
      "type": "integer"
    }
  5. The example value

    A vocabulary's example is one of its codes. See it in the type map.

    1

Design notes: codes and flags

Integer codes carry their names

Every list here is integer codes, such as 3 for Connected TV in List: Device Types. 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 registry maps lists only, so no flag appears here yet; when the objects are mapped, a flag such as instl keeps the 0 | 1 the text writes.

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/adcom/spec-input/spec-lock.yaml and the other steps run on one.