Module adcp.types.canonical_decl

Wire-faithful ProductFormatDeclaration for the v2 catalog surface.

The upstream schema core/product-format-declaration.json is a discriminated oneOf over 13 format_kind values, each binding params to a canonical-specific schema. datamodel-code-generator collapses this shape to a single class carrying only the shared properties — format_kind and params disappear entirely because they live on the per-variant branches.

That generated stub is unusable for canonical-formats: it can't carry the discriminator the projection layer routes on, and it silently drops params (extra='ignore') so adopters who construct a declaration with a typed canonical body lose it on serialization.

This module replaces the public ProductFormatDeclaration symbol with a hand-rolled class that:

  • Carries all 9 shared properties the generator emits.
  • Adds format_kind: CanonicalFormatKind (the discriminator).
  • Adds params: dict[str, Any] — the per-canonical body. Required per the upstream schema (required: ["format_kind", "params"]). Kept as an open dict at this level so the same class works across all 13 canonical kinds; callers needing typed access SHOULD use :meth:ProductFormatDeclaration.params_as() to validate against the typed canonical format class.
  • Sets extra='allow' so future ProductFormatDeclaration field additions in 3.1.x don't break round-trip through this model. Extra fields are scanned for credential-shaped key suffixes at construction time; presence of one raises (see _CREDENTIAL_SHAPED_KEY_SUFFIXES).
  • Enforces the schema's normative cross-field constraint that canonical_formats_only=True and v1_format_ref[] are mutually exclusive (product-format-declaration.json allOf.not clause).

The generated class is preserved as ProductFormatDeclaration for callers that need the original codegen output (validation hooks, schema-loader cross-references). New code SHOULD import the hand-rolled class via :mod:adcp.types.

Classes

