DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

I Built a VS Code Extension That Validates VAST XML as You Type

If you work in ad tech you've probably seen VAST XML. It's the IAB standard that carries every video and CTV ad impression: the tag that tells a player what to show, where to fire tracking pixels, and how to handle wrappers. $30B+ in annual US ad spend runs through it.

It's also surprisingly easy to ship a broken one. The spec is six versions deep (2.0 through 4.3), touches several other standards (XML, RFC 3986, IANA media types, ISO 4217), and the tooling for validating it has historically been: read the PDF, or find out from your partner's discrepancy report.

So I built vastlint, an open-source VAST linter with 108 rules written in Rust. Last week I shipped the VS Code extension.

What it does

Install it from the VS Code Marketplace. Open any .xml file that contains a VAST tag.

You get squiggles on the exact element or attribute that violates the spec, hover tooltips with the rule ID and the fix, and Problems panel integration with file path and line number. VAST version is auto-detected. No config, no CLI, no external service. The validator runs entirely in-process via WebAssembly.

What it actually catches

Here's a broken tag in the editor. This one is missing on the element, a required field since VAST 2.0 that's easy to drop when templating:

<Linear>
  <!-- Duration is required โ€” VAST-2.0-linear-duration -->
  <MediaFiles>
    <MediaFile delivery="progressive" type="video/mp4" width="640" height="360">
      <![CDATA[https://example.com/video.mp4]]>
    </MediaFile>
  </MediaFiles>
</Linear>
Enter fullscreen mode Exit fullscreen mode

The squiggle lands on . Hovering shows:

VAST-2.0-linear-duration ยท Error
<Linear> is missing required <Duration>
IAB VAST 2.0 ยง3.7.2
Enter fullscreen mode Exit fullscreen mode

Other rules are less obvious than a missing required element:

  • VAST-2.0-mediafile-https: HTTP URL on HTTPS inventory (mixed-content, ad won't play)
  • VAST-2.0-duration-format: is 00:30 instead of 00:00:30 (strict players reject this, others misfire tracking events at the wrong times)
  • VAST-4.1-mezzanine-recommended: no on a 4.1 tag (SSAI platforms reject these for CTV)
  • VAST-4.1-vpaid-apiframework: VPAID apiFramework in a 4.1 tag (deprecated, Roku and Fire TV skip it)
  • VAST-2.0-duplicate-impression: same impression URL appears twice (both sides double-count, neither report is technically wrong, they just never match)

The core library is vastlint-core on crates.io, a pure Rust library with no external dependencies. The VS Code extension loads it as a WebAssembly module so validation runs locally with no network calls.

string in -> parse XML -> detect VAST version -> run rule set -> issues out
Enter fullscreen mode Exit fullscreen mode

Typical validation time on a real-world CTV tag is under 100 microseconds. The WASM overhead adds a bit in the editor context but it's imperceptible while typing.

The same core powers a CLI (cargo install vastlint), a REST API on RapidAPI, an MCP server for Claude/Cursor/Windsurf, and the online validator at vastlint.org.

Per-rule configuration

Not every rule applies to every context. Mezzanine is noise for browser-only inventory but critical for Roku. You can tune per-rule in settings.json:

{
  "vastlint.rules": {
    "VAST-4.1-mezzanine-recommended": "error",
    "VAST-2.0-mediafile-https": "error",
    "VAST-4.1-vpaid-apiframework": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Valid levels: "error", "warning", "info", "off".

Why I built it

I ran into this problem at work. The available tooling was either proprietary, behind a paywall, or only checked structure against the XSD. The spec is public. The failure modes are documented. There was no good reason an open-source implementation didn't exist.

The other thing: these errors get caught way too late. A malformed tag makes it through QA, gets sent to the partner, fails on their platform, and shows up three weeks later in a discrepancy report. The campaign has already run, the impressions are gone. Catching it in the editor before the file leaves your machine is the right fix.

Try it

Top comments (0)