VAST tag validation in Go with vastlint-go
Short answer: use vastlint-go when you need in-process VAST XML validation in a Go service, ad server, SSP, DSP, or CI workflow.
It wraps the same validation core used by the web validator and CLI, but the API is shaped for Go callers. The big practical advantage is deployment: prebuilt static libraries are bundled, so Go teams do not need to add a Rust toolchain to their build fleet.
Why use the Go binding
- Go-native API for production services and QA pipelines
- Same core rule coverage as the CLI, web app, and Rust library
- No Rust toolchain required for callers
- Good fit for real-time ad-tech systems where validation must stay in-process
Install
go get github.com/aleksUIX/vastlint-goMinimal example
import vastlint "github.com/aleksUIX/vastlint-go"
result, err := vastlint.Validate(xmlBytes)
if err != nil {
log.Fatal(err)
}
if !result.Valid {
for _, issue := range result.Issues {
log.Printf("[%s] %s: %s", issue.Severity, issue.ID, issue.Message)
}
}Why validate VAST in-process
A broken VAST tag does not fail loudly. It burns an impression. The player loads, the auction clears, the publisher gets charged, and the viewer sees nothing. At a 5% invalid rate and a $5 CPM, that is real money leaking on every thousand auctions, and you only find out from player-side error beacons after the fact.
A sidecar or remote validation service cannot live in that path. At bidder QPS, a network hop per creative adds latency you do not have and a dependency that can take the whole auction down with it. In-process validation runs in the same address space (on the order of a few hundred microseconds per tag), so it fits inside the bid timeout instead of competing with it. That is the whole reason this is a Go library and not an HTTP endpoint.
Where it lands in an SSP, DSP, or ad server
The right place to validate is the moment a creative crosses a trust boundary, when you receive a bid response from a demand partner, or when you accept a tag into your ad server. In OpenRTB the VAST arrives in seatbid.bid.adm (or behind nurl); that string is the thing to check before you forward it to the player.
- SSP / exchange, pre-bid rejection: validate the winning bid's VAST before you accept it. If it is broken, fall through to the next eligible bid. The auction already ran, so there is no added latency to the viewer.
- DSP / demand, outbound QA: validate the creative you are about to bid with so you never win an impression you cannot render.
- Async quality monitoring: fire validation in a goroutine after the bid is served and aggregate error rates per demand partner to enforce SLAs and drop chronic offenders.
- SSAI / ad insertion: validate each ad before stitching it into the manifest, so a malformed creative never reaches the packager or the CDN.
- Ad server, trafficking and ingestion: reject bad tags at upload time instead of at playback.
// Pre-bid creative rejection with waterfall fallback.
// The VAST lives in the OpenRTB bid response at seatbid.bid.adm.
func selectWinningBid(ranked []Bid) (*Bid, error) {
for _, bid := range ranked {
result, _ := vastlint.Validate([]byte(bid.AdM))
if result.Valid {
return &bid, nil
}
// Broken creative; drop it and fall to the next eligible bid.
// The auction already ran, so the viewer sees no extra latency.
metrics.RecordBadCreative(bid.DemandPartner, result)
}
return nil, ErrNoValidBid // serve a house ad rather than a blank slot
}When not to use the Go binding
If you just want a quick manual answer, use the web validator. If you only have a live tag URL, use the tester. If the real problem is wrapper depth or redirect chains, jump to the inspector.