Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

The IAB's own VAST sample tags pass XSD validation, and two of them still break the spec

If you build a video player, an ad server, or anything that touches VAST, the IAB Tech Lab VAST_Samples repo is probably where you got your test fixtures. I ran every file in it through two checks: xmllint against the

If you build a video player, an ad server, or anything that touches VAST, the IAB Tech Lab VAST_Samples repo is probably where you got your test fixtures. I ran every file in it through two checks:

  1. xmllint against the official IAB XSD for each file's declared version.
  2. vastlint, an open-source linter with 235 rules taken from the XSDs and from the normative text of the spec (the RFC 2119 "must" and "required" sentences).

The short version: 75 files, 4 with spec errors, 72 with at least one warning, and 2 files that pass the XSD while breaking the VAST 4.1 spec text. The full write-up, including a production sample where 21.5% of 694 live tags couldn't play or be counted, is on vastlint.org: VAST Tag Benchmark 2026.

This post is the part you can rerun yourself.

Reproduce it

git clone --depth 1 https://github.com/InteractiveAdvertisingBureau/VAST_Samples.git
npm init -y >/dev/null && npm i [email protected]
curl -sO https://vastlint.org/data/vast-benchmark-2026/bench.mjs
node bench.mjs VAST_Samples

Output on my machine (Apple M4, Node 22.16):

error  VAST 1-2.0 Samples/Tremor-Video-Samples/vast2Nonlinear.xml  VAST-2.0-inline-impression
error  VAST 1-2.0 Samples/Tremor-Video-Samples/vast2VPAIDLinear.xml  VAST-2.0-inline-impression, VAST-2.0-mediafile-delivery
error  VAST 4.1 Samples/Ad_Verification-test.xml  VAST-4.1-verification-vendor, VAST-4.1-js-resource-apiframework
error  VAST 4.2 Samples/Ad_Verification-test.xml  VAST-4.1-verification-vendor, VAST-4.1-js-resource-apiframework

75 files, 4 with errors, 72 with warnings
most common warnings (files):
  63  VAST-2.0-tracking-https
  45  VAST-3.0-bitrate-conflict
  28  VAST-2.0-url-cdata
  17  VAST-4.0-conditionalad
  6  VAST-2.0-root-element

30000 validations, 19563 docs/s
p50 50.8 µs, p95 83.3 µs, p99 176.5 µs

The core of the script is tiny:

import { createRequire } from "node:module";
const { validate } = createRequire(import.meta.url)("vastlint");

const { summary, issues } = validate(xml);
for (const i of issues.filter((i) => i.severity === "error")) {
  console.log(i.id, i.path, `line ${i.line}`, i.spec_ref);
}

(The package's ESM entry is built for bundlers, so in plain Node I load the CommonJS build through createRequire.)

The two files the schema waves through

Here is the first of the two verification blocks in VAST 4.1 Samples/Ad_Verification-test.xml:

<AdVerifications>
  <Verification>
    <JavaScriptResource>
      <![CDATA[https://verificationcompany1.com/verification_script1.js]]>
    </JavaScriptResource>
  </Verification>
</AdVerifications>

xmllint is happy with it:

xmllint --noout --schema vast_4.1.xsd "VAST_Samples/VAST 4.1 Samples/Ad_Verification-test.xml"
# ... validates

That's because the published VAST 4.1 XSD declares both attributes like this:

<!-- vast_4.1.xsd, on <JavaScriptResource> -->
<xs:attribute name="apiFramework" type="xs:string" use="optional" />
<!-- vast_4.1.xsd, on <Verification> -->
<xs:attribute name="vendor" type="xs:string" use="optional">

But the VAST 4.1 specification text says otherwise. Section 3.17 lists vendor on <Verification> as required, with a recommended format like company.com-omid. Section 3.17.1 lists apiFramework on <JavaScriptResource> as required. What the spec actually asks for:

<AdVerifications>
  <Verification vendor="verificationcompany1.com-omid">
    <JavaScriptResource apiFramework="omid" browserOptional="true">
      <![CDATA[https://verificationcompany1.com/verification_script1.js]]>
    </JavaScriptResource>
  </Verification>
</AdVerifications>

This matters in practice. The vendor key is how a player or an Open Measurement integration decides whose script it is about to run and whether that vendor is allowed at all. The spec's verificationNotExecuted event even has a reason code for a vendor the publisher doesn't recognize or allow. A <Verification> with no vendor gives the player nothing to make that call with.

The VAST 4.2 sample has the same gap, because the 4.2 XSD inherits the same declarations.

Why schema validation can't catch this

An XSD describes structure: which elements may appear, in what order, how many times, and with what attribute types. A lot of what makes a VAST tag work lives outside that, in sentences:

  • an <InLine> must have an <Impression> (that one the XSD does catch)
  • vendor is required on <Verification> (the 4.1 XSD doesn't encode it)
  • tracking URLs over plain http:// get blocked as mixed content on HTTPS pages and on many CTV platforms (no schema can express that)
  • conditionalAd is deprecated as of 4.1 (no schema flags deprecation)

Here is what each check found on the same corpus:

Check xmllint + IAB XSD vastlint 0.13.12
Files checked 69 (VAST 1.0 has no XSD) 75
Files failing 2 4
VAST 2.0 InLine without Impression, MediaFile without delivery caught caught
4.1/4.2 Verification without vendor / apiFramework passes caught
Tracking or click URL over HTTP not checked warning on 63 files
conditionalAd (deprecated in 4.1) not checked warning on 17 files

The two Tremor VAST 2.0 samples are missing elements the 2.0 schema requires, and both tools catch them. There's an open PR (#47) on the repo to fix them.

Does checking the prose cost more?

Not meaningfully, at least on real-sized tags (these files run from 444 bytes to 7.5 KB, median 2.9 KB):

Docs/sec, one thread Work per document
vastlint (Rust compiled to WASM, in Node 22) ~19,800 parse + 235 rules
xmllint (libxml2 2.9.13, native C) ~21,500 one XSD pass

Median latency for vastlint was 50 µs per document, p99 175 µs. So "we only run the XSD because it's fast" doesn't hold up: the full spec check costs about the same.

Put it in CI

If you keep VAST fixtures or templates in a repo, this is the whole setup:

- uses: actions/checkout@v4
- name: Validate VAST tags
  uses: aleksUIX/vastlint-action@v1
  with:
    path: tags/**/*.xml
    fail-on-warning: "true"

Or in a Node test:

const { validate } = require("vastlint");
test("fixture is valid VAST", () => {
  expect(validate(xml).summary.errors).toBe(0);
});

What about real production tags?

Sample repos are tidy. Production isn't. In the full benchmark I looked at 694 tags that people pasted into or fetched through vastlint.org's tools between July and September 2026. 21.5% had a defect that stops an ad from playing or being counted, and the single biggest class (11.7%) was a response that wasn't readable VAST at all. That sample is self-selected (people check tags when something is already wrong), so read it as "how often a tag that reaches QA is broken," not as a market rate. The defect breakdown, the top 12 error rules, and the caveats are all in the write-up:

VAST Tag Benchmark 2026: 694 live tags, the IAB's 75 samples, and what XSD validation misses

The raw data is public if you want to slice it yourself: per-file IAB results and production aggregates.

If you've hit a case where a tag validated against the XSD and still broke in a player, I'd like to hear about it in the comments.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.