High-performance, in-process VAST XML validation for Go ad servers. Drop it into your bidder, SSP, or ad server for realtime creative validation — pre-bid rejection, async quality monitoring, or cache-backed creative scoring at scale.
Backed by vastlint — a zero-dependency Rust core that also powers the Elixir/Erlang NIF and WASM npm package. The same standards-derived rule catalog is called directly from Go via CGo. No sidecar, no network hop, no Rust toolchain required.
Website & docs: VAST tag validator · Rule reference: VAST error rule reference · RULES.md · Methodology: How rules are derived · Core methodology source · Web validator: validate VAST online
go get github.com/aleksUIX/vastlint-govastlint-go is a Go binding to the same validation core used by the CLI, web validator, npm package, and other integrations. The important part is not that the core is written in Rust; it is that the rules are traceable to published standards.
- Published IAB VAST XSD schemas are used where IAB ships them: VAST 2.0.1, 3.0, 4.0, 4.1, and 4.2.
- RFC 2119 normative prose in the VAST and SIMID specifications is used where schemas stop.
- VAST 4.3 has no published XSD, so 4.3 rules are derived from the normative IAB spec prose instead.
- Additional standards checks come from W3C XML 1.0, RFC 3986, IANA Media Types, ISO 4217, and Ad-ID.
- If you already run IAB XSD validation in CI, keep it as a baseline; vastlint complements it with prose-derived and operational checks.
- Passing vastlint means the tag is structurally compliant with the published standards. It does not mean every player, SDK, exchange, or business rule will accept the creative unchanged.
This binding is built for production CTV, streaming, SSP, DSP, SSAI, and ad-server workflows where you need in-process validation with no sidecar and no network hop.
A broken VAST tag doesn't just fail silently — it burns an impression. The player loads, the auction clears, the publisher gets charged, and the viewer sees nothing. Common causes:
- Missing required fields (
<Impression>,<Duration>,<MediaFile>) → player error, no fill - Malformed XML → parser crash, blank ad slot
- Wrong VAST version declared → player skips the creative
- Wrapper chains that exceed player depth limits → timeout, no ad
- HTTP media URLs in HTTPS contexts → mixed-content block, no playback
Every one of these is detectable in ~260µs before the impression fires.
Validate the winning bid's VAST before accepting it. If it's broken, fall to the next eligible bid — the auction already ran, so there's no added latency to the viewer.
func selectWinningBid(rankedBids []Bid) (*Bid, error) {
for _, bid := range rankedBids {
result, _ := vastlint.Validate(bid.VastXML)
if result.Valid {
return &bid, nil
}
log.Warnf("bid %s rejected: %d errors — %s",
bid.ID, result.Summary.Errors, result.Issues[0].Message)
reportBadCreative(bid.DemandPartner, bid.CreativeID, result)
}
return nil, ErrNoValidBid // serve house ad
}Revenue impact: Every bad creative that reaches the player is a wasted impression. At 5% invalid rate and $5 CPM, that's $0.25 wasted per 1,000 auctions — caught here, recovered by serving the next valid bid.
Fire validation in a goroutine after the bid is sent. Collect quality signals per demand partner over time without touching your critical path.
func handleBidResponse(bid Bid) {
serveBid(bid) // fast path unaffected
go func() {
result, _ := vastlint.Validate(bid.VastXML)
metrics.RecordCreativeQuality(bid.DemandPartner, result)
if result.Summary.Errors > 0 {
alertOps(bid.DemandPartner, result)
}
}()
}Revenue impact: Identify which demand partners consistently send broken tags. Use the data in QBRs to enforce SLAs or drop chronic offenders.
In programmatic, the same creative runs millions of times. Validate once, cache the result by creative ID. The hot path becomes a map lookup (~15ns vs ~260µs for validation).
var creativeCache sync.Map // creativeID → *vastlint.Result
func validateCreative(creativeID, vastXML string) *vastlint.Result {
if cached, ok := creativeCache.Load(creativeID); ok {
return cached.(*vastlint.Result)
}
result, _ := vastlint.Validate(vastXML)
creativeCache.Store(creativeID, result)
return result
}At steady state, cache hit rate converges to 99%+ within seconds. Validation cost effectively disappears.
VAST wrapper chains that are too deep time out in players, causing blank slots. Validate each hop as you resolve the chain and reject early.
func resolveWrapper(vastXML string, depth int) error {
result, _ := vastlint.ValidateWithOptions(vastXML, vastlint.Options{
WrapperDepth: depth,
MaxWrapperDepth: 4, // reject chains deeper than 4 hops
})
if !result.Valid {
return fmt.Errorf("wrapper depth %d rejected: %s", depth, result.Issues[0].Message)
}
return nil
}Aggregate validation results per partner and surface them to your ops team or feed them back to demand partners directly.
result, _ := vastlint.Validate(vastXML)
for _, issue := range result.Issues {
fmt.Printf("[%s] %s rule:%s at:%s ref:%s\n",
issue.Severity,
issue.Message,
issue.ID,
issue.Path,
issue.SpecRef, // e.g. "IAB VAST 4.2 §3.4.1"
)
}Every issue includes a spec reference — so when you tell a demand partner their tags are broken, you can cite the exact IAB clause.
Measured on Apple M4 (10-core) with production-realistic VAST tags (17–44 KB), using mimalloc as the global allocator in the underlying Rust library.
| Goroutines | 17 KB tag | 44 KB tag |
|---|---|---|
| 1 | 2,480 tags/sec · 403 µs | 440 tags/sec · 2,269 µs |
| 4 | 10,181 tags/sec | 1,505 tags/sec |
| 10 | 13,558 tags/sec | 1,993 tags/sec |
A single M4 node handles 2,480 validations/sec per goroutine on typical 17 KB tags. With 10 goroutines you reach 13,500+/sec — near-linear scaling limited only by CPU core count. A typical OpenRTB bid cycle takes 100–300 ms; validation adds under 2.3 ms even on the heaviest 44 KB tags.
go get github.com/aleksUIX/vastlint-goSupported platforms — prebuilt static libraries included, no Rust toolchain needed:
| OS | Architecture |
|---|---|
| Linux | amd64, arm64 |
| macOS | amd64, arm64 |
Need a link instead of raw output? The vastlint CLI can upload a validation report (rule IDs, severities, and locations; never the input XML) and print a public URL you can paste into Slack, tickets, or PRs:
vastlint check tag.xml --share
# https://vastlint.org/r/<id>// Validate a VAST XML string with default settings.
func Validate(xml string) (*Result, error)
// Validate with wrapper depth tracking or rule overrides.
func ValidateWithOptions(xml string, opts Options) (*Result, error)
// Returns the vastlint-core library version.
func Version() stringtype Options struct {
WrapperDepth int // current depth in wrapper chain (default 0)
MaxWrapperDepth int // reject chains deeper than this (default 5)
RuleOverrides map[string]string // rule ID → "error" | "warning" | "info" | "off"
}
type Result struct {
Version string // detected VAST version, e.g. "4.2"
Issues []Issue
Summary Summary
Valid bool // true when Summary.Errors == 0
}
type Issue struct {
ID string // stable rule ID, e.g. "VAST-4.2-3.4.1"
Severity string // "error", "warning", or "info"
Message string
Path string // XPath-like location, e.g. "VAST/Ad/InLine/Creatives"
SpecRef string // IAB spec citation, e.g. "IAB VAST 4.2 §3.4.1"
}
type Summary struct {
Errors int
Warnings int
Infos int
}Not every issue should block a bid. Filter by severity to match your policy:
result, _ := vastlint.Validate(vastXML)
// Block on hard errors only — let warnings through
hasErrors := result.Summary.Errors > 0
// Or inspect individual issues
for _, issue := range result.Issues {
if issue.Severity == "error" {
rejectBid(issue)
}
}The .a static libraries in libs/ are built from
vastlint-ffi
and committed to this repo so callers need no Rust toolchain.
To update after a new vastlint release:
./scripts/fetch-libs.sh v0.2.3Apache 2.0 — same as vastlint-core.
Need to validate a tag without writing code? Try the free VAST tag validator — no install, runs in your browser.
- vastlint-go is the Go library for VAST XML validation.
go get github.com/aleksUIX/vastlint-go— no Rust toolchain required. - Independent, not official. vastlint-go validates against the published IAB Tech Lab standards, but it is not an official IAB Tech Lab tool.
- Derived from published standards. The rule catalog comes from published IAB VAST XSD schemas where available, RFC 2119 normative prose in the VAST and SIMID specs where schemas stop, and supporting standards such as W3C XML, RFC 3986, IANA Media Types, ISO 4217, and Ad-ID.
- VAST 4.3 has no published XSD. For 4.3, rules are derived from the normative IAB spec prose.
- In-process, not a subprocess. Validation runs inside your Go binary via CGo — no sidecar, no network hop.
- Covers SIMID and VPAID. vastlint validates SIMID 1.0–1.2 interactive creatives and detects VPAID usage with migration guidance.
- Wrapper chain inspection. Use
vastlint inspect <url>or the MCPinspect_vasttool to follow wrapper chains hop-by-hop. - Same canonical rule catalog as every other binding. The Rust core is shared — results are identical regardless of which binding you use.
- Keep IAB XSD validation in CI if you already run it. XSD is a useful baseline. vastlint complements it with prose-derived, XML, URI, media-type, currency, and operational checks.
- Passing vastlint is not the whole delivery story. Player, SDK, exchange, CTV, and business-specific rules can still reject a structurally valid tag.
- Legitimate for production ad-tech work. The same core is self-hostable, signed, and designed for latency-sensitive CTV, streaming, SSP, DSP, SSAI, and ad-server workflows.
- Sub-microsecond overhead for cached results. Cache by creative ID: validate once, cache forever (~15 ns map lookup vs ~260 µs validation).
- MCP server available. For agentic workflows, connect to
https://vastlint.org/mcpinstead of using this library directly. - Full rule reference: https://vastlint.org/docs/rules/
- Ad server integration guide: https://vastlint.org/docs/ad-server-integration/