fix(schemas): accept ad-server macros in VAST/DAAST tag URLs#5504
Merged
EmmaLouise2018 merged 2 commits intoJun 13, 2026
Conversation
Real-world VAST tag URLs carry unsubstituted macros — [OMIDPARTNER]-style
VAST macros and ${GDPR_CONSENT}-style privacy macros — whose square
brackets and curly braces are illegal in strict RFC 3986 URIs. The
vast-asset and daast-asset url fields used format: "uri", so any
verification-wrapped CTV tag failed create_media_buy validation even
though the tag is valid per the IAB VAST spec (players substitute
macros before treating the string as a URL).
Switch both fields to format: "uri-template" (RFC 6570), the same
convention url-asset.json already uses for AdCP universal macros.
RFC 6570 permits [...] as literals and parses ${MACRO} as a literal
$ plus a {MACRO} expression, so macro-laden tags validate while
malformed strings (raw spaces, control chars) are still rejected.
Adds a regression test validating a real IAS-wrapped CTV tag against
both asset schemas, with a negative case for malformed URLs.
Contributor
There was a problem hiding this comment.
Correct fix for the right reason. format: "uri" was the wrong shape for a field that holds a pre-substitution template — the player resolves macros before dereferencing, so the stored tag was never meant to be a syntactically valid RFC 3986 URI.
Things I checked
- Precedent is real.
url-asset.json:16already usesformat: "uri-template"for AdCP universal macros (release-notes #2801, the identicaluri→uri-templaterelaxation). This aligns the tag assets with established repo convention rather than inventing a mechanism. - Non-breaking →
patchis correct. Every formerly-valid RFC 3986 URI still validates under RFC 6570 (literals are a superset for the back-compat direction that matters). Pure relaxation, no previously-accepted value newly rejected.ad-tech-protocol-expert: sound-with-caveats;code-reviewer: clean. oneOfis untouched and stays discriminated (propertyName: delivery_type,vast-asset.json). No undiscriminated oneOf added, no walker regression.- Test 12B tightly proves the constraint. The negative fixture (
url: 'not a valid uri template') is fully valid except the raw space, and ajv-formats'uri-templateregex excludes\x00-\x20— so only theurlformat can reject it. It can't false-pass. Every${...}token in the positive fixture is[A-Z0-9_](valid RFC 6570 expressions); brackets are permitted literals. Genuine validation, not a silent pass. - Changeset present and correctly typed (
.changeset/vast-daast-tag-url-macro-validation.md,patch).
Follow-ups (non-blocking — file as issues)
- mdx sync gap.
docs/creative/asset-types.mdx:306(VAST) and:394(DAAST) still read "URL endpoint that returns VAST/DAAST XML" with no mention that macros are accepted as-is or that buyers MUST NOT pre-encode delimiters. The schema descriptions now carry that guidance; the human-facing reference should mirror it per the schema↔mdx sync invariant. Accurate-but-incomplete, not drift — doesn't block. - Surface the
%%MACRO%%limitation where integrators look. The GAM%%...%%gap (bare%illegal under RFC 6570) lives only in the changeset. An integrator hitting that validation failure won't read changesets — consider a line in the schema description or docs.
Minor nits (non-blocking)
- Strengthen the negative case. Test 12B's malformed fixture proves spaces are rejected; adding a GAM
%%CACHEBUSTER%%URL as a second rejection would pin the documented known-limitation in the test, so a future format widening can't silently regress it.tests/schema-validation.test.cjs~L1668.
The test plan reports test:schemas 19/19 and build:schemas clean; neither expert could execute the suite locally (no installed deps). Ship it once CI validates green.
Approving on the strength of the #2801 precedent plus a regression test that can't false-pass.
BaiyuScope3
approved these changes
Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
create_media_buyrejects valid VAST tags:The failing tag was a standard verification-wrapped CTV tag (IAS wrapping an Extreme Reach VAST endpoint) carrying unsubstituted macros:
vast-asset.jsonanddaast-asset.jsonvalidate thedelivery_type: "url"branch'surlwithformat: "uri", i.e. strict RFC 3986. Square brackets are only legal in a URI as IPv6 authority delimiters, and curly braces are not legal anywhere unencoded — so[OMIDPARTNER]and${GDPR}fail. But these are exactly the macro conventions the IAB VAST spec and privacy frameworks (TCF, GPP, US Privacy) use: players substitute macros before treating the string as a URL, so the pre-substitution string is never meant to be a syntactically valid URI.Pre-encoding the delimiters is not a workaround — players and verification vendors match the literal macro token, so an encoded
%5BOMIDPARTNER%5Dsilently never gets substituted. In practice this meant essentially every verification-wrapped CTV tag was unsubmittable viavast_tagURL delivery, with inline XML delivery as the only escape hatch.Fix
Switch both
urlfields fromformat: "uri"toformat: "uri-template"(RFC 6570) — the conventionurl-asset.jsonalready established for AdCP universal macros, so this aligns the tag assets with existing repo precedent rather than inventing a new mechanism.RFC 6570 makes this work without any custom pattern:
[(0x5B) and](0x5D) are permitted literal characters${GDPR_CONSENT_1002}parses as a literal$followed by a{GDPR_CONSENT_1002}template expressionVerified against ajv + ajv-formats (the validator stack used in CI): the exact failing production tag passes
uri-templateand failsuri; junk strings (raw spaces, unbalanced braces) are still rejected.Schema descriptions on both fields now document that macros are accepted as-is and that buyers MUST NOT pre-encode macro delimiters.
Changes
static/schemas/source/core/assets/vast-asset.json—urlformaturi→uri-template, description documents macro handlingstatic/schemas/source/core/assets/daast-asset.json— same fix for the audio analoguetests/schema-validation.test.cjs— regression test validating the real verification-wrapped tag against both asset schemas, plus a negative case confirming malformed URLs still fail.changeset/vast-daast-tag-url-macro-validation.md— patch changesetKnown limitation
GAM-style
%%MACRO%%placeholders still fail validation — a bare%must be percent-encoded under RFC 6570. VAST 4.x official macros are[MACRO]and privacy conventions use${...}, so the common cases are covered. Supporting%%would require droppingformatvalidation entirely in favor of a loose pattern; left out of scope here deliberately, flagging for maintainers in case that's wanted.Testing
test:schemas— 19/19 pass (includes the new regression test)test:json-schema— 284 doc blocks passtest:examples— 55 passbuild:schemas+typecheck— cleantest:snippetsfailures (live-SDK snippets inget_products.mdx/sync_creatives.mdx) reproduce identically on a clean tree — unrelatedNote on released versions
This fixes
static/schemas/source/(/schemas/latest/). The error in the report cited the released3.0.12bundled schema, which is immutable — callers pinned to 3.0.x get the fix when the next release is cut. Until then, the workaround for macro-laden tags isdelivery_type: "inline".