class ProductFormatDeclaration (**data: Any)
Expand source code
class ProductFormatDeclaration(AdCPBaseModel):
    """v2 catalog-side format declaration carrying the canonical discriminator.

    Wire-faithful Python representation of
    ``core/product-format-declaration.json``. See the module docstring for
    why this class replaces the codegen output.
    """

    model_config = ConfigDict(extra="allow")

    format_kind: Annotated[
        CanonicalFormatKind,
        Field(description="The canonical format kind this declaration declares."),
    ]
    params: Annotated[
        dict[str, Any],
        Field(
            description=(
                "Per-canonical body. Shape varies by format_kind — see the "
                "canonical's own schema (``formats/canonical/<kind>.json``). "
                "Use :meth:`params_as` for typed access."
            ),
        ),
    ]
    capability_id: Annotated[
        str | None,
        Field(
            description=(
                "Stable identifier for this declaration. REQUIRED when the "
                "parent product's format_options[] contains multiple "
                "declarations sharing the same format_kind."
            ),
        ),
    ] = None
    display_name: Annotated[
        str | None,
        Field(description="Optional seller-controlled human-readable label."),
    ] = None
    applies_to_channels: Annotated[
        list[MediaChannel] | None,
        Field(
            description=(
                "Optional subset of the parent product's channels to which "
                "this declaration applies."
            ),
        ),
    ] = None
    seller_preference: Annotated[
        SellerPreference | None,
        Field(description="Soft routing hint within the accepted set."),
    ] = None
    canonical_formats_only: Annotated[
        bool,
        Field(
            description=(
                "When true, this declaration has no clean v1 projection — "
                "SDKs MUST NOT synthesize a v1 format_id. Mutually exclusive "
                "with ``v1_format_ref``."
            ),
        ),
    ] = False
    experimental: Annotated[
        bool,
        Field(
            description=("When true, THIS seller's specific declaration may not work as declared."),
        ),
    ] = False
    format_shape: Annotated[
        str | None,
        Field(
            description=(
                "REQUIRED when format_kind='custom'; otherwise MUST be absent. "
                "Recognized format-shape-vocabulary entry."
            ),
        ),
    ] = None
    v1_format_ref: Annotated[
        list[FormatReferenceStructuredObject] | None,
        Field(
            description=(
                "Authoritative v2 → v1 link as one or more v1 format_id "
                "({agent_url, id}) values. Mutually exclusive with "
                "``canonical_formats_only=True``."
            ),
            min_length=1,
        ),
    ] = None
    format_schema: Annotated[
        PlatformExtensionReference | None,
        Field(
            description=(
                "REQUIRED when format_kind='custom'; otherwise MUST be absent. "
                "URI+digest reference to the custom shape's schema."
            ),
        ),
    ] = None

    @model_validator(mode="after")
    def _check_mutual_exclusion(self) -> Self:
        """Enforce the schema's ``allOf.not`` clause.

        ``product-format-declaration.json`` declares
        ``canonical_formats_only=True`` and ``v1_format_ref[]`` mutually
        exclusive. The Pydantic model rejects the combination at
        construction so the SDK never launders a wire-invalid declaration
        into a wire-valid one.
        """
        if self.canonical_formats_only and self.v1_format_ref:
            raise ValueError(
                "ProductFormatDeclaration: canonical_formats_only=True is "
                "mutually exclusive with v1_format_ref[] — a declaration can "
                "EITHER assert no v1 projection OR link to v1 named formats, "
                "never both. See product-format-declaration.json#allOf.not."
            )
        return self

    @model_validator(mode="after")
    def _reject_credential_shaped_extras(self) -> Self:
        """Fail-closed scan for credential-shaped keys in ``params`` + extras.

        ``params`` is an open dict and ``model_config['extra']='allow'``
        means unknown top-level fields are stored on the instance. Both
        are adopter-controlled bags that round-trip through
        ``format_options[]`` responses and the idempotency replay cache.
        Mirrors the dispatcher's ``ctx_metadata`` credential gate.
        """
        for bag_name, bag_value in (
            ("params", self.params),
            ("extras", self.__pydantic_extra__),
        ):
            if bag_value is None:
                continue
            found = _walk_for_credential_keys(bag_value, path=bag_name)
            if found is not None:
                raise ValueError(
                    f"ProductFormatDeclaration: {found!r} matches a "
                    f"credential-shaped key suffix and will round-trip to "
                    f"buyers via format_options[]. Move the value to "
                    f"AuthInfo.credential or a typed credential class. "
                    f"See CLAUDE.md → 'ctx_metadata: write-only credentials "
                    f"prohibited' for the equivalent dispatch-side rule."
                )
        return self

    def params_as(self, canonical_type: type[_TypedParams]) -> _TypedParams:
        """Validate ``params`` against the typed canonical-format class.

        Lets buyers and seller-side validators recover full typing on
        the per-canonical body — e.g., ``decl.params_as(CanonicalFormatImage)``
        returns a ``CanonicalFormatImage`` with ``.sizes`` / ``.format`` /
        etc. narrowed. Raises :class:`pydantic.ValidationError` when
        ``params`` doesn't match the canonical's schema.

        Args:
            canonical_type: A Pydantic model class from the canonical
                vocabulary (e.g., :class:`adcp.types.CanonicalFormatImage`).

        Returns:
            An instance of ``canonical_type`` validated against ``params``.
        """
        return canonical_type.model_validate(self.params)

v2 catalog-side format declaration carrying the canonical discriminator.

Wire-faithful Python representation of core/product-format-declaration.json. See the module docstring for why this class replaces the codegen output.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var applies_to_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var canonical_formats_only : bool
var capability_id : str | None
var display_name : str | None
var experimental : bool
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind
var format_schema : adcp.types.generated_poc.core.platform_extension_ref.PlatformExtensionReference | None
var format_shape : str | None
var model_config
var params : dict[str, typing.Any]
var seller_preference : adcp.types.generated_poc.core.product_format_declaration.SellerPreference | None
var v1_format_ref : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None

Methods

def params_as(self, canonical_type: type[_TypedParams]) ‑> ~_TypedParams
Expand source code
def params_as(self, canonical_type: type[_TypedParams]) -> _TypedParams:
    """Validate ``params`` against the typed canonical-format class.

    Lets buyers and seller-side validators recover full typing on
    the per-canonical body — e.g., ``decl.params_as(CanonicalFormatImage)``
    returns a ``CanonicalFormatImage`` with ``.sizes`` / ``.format`` /
    etc. narrowed. Raises :class:`pydantic.ValidationError` when
    ``params`` doesn't match the canonical's schema.

    Args:
        canonical_type: A Pydantic model class from the canonical
            vocabulary (e.g., :class:`adcp.types.CanonicalFormatImage`).

    Returns:
        An instance of ``canonical_type`` validated against ``params``.
    """
    return canonical_type.model_validate(self.params)

