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 official IAB XSD for each file's declared version.
- 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 vastlint@0.13.12
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) -
vendoris 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) -
conditionalAdis 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.
Top comments (0)