Validate params against the typed canonical-format class.

Lets buyers and seller-side validators recover full typing on the per-canonical body — e.g., decl.params_as(CanonicalFormatImage) returns a CanonicalFormatImage with .sizes / .format / etc. narrowed. Raises :class:pydantic.ValidationError when params doesn't match the canonical's schema.

Args

canonical_type
A Pydantic model class from the canonical vocabulary (e.g., :class:CanonicalFormatImage).

Returns

An instance of canonical_type validated against params.

class _GeneratedProductFormatDeclaration (**data: Any)
Expand source code
class ProductFormatDeclaration(AdCPBaseModel):
    format_option_id: Annotated[
        str | None,
        Field(
            description="Stable identifier for this format declaration within its namespace. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.format_option_ref`). SHOULD be set on EVERY `format_options[]` entry — not just when structurally required to break a `format_kind` collision — so V2-mental-model buyers can use the V2 authoring path (`PackageRequest.format_option_refs[]`, `creative-manifest.format_option_ref`) against the product. Publisher-catalog-backed options pair this with `publisher_domain`; product-local options omit `publisher_domain` and are selected by `format_option_id` within the target product. A product that ships without selectable `format_option_id` values on its `format_options[]` entries is structurally 3.1-conformant but is not V2-authorable: buyers fall back to v1 `format_ids[]` and lose the stable naming the V2 path was designed to provide. Sellers MUST reject V2 authoring against such products with `UNSUPPORTED_FEATURE` and `error.details.reason` set to `format_option_refs_not_published` per `package-request.json`. Format-internal (not a URI). Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'."
        ),
    ] = None
    publisher_domain: Annotated[
        str | None,
        Field(
            description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    display_name: Annotated[
        str | None,
        Field(
            description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn."
        ),
    ] = None
    applies_to_channels: Annotated[
        list[channels.MediaChannel] | None,
        Field(
            description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`."
        ),
    ] = None
    seller_preference: Annotated[
        SellerPreference | None,
        Field(
            description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it."
        ),
    ] = None
    canonical_formats_only: Annotated[
        bool | None,
        Field(
            description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.'
        ),
    ] = False
    experimental: Annotated[
        bool | None,
        Field(
            description="When true, THIS seller's specific product declaration may not work as declared — even if the underlying canonical is stable. Use for beta runtime paths, forward-looking catalog entries the runtime doesn't yet honor, or experimental products where the seller wants buyer-side caution. Buyers reading `experimental: true` on a product declaration SHOULD prefer the legacy named-format path when a fallback exists for the same product (via `format_ids` on the parent product or via this declaration's `v1_format_ref`) and SHOULD validate via `validate_input` or a sandbox before routing production budget.\n\nIndependent of the canonical's own `experimental` flag — a stable canonical (e.g., `image`, `video_hosted`) can carry an experimental product declaration when the seller is shipping a new runtime path that isn't fully wired yet. Conversely, an experimental canonical (`sponsored_placement`, `responsive_creative`, `agent_placement`) MAY carry non-experimental product declarations where the seller's adopter contract is well-tested. Buyer SDKs SHOULD filter products with `experimental: true` from default views and offer an opt-in flag to surface them.\n\nReplaces the earlier `runtime_status` enum (`stable | preview | declared_only`) — same semantic ('use with caution') without the cognitive overhead of two stability axes."
        ),
    ] = False
    format_shape: Annotated[
        str | None,
        Field(
            description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical.'
        ),
    ] = None
    v1_format_ref: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/<platform>` + `id: <platform-format-name>` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/<platform>`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/<platform>` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.",
            min_length=1,
        ),
    ] = None
    format_schema: Annotated[
        platform_extension_ref.PlatformExtensionReference | None,
        Field(
            description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var applies_to_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var canonical_formats_only : bool | None
var display_name : str | None
var experimental : bool | None
var format_option_id : str | None
var format_schema : adcp.types.generated_poc.core.platform_extension_ref.PlatformExtensionReference | None
var format_shape : str | None
var model_config
var publisher_domain : str | None
var seller_preference : adcp.types.generated_poc.core.product_format_declaration.SellerPreference | None
var v1_format_ref : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None

Inherited members