From 3502db80514157a514f83d2204812051031a896d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 21 Oct 2026 12:15:37 +0000 Subject: [PATCH] fix(codegen)!: refuse a JSON null on optional properties whose schema forbids one The generator rendered every optional property as `T | None = None`, which admitted an explicit JSON `null` the pinned schema rejects. A pre-generation pass now grades each optional property with the bundled validator and marks the ones that refuse `null`; the first post-generation fix turns the mark into `adcp.types.base.NoExplicitNull`, appended to the field's `Annotated` metadata. The annotation, default and attribute access are unchanged. `NoExplicitNull` is a `json_or_python` core schema: the JSON side is the type without its null arm, so `model_validate_json` refuses `null` with the type's own error and `model_json_schema()` publishes no null arm; the Python side keeps `T | None`, so `Model(field=None)` and `model_validate` on a dict still read `None` as "not supplied", symmetric with `model_dump`'s `exclude_none` default. `ADCP_STRICT_VALIDATION=1` refuses a Python `None` too. Properties whose schema admits `null` are generated without the marker. Also: the eight submitted response arms drop a leftover `validate_default=True`; the three literal-widening fixes carry the marker; the parity suite grades the model in JSON mode and no longer declares the explicit-null divergence. Fixes #1547 Refs #1522, #1414, #1409, #1397, #1359, #1362 BREAKING CHANGE: a JSON document carrying `null` for an optional property whose schema does not admit `null` is now refused by `model_validate_json` and `TypeAdapter.validate_json`, and the published JSON Schema of such a property has no null arm. A Python `None` is still accepted. JSON produced with `exclude_none=False` must be read back in Python mode (`model_validate(json.loads(...))`). Co-Authored-By: Claude Fable 5.1 --- docs/design/type-generation.md | 30 +- docs/validation-contract.md | 17 + scripts/generate_types.py | 175 ++++ scripts/post_generate_fixes.py | 423 ++++++++-- scripts/post_generation_manifest.json | 1 + src/adcp/types/base.py | 107 ++- src/adcp/types/canonical_creative.py | 5 +- src/adcp/types/domains/a2ui/component.py | 6 +- src/adcp/types/domains/a2ui/si_catalog.py | 93 ++- src/adcp/types/domains/a2ui/surface.py | 11 +- src/adcp/types/domains/a2ui/user_action.py | 12 +- .../types/domains/aao/agent_publishers.py | 6 +- .../account/get_account_financials_request.py | 8 +- .../account/list_account_changes_request.py | 16 +- .../account/list_account_changes_response.py | 21 +- .../domains/account/list_accounts_request.py | 14 +- .../domains/account/list_accounts_response.py | 13 +- .../domains/account/report_usage_request.py | 24 +- .../domains/account/report_usage_response.py | 9 +- .../domains/account/sync_accounts_request.py | 39 +- .../account/sync_governance_request.py | 8 +- .../account/sync_governance_response.py | 14 +- src/adcp/types/domains/adagents.py | 96 ++- .../domains/brand/acquire_rights_request.py | 21 +- .../brand/creative_approval_request.py | 14 +- .../brand/creative_approval_response.py | 40 +- .../brand/get_brand_identity_request.py | 9 +- .../types/domains/brand/get_rights_request.py | 16 +- .../domains/brand/revocation_notification.py | 9 +- .../domains/brand/rights_pricing_option.py | 13 +- src/adcp/types/domains/brand/rights_terms.py | 20 +- .../domains/brand/search_brands_request.py | 14 +- .../domains/brand/search_brands_response.py | 38 +- .../domains/brand/update_rights_request.py | 14 +- .../brand/verify_brand_claims_request.py | 31 +- src/adcp/types/domains/brand_discovery.py | 95 ++- .../get_adcp_capabilities_response.py | 786 ++++++++++++++---- .../domains/collection/collection_list.py | 25 +- .../collection_list_changed_webhook.py | 27 +- .../collection/collection_list_filters.py | 12 +- .../create_collection_list_request.py | 17 +- .../create_collection_list_response.py | 8 +- .../delete_collection_list_request.py | 8 +- .../delete_collection_list_response.py | 8 +- .../collection/get_collection_list_request.py | 13 +- .../get_collection_list_response.py | 30 +- .../list_collection_lists_request.py | 14 +- .../list_collection_lists_response.py | 9 +- .../update_collection_list_request.py | 16 +- .../update_collection_list_response.py | 8 +- .../comply_test_controller_request.py | 79 +- .../comply_test_controller_response.py | 75 +- .../get_creative_features_completion.py | 12 +- .../domains/content_standards/artifact.py | 119 ++- .../artifact_webhook_payload.py | 25 +- .../calibrate_content_request.py | 7 +- .../content_standards/content_standards.py | 18 +- .../create_content_standards_request.py | 24 +- .../create_content_standards_response.py | 12 +- .../get_content_standards_request.py | 7 +- .../get_media_buy_artifacts_request.py | 30 +- .../list_content_standards_request.py | 18 +- .../list_content_standards_response.py | 13 +- .../update_content_standards_request.py | 26 +- .../update_content_standards_response.py | 12 +- .../validate_content_delivery_request.py | 33 +- src/adcp/types/domains/core/account.py | 48 +- .../domains/core/account_authorization.py | 7 +- src/adcp/types/domains/core/account_change.py | 21 +- .../core/account_change_recorded_webhook.py | 8 +- .../domains/core/account_identity_change.py | 12 +- .../core/account_identity_change_preview.py | 8 +- src/adcp/types/domains/core/account_ref.py | 8 +- .../core/account_status_changed_webhook.py | 12 +- .../core/account_timezone_capability.py | 7 +- .../core/account_with_authorization.py | 4 +- .../types/domains/core/ad_inventory_config.py | 11 +- .../domains/core/agent_notification_config.py | 11 +- .../core/agent_notification_config_state.py | 15 +- .../core/agent_reporting_destination.py | 7 +- .../core/agent_reporting_destination_state.py | 9 +- .../types/domains/core/agent_signing_key.py | 24 +- .../domains/core/agent_webhook_challenge.py | 5 +- src/adcp/types/domains/core/app_item.py | 26 +- .../types/domains/core/assets/audio_asset.py | 39 +- .../types/domains/core/assets/card_asset.py | 10 +- .../types/domains/core/assets/css_asset.py | 9 +- .../types/domains/core/assets/daast_asset.py | 46 +- .../core/assets/daast_tracker_asset.py | 8 +- .../domains/core/assets/display_tag_asset.py | 25 +- .../types/domains/core/assets/html_asset.py | 17 +- .../types/domains/core/assets/image_asset.py | 18 +- .../domains/core/assets/javascript_asset.py | 14 +- .../domains/core/assets/markdown_asset.py | 7 +- .../core/assets/pixel_tracker_asset.py | 8 +- .../core/assets/published_post_asset.py | 25 +- .../types/domains/core/assets/text_asset.py | 6 +- .../types/domains/core/assets/url_asset.py | 10 +- .../types/domains/core/assets/vast_asset.py | 32 +- .../domains/core/assets/vast_tracker_asset.py | 8 +- .../types/domains/core/assets/video_asset.py | 78 +- .../domains/core/assets/webhook_asset.py | 20 +- .../types/domains/core/assets/zip_asset.py | 18 +- ...ative_features_async_response_submitted.py | 10 +- ...creatives_async_response_input_required.py | 12 +- ...sync_creatives_async_response_submitted.py | 10 +- .../sync_creatives_async_response_working.py | 30 +- ..._creative_async_response_input_required.py | 13 +- ...build_creative_async_response_submitted.py | 10 +- .../build_creative_async_response_working.py | 21 +- ...media_buy_async_response_input_required.py | 13 +- ...eate_media_buy_async_response_submitted.py | 10 +- ...create_media_buy_async_response_working.py | 22 +- ..._products_async_response_input_required.py | 17 +- .../get_products_async_response_submitted.py | 14 +- .../get_products_async_response_working.py | 16 +- ..._catalogs_async_response_input_required.py | 9 +- .../sync_catalogs_async_response_submitted.py | 10 +- .../sync_catalogs_async_response_working.py | 31 +- ...media_buy_async_response_input_required.py | 12 +- ...date_media_buy_async_response_submitted.py | 10 +- ...update_media_buy_async_response_working.py | 22 +- .../get_signals_async_response_submitted.py | 11 +- .../get_signals_async_response_working.py | 13 +- .../domains/core/attestation_capabilities.py | 20 +- .../domains/core/attestation_evaluation.py | 16 +- .../types/domains/core/attestation_issuer.py | 10 +- .../domains/core/attestation_reference.py | 22 +- .../types/domains/core/attestation_subject.py | 11 +- .../types/domains/core/attribution_window.py | 7 +- .../core/audience_activation_method.py | 13 +- .../domains/core/audience_characteristic.py | 19 +- .../types/domains/core/audience_evidence.py | 28 +- .../domains/core/audience_evidence_pin.py | 6 +- .../core/audience_evidence_requirements.py | 30 +- .../core/audience_evidence_selection.py | 8 +- .../types/domains/core/audience_member.py | 9 +- .../types/domains/core/audience_selector.py | 13 +- .../types/domains/core/audience_source.py | 5 +- .../domains/core/authorized_agent_base.py | 7 +- src/adcp/types/domains/core/bidding_policy.py | 9 +- .../domains/core/bidding_policy_capability.py | 12 +- src/adcp/types/domains/core/brand_key.py | 6 +- src/adcp/types/domains/core/brand_ref.py | 34 +- .../brand_response_authorization_result.py | 13 +- .../types/domains/core/budget_allocation.py | 21 +- src/adcp/types/domains/core/budget_range.py | 8 +- .../types/domains/core/business_entity.py | 34 +- .../types/domains/core/cancellation_policy.py | 6 +- .../domains/core/canonical_account_ref.py | 9 +- .../core/canonical_audience_evidence.py | 19 +- .../canonical_audience_evidence_selection.py | 12 +- .../core/canonical_delivery_forecast.py | 26 +- .../domains/core/canonical_forecast_point.py | 41 +- .../canonical_forecast_vendor_metric_value.py | 12 +- .../domains/core/canonical_format_option.py | 34 +- .../core/canonical_measurement_terms.py | 16 +- .../core/canonical_media_buy_action.py | 2 +- .../core/canonical_media_buy_action_fields.py | 15 +- .../core/canonical_media_buy_features.py | 17 +- .../core/canonical_metric_qualifier.py | 20 +- .../core/canonical_optimization_goal.py | 40 +- .../core/canonical_performance_standard.py | 6 +- .../types/domains/core/canonical_placement.py | 68 +- .../domains/core/canonical_pricing_option.py | 32 +- .../types/domains/core/canonical_product.py | 112 ++- .../domains/core/canonical_product_action.py | 10 +- .../domains/core/canonical_projection_ref.py | 6 +- .../canonical_projection_slot_override.py | 10 +- .../types/domains/core/canonical_proposal.py | 27 +- .../core/canonical_reporting_capabilities.py | 53 +- .../core/canonical_reporting_commitment.py | 16 +- .../types/domains/core/canvas_constraint.py | 10 +- .../core/capabilities_changed_webhook.py | 7 +- src/adcp/types/domains/core/catalog.py | 18 +- .../domains/core/catalog_field_mapping.py | 14 +- .../core/catalog_item_availability_state.py | 11 +- .../core/catalog_item_availability_update.py | 8 +- ...catalog_item_availability_update_result.py | 11 +- .../core/catalog_item_delivery_metrics.py | 4 +- .../types/domains/core/catalog_selection.py | 10 +- src/adcp/types/domains/core/catchment.py | 11 +- src/adcp/types/domains/core/collection.py | 29 +- .../core/collection_delivery_metrics.py | 4 +- .../domains/core/collection_distribution.py | 6 +- .../types/domains/core/collection_list_ref.py | 5 +- .../collection_property_delivery_metrics.py | 6 +- .../domains/core/collection_selection.py | 8 +- .../types/domains/core/collection_selector.py | 5 +- .../types/domains/core/committed_metric.py | 17 +- .../core/compact_task_input_required.py | 14 +- .../domains/core/compact_task_submitted.py | 11 +- .../domains/core/compact_task_working.py | 16 +- .../domains/core/creative_approval_scope.py | 8 +- src/adcp/types/domains/core/creative_asset.py | 26 +- .../types/domains/core/creative_assignment.py | 10 +- src/adcp/types/domains/core/creative_brief.py | 35 +- .../domains/core/creative_consumption.py | 11 +- .../domains/core/creative_delivery_metrics.py | 5 +- .../types/domains/core/creative_filters.py | 25 +- .../domains/core/creative_localization.py | 5 +- .../core/creative_localization_readback.py | 5 +- .../types/domains/core/creative_manifest.py | 15 +- .../creative_operation_format_declaration.py | 33 +- .../types/domains/core/creative_policy.py | 13 +- .../domains/core/creative_representation.py | 5 +- .../core/creative_representation_set.py | 5 +- .../types/domains/core/creative_variable.py | 6 +- .../types/domains/core/creative_variant.py | 12 +- .../domains/core/daast_tracker_constraints.py | 12 +- src/adcp/types/domains/core/daypart_target.py | 6 +- .../types/domains/core/deadline_policy.py | 12 +- .../core/delivery_breakdown_controls.py | 7 +- .../types/domains/core/delivery_forecast.py | 14 +- .../domains/core/delivery_metric_aggregate.py | 31 +- .../types/domains/core/delivery_metrics.py | 156 +++- .../types/domains/core/delivery_recipient.py | 8 +- .../domains/core/demographic_age_range.py | 6 +- .../core/demographic_reporting_capability.py | 19 +- .../core/demographic_targeting_capability.py | 11 +- .../core/demographic_targeting_intent.py | 6 +- .../core/demographic_targeting_resolution.py | 14 +- src/adcp/types/domains/core/deployment.py | 18 +- src/adcp/types/domains/core/destination.py | 8 +- .../types/domains/core/destination_item.py | 45 +- .../core/downstream_connection_requirement.py | 28 +- src/adcp/types/domains/core/education_item.py | 48 +- src/adcp/types/domains/core/error.py | 19 +- src/adcp/types/domains/core/evaluator_spec.py | 28 +- src/adcp/types/domains/core/event.py | 18 +- .../types/domains/core/event_custom_data.py | 44 +- .../types/domains/core/event_source_health.py | 11 +- src/adcp/types/domains/core/event_surface.py | 9 +- .../types/domains/core/feature_requirement.py | 9 +- src/adcp/types/domains/core/flight_item.py | 42 +- .../core/forecast_dimension_audience.py | 6 +- .../domains/core/forecast_dimension_geo.py | 7 +- .../core/forecast_dimension_placement.py | 5 +- .../domains/core/forecast_dimension_signal.py | 11 +- src/adcp/types/domains/core/forecast_point.py | 53 +- src/adcp/types/domains/core/forecast_range.py | 16 +- .../types/domains/core/forecast_rate_range.py | 6 +- .../core/forecast_vendor_metric_value.py | 9 +- src/adcp/types/domains/core/format.py | 396 ++++++++- src/adcp/types/domains/core/format_id.py | 8 +- src/adcp/types/domains/core/frequency_cap.py | 9 +- .../domains/core/frequency_cap_constraints.py | 12 +- .../frequency_cap_impression_constraints.py | 12 +- .../frequency_cap_interval_constraints.py | 14 +- .../core/frequency_cap_requirements.py | 15 +- .../domains/core/generation_credential.py | 8 +- .../domains/core/geo_breakdown_support.py | 8 +- .../domains/core/geo_delivery_metrics.py | 6 +- src/adcp/types/domains/core/geo_place_area.py | 8 +- .../core/geo_place_catalog_capability.py | 5 +- .../domains/core/geo_place_catalog_entry.py | 9 +- .../domains/core/geo_place_requirement.py | 5 +- .../types/domains/core/geo_region_support.py | 10 +- .../core/get_geo_place_resolution_request.py | 15 +- .../core/get_geo_place_resolution_response.py | 6 +- src/adcp/types/domains/core/hotel_item.py | 54 +- src/adcp/types/domains/core/impairment.py | 7 +- src/adcp/types/domains/core/indicator.py | 7 +- .../types/domains/core/indicator_bearing.py | 8 +- .../types/domains/core/indicator_scope.py | 5 +- .../core/indicators_changed_webhook.py | 10 +- .../types/domains/core/insertion_order.py | 31 +- src/adcp/types/domains/core/installment.py | 36 +- .../domains/core/installment_deadlines.py | 7 +- .../core/installment_delivery_metrics.py | 5 +- .../installment_property_delivery_metrics.py | 7 +- src/adcp/types/domains/core/job_item.py | 36 +- src/adcp/types/domains/core/keyword_target.py | 6 +- src/adcp/types/domains/core/limited_series.py | 12 +- .../types/domains/core/macro_declaration.py | 9 +- .../core/macro_resolution_capability.py | 15 +- .../domains/core/macro_resolution_result.py | 15 +- .../domains/core/macro_translation_target.py | 10 +- .../types/domains/core/material_deadline.py | 5 +- .../types/domains/core/mcp_webhook_payload.py | 10 +- .../domains/core/measurement_readiness.py | 8 +- .../types/domains/core/measurement_terms.py | 9 +- .../types/domains/core/measurement_window.py | 7 +- src/adcp/types/domains/core/media_buy.py | 39 +- .../core/media_buy_available_action.py | 15 +- .../types/domains/core/media_buy_features.py | 14 +- .../media_buy_frequency_cap_requirement.py | 4 +- .../core/media_buy_frequency_cap_support.py | 4 +- .../types/domains/core/media_buy_support.py | 13 +- .../core/media_buy_support_requirements.py | 11 +- src/adcp/types/domains/core/missing_metric.py | 20 +- .../types/domains/core/notification_config.py | 10 +- src/adcp/types/domains/core/offering.py | 26 +- .../domains/core/offering_asset_group.py | 6 +- .../types/domains/core/operator_identity.py | 5 +- src/adcp/types/domains/core/operator_unit.py | 5 +- .../types/domains/core/opportunity_context.py | 14 +- .../types/domains/core/optimization_goal.py | 21 +- .../types/domains/core/outcome_measurement.py | 5 +- src/adcp/types/domains/core/overlay.py | 9 +- src/adcp/types/domains/core/package.py | 50 +- .../core/package_delivery_metric_value.py | 9 +- .../domains/core/package_format_snapshot.py | 346 +++++++- .../domains/core/package_signal_targeting.py | 18 +- .../core/package_targeting_resolution.py | 6 +- .../types/domains/core/pagination_request.py | 6 +- .../types/domains/core/pagination_response.py | 6 +- .../domains/core/performance_feedback.py | 29 +- .../core/performance_feedback_assertion.py | 26 +- .../core/performance_feedback_metric.py | 20 +- .../domains/core/performance_standard.py | 5 +- src/adcp/types/domains/core/placement.py | 28 +- .../domains/core/placement_definition.py | 33 +- .../core/placement_delivery_metrics.py | 6 +- .../types/domains/core/placement_evidence.py | 15 +- .../domains/core/placement_presentation.py | 12 +- .../placement_property_delivery_metrics.py | 8 +- src/adcp/types/domains/core/placement_ref.py | 5 +- .../types/domains/core/placement_selection.py | 8 +- .../types/domains/core/planned_delivery.py | 40 +- .../core/positive_postal_area_support.py | 25 +- .../types/domains/core/postal_area_support.py | 48 +- .../types/domains/core/preview_provider.py | 5 +- src/adcp/types/domains/core/price.py | 5 +- .../domains/core/principal_changed_webhook.py | 7 +- .../domains/core/principal_declarations.py | 7 +- .../core/principal_declarations_state.py | 6 +- .../types/domains/core/principal_state.py | 10 +- src/adcp/types/domains/core/product.py | 129 ++- .../types/domains/core/product_allocation.py | 18 +- .../domains/core/product_allowed_action.py | 8 +- .../product_audience_evidence_requirements.py | 55 +- .../core/product_card_reference_asset.py | 9 +- .../core/product_execution_requirement.py | 12 +- .../types/domains/core/product_filters.py | 98 ++- .../core/product_format_declaration.py | 244 +++++- .../types/domains/core/product_identity.py | 5 +- .../domains/core/product_offer_filters.py | 101 ++- .../core/product_signal_targeting_option.py | 9 +- .../core/product_targeting_resolution.py | 7 +- src/adcp/types/domains/core/property.py | 8 +- .../domains/core/property_delivery_metrics.py | 5 +- .../types/domains/core/property_list_ref.py | 5 +- src/adcp/types/domains/core/proposal.py | 37 +- .../types/domains/core/protocol_envelope.py | 14 +- src/adcp/types/domains/core/provenance.py | 51 +- .../core/publisher_property_selector.py | 8 +- .../domains/core/push_notification_config.py | 7 +- .../types/domains/core/real_estate_item.py | 64 +- .../types/domains/core/reference_asset.py | 5 +- .../types/domains/core/reference_renderer.py | 5 +- src/adcp/types/domains/core/registry_event.py | 162 ++-- .../domains/core/reporting_adjustment.py | 6 +- .../core/reporting_adjustment_receipt.py | 9 +- .../reporting_canonicalization_contract.py | 6 +- .../domains/core/reporting_capabilities.py | 25 +- .../domains/core/reporting_consumer_status.py | 14 +- .../domains/core/reporting_control_total.py | 6 +- .../types/domains/core/reporting_coverage.py | 8 +- .../reporting_dataset_share_destination.py | 8 +- .../core/reporting_delivery_capabilities.py | 15 +- .../domains/core/reporting_delivery_config.py | 14 +- .../core/reporting_delivery_config_state.py | 23 +- .../core/reporting_delivery_offering.py | 27 +- .../domains/core/reporting_file_entry.py | 10 +- .../core/reporting_ledger_changed_webhook.py | 24 +- .../domains/core/reporting_materialization.py | 18 +- .../domains/core/reporting_obligation.py | 16 +- .../types/domains/core/reporting_receipt.py | 28 +- .../core/reporting_reliability_statistics.py | 12 +- .../core/reporting_report_definition.py | 8 +- .../types/domains/core/reporting_resource.py | 10 +- .../types/domains/core/reporting_revision.py | 14 +- .../types/domains/core/reporting_schedule.py | 6 +- .../core/reporting_schedule_offering.py | 12 +- .../core/reporting_status_changed_webhook.py | 10 +- .../domains/core/reporting_status_issue.py | 34 +- .../domains/core/reporting_verification.py | 12 +- .../types/domains/core/reporting_webhook.py | 6 +- .../core/reporting_write_destination.py | 5 +- .../core/representation_destination.py | 7 +- .../domains/core/representation_rejection.py | 6 +- .../domains/core/representation_selection.py | 6 +- .../requirements/audio_asset_requirements.py | 35 +- .../requirements/catalog_field_binding.py | 11 +- .../core/requirements/catalog_requirements.py | 11 +- .../requirements/css_asset_requirements.py | 6 +- .../requirements/daast_asset_requirements.py | 6 +- .../requirements/html_asset_requirements.py | 8 +- .../requirements/image_asset_requirements.py | 28 +- .../javascript_asset_requirements.py | 12 +- .../markdown_asset_requirements.py | 10 +- .../requirements/offering_asset_constraint.py | 16 +- .../requirements/text_asset_requirements.py | 32 +- .../requirements/url_asset_requirements.py | 13 +- .../requirements/vast_asset_requirements.py | 7 +- .../requirements/video_asset_requirements.py | 74 +- .../webhook_asset_requirements.py | 8 +- src/adcp/types/domains/core/response.py | 5 +- .../core/rights_attestation_evaluation.py | 20 +- .../types/domains/core/rights_constraint.py | 40 +- .../types/domains/core/seller_agent_ref.py | 5 +- .../domains/core/signal_coverage_forecast.py | 54 +- .../types/domains/core/signal_definition.py | 74 +- .../core/signal_definition_enrichment.py | 79 +- src/adcp/types/domains/core/signal_filters.py | 16 +- src/adcp/types/domains/core/signal_listing.py | 15 +- .../core/signal_modeling_disclosure.py | 13 +- src/adcp/types/domains/core/signal_pricing.py | 17 +- .../core/signal_selection_group_rule.py | 8 +- .../types/domains/core/signal_targeting.py | 12 +- .../core/signal_targeting_expression.py | 6 +- .../domains/core/signal_targeting_rules.py | 12 +- src/adcp/types/domains/core/sla_window.py | 6 +- src/adcp/types/domains/core/special.py | 11 +- src/adcp/types/domains/core/store_item.py | 33 +- src/adcp/types/domains/core/talent.py | 5 +- src/adcp/types/domains/core/targeting.py | 56 +- .../types/domains/core/targeting_input.py | 16 +- .../domains/core/targeting_modification.py | 9 +- .../core/targeting_overlay_requirements.py | 102 ++- .../domains/core/targeting_overlay_support.py | 129 +-- .../types/domains/core/tasks_get_request.py | 10 +- .../types/domains/core/tasks_get_response.py | 48 +- .../types/domains/core/tasks_list_request.py | 52 +- .../types/domains/core/tasks_list_response.py | 37 +- .../core/tracker_execution_selector.py | 12 +- src/adcp/types/domains/core/transformer.py | 263 +++++- .../types/domains/core/transformer_param.py | 20 +- .../types/domains/core/truncation_sentinel.py | 6 +- src/adcp/types/domains/core/user_match.py | 22 +- .../core/vast_media_file_requirements.py | 27 +- .../domains/core/vast_tracker_constraints.py | 12 +- src/adcp/types/domains/core/vehicle_item.py | 60 +- ...or_metric_optimization_supported_metric.py | 5 +- .../types/domains/core/vendor_metric_value.py | 25 +- .../domains/core/vendor_pricing_option.py | 19 +- .../domains/core/verification_token_claims.py | 12 +- .../types/domains/core/version_envelope.py | 6 +- src/adcp/types/domains/core/warning.py | 7 +- .../types/domains/core/warning_resource.py | 11 +- .../domains/core/webhook_activity_record.py | 9 +- .../types/domains/core/webhook_challenge.py | 5 +- .../core/webhook_challenge_response.py | 6 +- .../domains/core/wholesale_feed_event.py | 47 +- .../domains/core/wholesale_feed_webhook.py | 8 +- .../domains/creative/audit_observation.py | 12 +- .../creative_assignment_changed_webhook.py | 6 +- .../creative/creative_feature_result.py | 17 +- .../creative/creative_purged_webhook.py | 7 +- .../creative_status_changed_webhook.py | 8 +- .../creative/get_creative_delivery_request.py | 14 +- .../get_creative_delivery_response.py | 27 +- ...ative_features_async_response_submitted.py | 10 +- .../creative/get_creative_features_request.py | 11 +- .../get_creative_features_response.py | 2 +- .../get_creative_features_terminal_success.py | 15 +- .../creative/list_creative_formats_request.py | 28 +- .../list_creative_formats_response.py | 18 +- .../creative/list_creatives_request.py | 35 +- .../creative/list_creatives_response.py | 85 +- .../creative/list_transformers_request.py | 21 +- .../creative/list_transformers_response.py | 13 +- .../creative/preview_creative_request.py | 40 +- .../creative/preview_creative_response.py | 2 +- .../types/domains/creative/preview_render.py | 28 +- ...creatives_async_response_input_required.py | 12 +- ...sync_creatives_async_response_submitted.py | 10 +- .../sync_creatives_async_response_working.py | 30 +- .../creative/sync_creatives_request.py | 27 +- .../creative/sync_creatives_response.py | 2 +- .../creative/validate_input_request.py | 7 +- .../domains/creative/validate_input_result.py | 8 +- .../types/domains/creative/video_brief.py | 14 +- .../error_details/accessibility_violation.py | 6 +- .../domains/error_details/account_moved.py | 5 +- .../error_details/account_setup_required.py | 10 +- .../error_details/action_not_allowed.py | 6 +- .../error_details/audience_too_small.py | 10 +- .../error_details/authorization_required.py | 9 +- .../billing_not_permitted_for_agent.py | 5 +- .../error_details/billing_not_supported.py | 6 +- .../domains/error_details/budget_too_low.py | 12 +- .../types/domains/error_details/conflict.py | 11 +- .../error_details/creative_rejected.py | 16 +- .../governance_agent_not_accepted.py | 5 +- .../domains/error_details/policy_violation.py | 14 +- .../domains/error_details/rate_limited.py | 20 +- .../domains/error_details/requote_required.py | 7 +- .../domains/error_details/stale_response.py | 11 +- .../error_details/vast_version_mismatch.py | 41 +- .../error_details/vendor_error_codes.py | 9 +- .../error_details/version_unsupported.py | 8 +- .../domains/extensions/extension_meta.py | 13 +- .../types/domains/formats/canonical/_base.py | 37 +- .../formats/canonical/agent_placement.py | 11 +- .../domains/formats/canonical/audio_daast.py | 19 +- .../domains/formats/canonical/audio_hosted.py | 38 +- .../domains/formats/canonical/audio_vast.py | 20 +- .../canonical/coordinated_placements.py | 447 +++++++--- .../domains/formats/canonical/display_tag.py | 24 +- .../types/domains/formats/canonical/html5.py | 30 +- .../types/domains/formats/canonical/image.py | 29 +- .../formats/canonical/image_carousel.py | 23 +- .../formats/canonical/native_in_feed.py | 21 +- .../formats/canonical/responsive_creative.py | 55 +- .../seller_rendered_stateful_display.py | 82 +- .../formats/canonical/sponsored_placement.py | 19 +- .../domains/formats/canonical/video_hosted.py | 50 +- .../domains/formats/canonical/video_vast.py | 36 +- .../governance/attribute_definition.py | 10 +- .../governance/audience_constraints.py | 6 +- .../governance/check_governance_request.py | 93 ++- .../governance/check_governance_response.py | 32 +- .../governance/get_plan_audit_logs_request.py | 15 +- .../get_plan_audit_logs_response.py | 191 +++-- .../governance/policy_category_definition.py | 16 +- .../types/domains/governance/policy_entry.py | 38 +- .../types/domains/governance/policy_ref.py | 6 +- .../report_plan_adjustment_request.py | 27 +- .../report_plan_adjustment_response.py | 9 +- .../governance/report_plan_outcome_request.py | 29 +- .../report_plan_outcome_response.py | 23 +- .../governance/reported_outcome_error.py | 17 +- .../domains/governance/sync_plans_request.py | 49 +- .../domains/governance/sync_plans_response.py | 15 +- src/adcp/types/domains/manifest_schema.py | 22 +- .../media_buy/accept_proposal_request.py | 32 +- .../media_buy/accept_proposal_response.py | 31 +- .../domains/media_buy/acceptance_context.py | 20 +- .../media_buy/acceptance_policy_catalog.py | 13 +- .../media_buy/acceptance_policy_profile.py | 14 +- .../acceptance_policy_requirement.py | 53 +- .../media_buy/acceptance_policy_rule.py | 20 +- ..._creative_async_response_input_required.py | 13 +- ...build_creative_async_response_submitted.py | 10 +- .../build_creative_async_response_working.py | 21 +- .../media_buy/build_creative_request.py | 61 +- .../media_buy/build_creative_response.py | 2 +- .../domains/media_buy/buy_products_request.py | 45 +- .../media_buy/buy_products_response.py | 31 +- .../types/domains/media_buy/change_term.py | 12 +- .../media_buy/change_term_constraints.py | 29 +- .../domains/media_buy/commercial_terms.py | 38 +- .../media_buy/control_media_buy_request.py | 36 +- .../media_buy/control_media_buy_response.py | 35 +- ...media_buy_async_response_input_required.py | 13 +- ...eate_media_buy_async_response_submitted.py | 10 +- ...create_media_buy_async_response_working.py | 22 +- .../media_buy/create_media_buy_request.py | 37 +- .../media_buy/create_media_buy_response.py | 2 +- .../media_buy/decline_proposals_request.py | 17 +- .../media_buy/decline_proposals_response.py | 31 +- .../get_media_buy_delivery_request.py | 93 ++- .../get_media_buy_delivery_response.py | 165 +++- .../media_buy/get_media_buys_request.py | 16 +- .../media_buy/get_media_buys_response.py | 123 ++- ..._products_async_response_input_required.py | 17 +- .../get_products_async_response_submitted.py | 14 +- .../get_products_async_response_working.py | 16 +- .../media_buy/get_products_rejected.py | 11 +- .../domains/media_buy/get_products_request.py | 33 +- .../media_buy/get_products_response.py | 41 +- .../get_products_targeting_resolution.py | 6 +- .../media_buy/get_reporting_status_request.py | 22 +- .../get_reporting_status_response.py | 47 +- .../list_creative_formats_request.py | 28 +- .../list_creative_formats_response.py | 17 +- .../media_buy/list_products_request.py | 29 +- .../media_buy/list_products_response.py | 32 +- .../domains/media_buy/log_event_request.py | 8 +- .../media_buy_commitment_response.py | 31 +- .../media_buy_delivery_webhook_result.py | 51 +- .../types/domains/media_buy/outcome_target.py | 8 +- .../domains/media_buy/package_control.py | 28 +- .../domains/media_buy/package_request.py | 52 +- .../types/domains/media_buy/package_update.py | 32 +- .../media_buy/product_discovery_criteria.py | 18 +- .../domains/media_buy/product_purchase.py | 30 +- .../media_buy/product_purchase_input.py | 30 +- .../domains/media_buy/product_refinement.py | 21 +- .../media_buy/proposal_budget_constraint.py | 8 +- .../domains/media_buy/proposal_decline.py | 5 +- .../domains/media_buy/proposal_refinement.py | 61 +- .../provide_performance_feedback_request.py | 7 +- .../media_buy/refine_proposals_request.py | 17 +- .../media_buy/refine_proposals_response.py | 131 ++- .../media_buy/request_proposals_request.py | 23 +- .../media_buy/request_proposals_response.py | 102 ++- .../media_buy/sync_audiences_request.py | 22 +- .../media_buy/sync_audiences_response.py | 2 +- ..._catalogs_async_response_input_required.py | 9 +- .../sync_catalogs_async_response_submitted.py | 10 +- .../sync_catalogs_async_response_working.py | 31 +- .../media_buy/sync_catalogs_request.py | 15 +- .../media_buy/sync_catalogs_response.py | 2 +- .../media_buy/sync_event_sources_request.py | 21 +- .../sync_reporting_receipts_request.py | 14 +- .../sync_reporting_receipts_response.py | 8 +- .../sync_reporting_status_request.py | 9 +- .../sync_reporting_status_response.py | 8 +- ...media_buy_async_response_input_required.py | 12 +- ...date_media_buy_async_response_submitted.py | 10 +- ...update_media_buy_async_response_working.py | 22 +- .../media_buy/update_media_buy_request.py | 28 +- .../media_buy/update_media_buy_response.py | 2 +- .../domains/pricing_options/cpa_option.py | 9 +- .../domains/pricing_options/cpc_option.py | 11 +- .../domains/pricing_options/cpcv_option.py | 11 +- .../domains/pricing_options/cpm_option.py | 11 +- .../domains/pricing_options/cpp_option.py | 13 +- .../domains/pricing_options/cpv_option.py | 11 +- .../pricing_options/flat_rate_option.py | 23 +- .../pricing_options/price_breakdown.py | 8 +- .../domains/pricing_options/price_guidance.py | 20 +- .../pricing_options/revenue_share_option.py | 5 +- .../domains/pricing_options/time_option.py | 15 +- .../domains/pricing_options/vcpm_option.py | 11 +- .../domains/property/authorization_result.py | 11 +- .../property/create_property_list_request.py | 17 +- .../property/create_property_list_response.py | 8 +- .../property/delete_property_list_request.py | 8 +- .../property/delete_property_list_response.py | 8 +- .../types/domains/property/delivery_record.py | 8 +- .../property/get_property_list_request.py | 13 +- .../property/get_property_list_response.py | 14 +- .../property/list_property_lists_request.py | 14 +- .../property/list_property_lists_response.py | 9 +- .../types/domains/property/property_error.py | 8 +- .../domains/property/property_feature.py | 5 +- .../property/property_feature_definition.py | 34 +- .../property/property_feature_result.py | 8 +- .../property/property_feature_value.py | 18 +- .../types/domains/property/property_list.py | 26 +- .../property/property_list_changed_webhook.py | 25 +- .../domains/property/property_list_filters.py | 9 +- .../property/update_property_list_request.py | 16 +- .../property/update_property_list_response.py | 8 +- .../validate_property_delivery_request.py | 9 +- .../validate_property_delivery_response.py | 19 +- .../domains/property/validation_result.py | 27 +- .../protocol/get_adcp_capabilities_request.py | 8 +- .../get_adcp_capabilities_response.py | 304 ++++++- .../domains/protocol/get_principal_request.py | 9 +- .../protocol/get_principal_response.py | 8 +- .../protocol/get_task_status_request.py | 10 +- .../protocol/get_task_status_response.py | 48 +- .../domains/protocol/list_tasks_request.py | 52 +- .../domains/protocol/list_tasks_response.py | 37 +- ...sync_agent_notification_configs_request.py | 8 +- ...ync_agent_notification_configs_response.py | 10 +- .../protocol/sync_principal_request.py | 14 +- .../protocol/sync_principal_response.py | 12 +- .../registries/v1_canonical_mapping.py | 21 +- .../signals/activate_signal_request.py | 11 +- .../get_signals_async_response_submitted.py | 11 +- .../get_signals_async_response_working.py | 13 +- .../domains/signals/get_signals_request.py | 25 +- .../domains/signals/get_signals_response.py | 107 ++- .../sponsored_intelligence/si_capabilities.py | 61 +- .../si_get_offering_request.py | 13 +- .../si_get_offering_response.py | 70 +- .../sponsored_intelligence/si_identity.py | 51 +- .../si_initiate_session_request.py | 17 +- .../si_initiate_session_response.py | 29 +- .../si_send_message_request.py | 20 +- .../si_send_message_response.py | 63 +- .../si_sponsored_context.py | 29 +- .../si_sponsored_context_receipt.py | 15 +- .../si_terminate_session_request.py | 25 +- .../si_terminate_session_response.py | 26 +- .../sponsored_intelligence/si_ui_element.py | 8 +- .../trusted_match/available_package.py | 7 +- .../trusted_match/context_match_request.py | 27 +- .../trusted_match/context_match_response.py | 7 +- src/adcp/types/domains/trusted_match/error.py | 8 +- .../trusted_match/identity_match_request.py | 29 +- .../trusted_match/identity_match_response.py | 6 +- src/adcp/types/domains/trusted_match/offer.py | 10 +- .../domains/trusted_match/offer_price.py | 8 +- .../provider_context_match_response.py | 14 +- .../provider_identity_match_response.py | 5 +- .../trusted_match/provider_registration.py | 20 +- tests/conformance/_schema_parity.py | 9 - tests/conformance/test_schema_model_parity.py | 6 +- tests/test_canceled_literal_default.py | 8 +- tests/test_codegen_invariants.py | 41 +- tests/test_manifest_response_readback.py | 14 +- tests/test_no_explicit_null.py | 289 +++++++ tests/test_post_generate_no_explicit_null.py | 179 ++++ tests/test_targeting_overlay_compat.py | 22 +- tests/test_targeting_overlay_schema.py | 37 +- 692 files changed, 13564 insertions(+), 4486 deletions(-) create mode 100644 tests/test_no_explicit_null.py create mode 100644 tests/test_post_generate_no_explicit_null.py diff --git a/docs/design/type-generation.md b/docs/design/type-generation.md index bfceedf49..fcd883797 100644 --- a/docs/design/type-generation.md +++ b/docs/design/type-generation.md @@ -139,6 +139,22 @@ Each of these operates on the staging copy only. The cached bundle, the schemas ships and any direct-from-schema validation are untouched, which is what keeps the transforms from changing what the SDK validates against. +One more transform runs once over the whole staged tree after every file is written, +because it has to resolve `$ref`s into the documents the generator will read: +`mark_no_explicit_null_properties`. JSON Schema separates two things the generator +conflates — a property outside `required` may be *absent*, and `null` is a valid *value* +only where the declared type admits it — and the generator renders both as +`T | None = None`, so the model accepted `{"creatives": null}` where the bundled validator +reports a `type` error (#1547). The pass grades every optional property with the same +`jsonschema` validator the bundled arm uses (`is_valid(None)` on the property node, refs +resolved against the staged tree), and marks the ones that refuse `null` with +`x-adcp-no-explicit-null: true`. The generator carries the mark into +`Field(json_schema_extra=...)` via `--field-extra-keys`, and the first post-generation fix +turns it into the `NoExplicitNull` annotation (below). The key exists only in the staged +input; the shipped schemas never carry it. A property whose schema admits `null` — a type +list, an `anyOf` with a null arm, a `$ref` to a nullable schema — is not marked and keeps +accepting it. + Two of them carry explicit ceilings rather than trusting a bump not to move the problem. `normalize_version_envelope_composition` raises if it would rewrite more than 12 schemas: *"A bump changed the shape of the problem: re-measure which schemas declare the version @@ -159,8 +175,18 @@ on the file being present and absent. `scripts/post_generate_fixes.py` is 7367 lines holding 130 module-level functions: 72 fix entrypoints, built as one ordered list in `main()` and run in list order, plus 58 helpers. -Three ordering constraints are commented in the list itself and must be preserved: - +Four ordering constraints are commented in the list itself and must be preserved: + +- `point_omittable_fields_at_no_explicit_null` runs first. It removes the + `json_schema_extra={'x-adcp-no-explicit-null': True}` keyword the generator emitted for + every marked property and appends `adcp.types.base.NoExplicitNull` to the field's + `Annotated` metadata, re-rendering the field the way black lays it out, so every later + fix reads exactly the text the generator produced on the pinned flags plus that one + element. The annotation stays `T | None = None`: omitted still reads as `None`, a JSON + `null` is refused (with the type's own error; a Python `None` stays the spelling of absence + unless `ADCP_STRICT_VALIDATION` is set), and the `null` arm leaves the published JSON + Schema. A marked field whose annotation already refuses `None` (a + `Literal` with a default, a field an `allOf` arm made required) only loses the keyword. - `point_integer_fields_at_the_schema_integer_type` retypes every `type: integer`, including an integer root's own `root` annotation, so it must precede `rewrite_scalar_rootmodels` — run after, and an integer root is still spelled diff --git a/docs/validation-contract.md b/docs/validation-contract.md index db368cf72..f8e532f4e 100644 --- a/docs/validation-contract.md +++ b/docs/validation-contract.md @@ -13,6 +13,23 @@ another composition keyword. `x-adcp-validation` carries behavioral rules that neither Pydantic nor a standard JSON Schema validator interprets generically; the SDK implements security- and commitment-critical instances explicitly. +Absence and `null` are two different things, and the models keep them apart +the way the schemas do. A property outside `required` may be omitted, and +reads back as `None`; an explicit JSON `null` is a value, valid only where the +schema's type admits it. The generator marks every optional property whose +schema refuses `null` with `adcp.types.base.NoExplicitNull`: a JSON document +carrying `null` there is refused (`model_validate_json`, with the type's own +error, e.g. `list_type`) and `model_json_schema()` publishes no null arm, while +the annotation stays `T | None = None`. A property whose schema lists `null` +(`"type": ["string", "null"]`, an `anyOf` with a null arm) keeps accepting it. + +On the Python side `None` remains the spelling of "not supplied": +`Model(field=None)` and `model_validate` on a dict accept it, it reads back as +`None`, and `model_dump()` (which defaults to `exclude_none=True`) leaves it +off the wire — the same document an omitted property produces. Set +`ADCP_STRICT_VALIDATION=1` (the switch that also forbids unknown fields) to +refuse a Python `None` as well, for suites that want to catch it. + ## Wire-boundary behavior - SDK clients validate requests before sending and responses after receiving, diff --git a/scripts/generate_types.py b/scripts/generate_types.py index ba53c3730..6a6dfd536 100755 --- a/scripts/generate_types.py +++ b/scripts/generate_types.py @@ -19,6 +19,7 @@ import subprocess import sys import tempfile +import warnings from pathlib import Path from typing import Any @@ -953,6 +954,174 @@ def flatten_root_object_validation_union(schema: dict, schema_path: Path) -> dic return schema +#: Pre-generation mark on an optional property whose schema does not admit +#: ``null``. The generator carries it into ``Field(json_schema_extra=...)`` +#: (``--field-extra-keys``), and ``post_generate_fixes.point_omittable_fields_at_no_explicit_null`` +#: turns that into the ``adcp.types.base.NoExplicitNull`` annotation. The key +#: exists only in the temporary generator input; the shipped schemas never +#: carry it. +NO_EXPLICIT_NULL_KEY = "x-adcp-no-explicit-null" + + +def _file_uri_resource(path: Path): + """Load *path* as a ``referencing`` resource addressed by its file URI. + + The flattened tree spells every cross-file ``$ref`` as a relative path + (``../core/account_ref.json``). Addressing each document by its own file + URI, rather than the canonical ``$id`` the bundle declares, makes those + paths resolve against the tree the generator actually reads. + """ + from referencing import Resource + from referencing.jsonschema import DRAFT7 + + with open(path) as f: + contents = json.load(f) + if isinstance(contents, dict): + contents["$id"] = path.resolve().as_uri() + return Resource.from_contents(contents, default_specification=DRAFT7) + + +def _property_admits_null(validator, node: dict[str, Any]) -> bool: + """Whether the schema *node* accepts JSON ``null``, as the bundled validator decides. + + A node that states its own ``type`` (or ``enum`` / ``const``) answers by + inspection — that is the overwhelming majority, and it is exactly what the + validator's ``type`` keyword would check. Every other spelling — a + ``$ref``, ``anyOf``/``oneOf``/``allOf``/``not``, a conditional, a bare + object with no ``type`` — goes to the validator itself, so the model and + the JSON-Schema arm reach one verdict without this pass re-deriving the + keyword semantics. (On CI the validator path dominated the run: every + ``$ref`` re-read its target, and 74k properties took minutes.) + """ + declared = node.get("type") + composes = any(key in node for key in ("$ref", "anyOf", "oneOf", "allOf", "not", "if")) + if not composes: + if isinstance(declared, str): + return declared == "null" + if isinstance(declared, list): + return "null" in declared + if "enum" in node and isinstance(node["enum"], list): + return None in node["enum"] + if "const" in node: + return node["const"] is None + return bool(validator.evolve(schema=node).is_valid(None)) + + +def _required_names(node: Any, resolver, seen: frozenset[str] = frozenset()) -> set[str]: + """Every property *node* requires, including through ``allOf`` and ``$ref``. + + A branch that composes a base schema (``allOf: [{"$ref": ...}]``) and + restates one of its properties leaves ``required`` to the base. The + generator merges the two and renders the field required; grading the + restated property as optional would mark it, and the mark alone is what + makes the generator stop merging it (#1547). So a property any composed + schema requires is not optional here either. + """ + if not isinstance(node, dict): + return set() + required = set(node.get("required", []) if isinstance(node.get("required"), list) else []) + ref = node.get("$ref") + if isinstance(ref, str) and ref not in seen: + resolved = resolver.lookup(ref) + required |= _required_names(resolved.contents, resolved.resolver, seen | {ref}) + for arm in node.get("allOf", []) if isinstance(node.get("allOf"), list) else []: + required |= _required_names(arm, resolver, seen) + return required + + +def _mark_no_explicit_null(node: Any, validator, resolver, counter: list[int]) -> None: + if isinstance(node, list): + for item in node: + _mark_no_explicit_null(item, validator, resolver, counter) + return + if not isinstance(node, dict): + return + properties = node.get("properties") + if isinstance(properties, dict): + required_names = _required_names(node, resolver) + for name, property_schema in properties.items(): + if name in required_names or not isinstance(property_schema, dict): + continue + if not _property_admits_null(validator, property_schema): + property_schema[NO_EXPLICIT_NULL_KEY] = True + counter[0] += 1 + for key, value in node.items(): + if key == NO_EXPLICIT_NULL_KEY: + continue + _mark_no_explicit_null(value, validator, resolver, counter) + + +def mark_no_explicit_null_properties(temp_dir: Path) -> int: + """Mark every optional property whose schema refuses an explicit ``null``. + + JSON Schema separates two things the generator conflates: a property + outside ``required`` may be *absent*, and ``null`` is a valid *value* only + where the declared type admits it. The generator renders both as + ``T | None = None``, so without this pass the model accepts + ``{"creatives": null}`` where the bundled schema — and so the SDK's own + ``validate_request`` — reports a ``type`` error (#1547). + + This runs over the whole flattened tree once every file is written, so a + ``$ref`` resolves to the document the generator will read. Each property + is graded by the same ``jsonschema`` validator the bundled arm uses, and + a property it says cannot be ``null`` is marked with + :data:`NO_EXPLICIT_NULL_KEY`. A property whose schema admits ``null`` is + left alone and keeps accepting it. Returns the number of marked properties. + """ + from jsonschema.validators import validator_for + from referencing import Registry + + def retrieve(uri: str): + from urllib.parse import urlparse + from urllib.request import url2pathname + + parsed = urlparse(uri) + if parsed.scheme == "file": + return _file_uri_resource(Path(url2pathname(parsed.path))) + # The references in PRESERVE_CANONICAL_URL_REFS keep their canonical + # URL; the generator reads those from a mirror of the same cache, and + # this pass reads them from the flattened tree the mirror was built from. + canonical = re.fullmatch(r"https://adcontextprotocol\.org/schemas/[^/]+/(.+)", uri) + if canonical is None: + raise LookupError(uri) + relative = Path(*(part.replace("-", "_") for part in Path(canonical.group(1)).parts)) + return _file_uri_resource(temp_dir / relative) + + # Every document in the tree, keyed by its file URI, so a ``$ref`` never + # retrieves from disk: a validator built per file starts from this one + # registry rather than re-reading each target it reaches. + registry = Registry(retrieve=retrieve).with_resources( + (path.resolve().as_uri(), _file_uri_resource(path)) + for path in sorted(temp_dir.rglob("*.json")) + ) + marked = 0 + for schema_file in sorted(temp_dir.rglob("*.json")): + with open(schema_file) as f: + schema = json.load(f) + if not isinstance(schema, dict): + continue + # Validate with a copy addressed by file URI; write the original back, + # so the mark is the only thing this pass adds to the generator input. + document = copy.deepcopy(schema) + document["$id"] = schema_file.resolve().as_uri() + with warnings.catch_warnings(): + # A few root documents name themselves rather than a metaschema in + # ``$schema``; jsonschema grades those with its latest draft and + # says so. The verdict on ``null`` is the same under every draft. + warnings.simplefilter("ignore", DeprecationWarning) + validator_class = validator_for(document) + validator = validator_class(document, registry=registry) + resolver = registry.resolver(base_uri=document["$id"]) + counter = [0] + _mark_no_explicit_null(schema, validator, resolver, counter) + if counter[0]: + with open(schema_file, "w") as f: + json.dump(schema, f, indent=2) + marked += counter[0] + print(f" Marked {marked} optional properties that refuse an explicit null") + return marked + + def flatten_schemas(temp_dir: Path): """ Copy schemas to temp directory, preserving directory structure. @@ -1047,6 +1216,7 @@ def flatten_schemas(temp_dir: Path): print(f" {rel_path}") count = len(schema_files) + mark_no_explicit_null_properties(temp_dir) if _VERSION_ENVELOPE_NORMALIZED > _VERSION_ENVELOPE_NORMALIZED_CEILING: raise ValueError( f"version-envelope normalization rewrote {_VERSION_ENVELOPE_NORMALIZED} schemas, " @@ -1161,6 +1331,11 @@ def _run_datamodel_codegen(input_path: Path, output_path: Path) -> subprocess.Co "--set-default-enum-member", "--enum-field-as-literal", "one", + # Carry the optional-but-not-nullable mark set by + # ``mark_no_explicit_null_properties`` into ``Field(json_schema_extra=...)``, + # where the post-generation pass turns it into ``NoExplicitNull``. + "--field-extra-keys", + NO_EXPLICIT_NULL_KEY, "--allow-remote-refs", ] diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index bb525cb40..ca322f248 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -1039,6 +1039,301 @@ def point_integer_fields_at_the_schema_integer_type() -> None: print(f" Pointed integer fields at SchemaInt in {modified} file(s)") +#: The ``Field`` keyword the generator emits for ``generate_types.NO_EXPLICIT_NULL_KEY``. +_NO_EXPLICIT_NULL_EXTRA = "json_schema_extra={'x-adcp-no-explicit-null': True}" +_NO_EXPLICIT_NULL_MARKER = "NoExplicitNull" + + +def _generated_line_width() -> int: + """The line length the generator's black pass used. + + datamodel-code-generator hands black ``settings_path=output``, so black + reads ``[tool.black] line-length`` from the nearest ``pyproject.toml`` + above the generated tree — this repository's. The rewrite below re-renders + a field the way black laid it out, so it must measure against the same + width. + """ + try: + pyproject = (REPO_ROOT / "pyproject.toml").read_text() + except FileNotFoundError: + return 88 + section = re.search(r"^\[tool\.black\]\n(.*?)(?=^\[|\Z)", pyproject, re.MULTILINE | re.DOTALL) + configured = section and re.search(r"^line-length\s*=\s*(\d+)", section.group(1), re.MULTILINE) + return int(configured.group(1)) if configured else 88 + + +_GENERATED_LINE_WIDTH = _generated_line_width() + + +def _absolute_offsets(source: bytes) -> list[int]: + """Byte offset of each line start. ``ast`` column offsets count UTF-8 bytes.""" + offsets = [0] + for line in source.splitlines(keepends=True): + offsets.append(offsets[-1] + len(line)) + return offsets + + +def _node_span(node: ast.AST, offsets: list[int]) -> tuple[int, int]: + assert node.end_lineno is not None and node.end_col_offset is not None + return ( + offsets[node.lineno - 1] + node.col_offset, + offsets[node.end_lineno - 1] + node.end_col_offset, + ) + + +def _segment(source: bytes, offsets: list[int], node: ast.AST) -> str: + """The source text of *node*, via precomputed byte offsets. + + ``ast.get_source_segment`` re-splits the whole module on every call, which + is quadratic over a generated module's fields. Spans are in bytes because + that is what ``ast`` column offsets count — a description holding an em + dash shifts every later column on its line. + """ + start, end = _node_span(node, offsets) + return source[start:end].decode("utf-8") + + +def _no_explicit_null_keyword(call: ast.expr) -> ast.keyword | None: + """The ``json_schema_extra`` keyword carrying the mark, if *call* is that ``Field(...)``.""" + if not ( + isinstance(call, ast.Call) and isinstance(call.func, ast.Name) and call.func.id == "Field" + ): + return None + for keyword in call.keywords: + if keyword.arg != "json_schema_extra" or not isinstance(keyword.value, ast.Dict): + continue + keys = [k.value for k in keyword.value.keys if isinstance(k, ast.Constant)] + if keys == ["x-adcp-no-explicit-null"]: + return keyword + return None + + +def _field_call_without_mark( + call: ast.Call, keyword: ast.keyword, source: bytes, offsets: list[int] +) -> list[str] | None: + """The ``Field(...)`` call's remaining arguments, each as source text. + + Returns ``None`` when the mark was the only argument, so the caller drops + the ``Field`` metadata altogether. + """ + arguments = [_segment(source, offsets, positional) for positional in call.args] + arguments += [ + _segment(source, offsets, other) for other in call.keywords if other is not keyword + ] + return arguments or None + + +def _render_field_call(arguments: list[str], indent: int) -> str: + """Render ``Field(...)`` at *indent* the way black lays out a call. + + One line when it fits; otherwise the arguments hugged on one inner line + when that fits; otherwise one argument per line with a trailing comma. + The ``+ 1`` leaves room for the comma the enclosing subscript adds. + """ + joined = ", ".join(arguments) + one_line = f"Field({joined})" + if "\n" not in one_line and indent + len(one_line) + 1 <= _GENERATED_LINE_WIDTH: + return one_line + inner_indent = " " * (indent + 4) + # A single argument has no delimiter to split on, so black keeps it on + # one inner line however long it is, and adds no trailing comma. + if "\n" not in joined and ( + len(arguments) == 1 or len(inner_indent) + len(joined) <= _GENERATED_LINE_WIDTH + ): + return f"Field(\n{inner_indent}{joined}\n{' ' * indent})" + lines = ["Field("] + for argument in arguments: + lines.append(f"{inner_indent}{argument},") + lines.append(f"{' ' * indent})") + return "\n".join(lines) + + +def _render_annotated( + prefix: str, type_source: str, metadata: list[str | list[str]], suffix: str +) -> str: + """Render ``prefix Annotated[type, *metadata] suffix`` the way black would. + + A metadata entry is either source text or, for the ``Field(...)`` call, + its argument list, which is laid out for whichever form is chosen. + """ + default = re.fullmatch(r" = (.+)", suffix) + + def split_right_hand_side(annotation: str) -> str | None: + # Black splits the right-hand side first: when everything up to + # ``= (`` fits, the default goes on its own line and the annotation + # stays whole. + head = f"{prefix}{annotation} = (" + if default is None or "\n" in head or len(head) > _GENERATED_LINE_WIDTH: + return None + return f"{head}\n {default.group(1)}\n )" + + if not metadata: + bare = f"{prefix}{type_source}{suffix}" + if "\n" not in type_source and len(bare) <= _GENERATED_LINE_WIDTH: + return bare + if "\n" not in type_source and (rhs := split_right_hand_side(type_source)) is not None: + return rhs + # Black splits a trailing subscript at its bracket; anything else it + # wraps in parentheses. A type that already spans lines was inside + # such parentheses, and keeps its own layout. + subscript = re.fullmatch(r"([\w.]+)\[(.+)\]", type_source, re.DOTALL) + if "\n" not in type_source and subscript is not None: + return f"{prefix}{subscript.group(1)}[\n {subscript.group(2)}\n ]{suffix}" + return f"{prefix}(\n {type_source}\n ){suffix}" + + def flat(element: str | list[str], indent: int) -> str: + return _render_field_call(element, indent) if isinstance(element, list) else element + + one_line_elements = [type_source, *(flat(element, 0) for element in metadata)] + inner = ", ".join(one_line_elements) + one_line = f"{prefix}Annotated[{inner}]{suffix}" + if "\n" not in one_line and len(one_line) <= _GENERATED_LINE_WIDTH: + return one_line + if "\n" not in inner and (rhs := split_right_hand_side(f"Annotated[{inner}]")) is not None: + return rhs + hugged = f" {inner}" + if "\n" not in hugged and len(hugged) <= _GENERATED_LINE_WIDTH: + return f"{prefix}Annotated[\n{hugged}\n ]{suffix}" + lines = [f"{prefix}Annotated["] + for element in [type_source, *metadata]: + lines.append(f" {flat(element, 8)},") + lines.append(f" ]{suffix}") + return "\n".join(lines) + + +def _rewrite_no_explicit_null_marks( + source: str, marker: str | None = _NO_EXPLICIT_NULL_MARKER +) -> tuple[str, int]: + """Turn every marked ``Field`` keyword into the ``NoExplicitNull`` annotation. + + Returns the rewritten source and the number of fields that now carry the + marker. A marked field whose annotation already refuses ``None`` (a + ``Literal`` with a default, a required field an ``allOf`` arm supplied) + only loses the keyword. With ``marker=None`` the keyword is removed and + nothing is appended, which is how the layout is graded against the + generator's own: the result must be byte-identical to a run without the + mark. + """ + raw = source.encode("utf-8") + tree = ast.parse(raw) + offsets = _absolute_offsets(raw) + edits: list[tuple[int, int, bytes]] = [] + marked = 0 + for node in ast.walk(tree): + if not isinstance(node, ast.AnnAssign) or not isinstance(node.target, ast.Name): + continue + annotation = node.annotation + if not ( + isinstance(annotation, ast.Subscript) + and isinstance(annotation.value, ast.Name) + and annotation.value.id == "Annotated" + and isinstance(annotation.slice, ast.Tuple) + ): + continue + elements = annotation.slice.elts + keyword = None + for element in elements[1:]: + keyword = _no_explicit_null_keyword(element) + if keyword is not None: + field_call = element + break + if keyword is None: + continue + type_source = _segment(raw, offsets, elements[0]) + metadata: list[str | list[str]] = [] + for element in elements[1:]: + if element is field_call: + assert isinstance(element, ast.Call) + arguments = _field_call_without_mark(element, keyword, raw, offsets) + if arguments is not None: + metadata.append(arguments) + continue + metadata.append(_segment(raw, offsets, element)) + annotation_names = {n.id for n in ast.walk(elements[0]) if isinstance(n, ast.Name)} | { + n.value for n in ast.walk(elements[0]) if isinstance(n, ast.Constant) + } + if None in annotation_names and marker is not None: + metadata.append(marker) + marked += 1 + # Measure from the start of the line: the indentation counts toward + # the width black lays the statement out against. + line_start = offsets[node.lineno - 1] + _statement_start, statement_end = _node_span(node, offsets) + annotation_start, annotation_end = _node_span(annotation, offsets) + prefix = raw[line_start:annotation_start].decode("utf-8") + suffix = raw[annotation_end:statement_end].decode("utf-8") + # A default black split onto its own line sits in parentheses the + # AST does not span: ``= (\n None\n )``. Read it back as + # ``= None`` and consume the closing parenthesis. + wrapped = re.fullmatch(r" = \(\s*(.+?)\s*(\))?", suffix, re.DOTALL) + if wrapped is not None: + if wrapped.group(2) is None: + closing = re.match(rb"\s*\)", raw[statement_end:]) + assert closing is not None, (node.target.id, suffix) + statement_end += closing.end() + suffix = f" = {wrapped.group(1)}" + rendered = _render_annotated(prefix, type_source, metadata, suffix) + edits.append((line_start, statement_end, rendered.encode("utf-8"))) + for start, end, replacement in sorted(edits, reverse=True): + raw = raw[:start] + replacement + raw[end:] + return raw.decode("utf-8"), marked + + +def _ensure_no_explicit_null_import(source: str) -> str: + """Import ``NoExplicitNull`` from ``adcp.types.base`` when the module lacks it.""" + if re.search(r"^from adcp\.types\.base import .*\bNoExplicitNull\b", source, re.MULTILINE): + return source + if re.search(r"^from adcp\.types\.base import ", source, re.MULTILINE): + return add_to_import(source, "adcp.types.base", _NO_EXPLICIT_NULL_MARKER) + statement = f"from adcp.types.base import {_NO_EXPLICIT_NULL_MARKER}\n" + anchor = _import_pattern("pydantic").search(source) + if anchor is not None: + return source[: anchor.start()] + statement + source[anchor.start() :] + # No pydantic import to sit beside (a fixture module): after the typing + # import, else after ``from __future__``, else at the top. + for pattern in (r"^from typing import [^\n]*\n", r"^from __future__ import [^\n]*\n\n?"): + match = re.search(pattern, source, re.MULTILINE) + if match is not None: + return source[: match.end()] + statement + source[match.end() :] + return statement + source + + +def point_omittable_fields_at_no_explicit_null() -> None: + """Refuse an explicit ``null`` on every optional property whose schema refuses one. + + ``generate_types.mark_no_explicit_null_properties`` grades each optional + property with the bundled validator and marks the ones that cannot be + ``null``; the generator carries the mark as a ``Field(json_schema_extra=...)`` + keyword. This pass removes that keyword and appends + ``adcp.types.base.NoExplicitNull`` to the field's ``Annotated`` metadata, + so the annotation stays ``T | None = None`` — omitted still reads as + ``None`` — while an explicit ``null`` is refused on input and the ``null`` + arm leaves the published JSON Schema. See #1547. + """ + modified = 0 + marked = 0 + for path in sorted(OUTPUT_DIR.rglob("*.py")): + source = path.read_text() + if _NO_EXPLICIT_NULL_EXTRA not in source: + continue + source, count = _rewrite_no_explicit_null_marks(source) + assert _NO_EXPLICIT_NULL_EXTRA not in source, path + # A module whose only ``Field`` or ``Annotated`` was the mark's is left + # exactly as the generator writes it without the mark. + if "Field(" not in source: + source = drop_from_imports(source, "pydantic", "Field") + if "Annotated[" not in source: + source = drop_from_imports(source, "typing", "Annotated") + # Dropping the only ``typing`` import leaves its blank line behind. + source = re.sub(r"\n{3,}(from |import )", r"\n\n\1", source, count=1) + if count: + source = _ensure_no_explicit_null_import(source) + path.write_text(source) + modified += 1 + marked += count + print(f" Pointed {marked} optional field(s) at NoExplicitNull in {modified} file(s)") + + def remove_imports_shadowed_by_a_local_class() -> None: """Drop an import whose name a class in the same module redefines. @@ -3406,15 +3701,21 @@ def _ensure_sequence_import(content: str) -> str: _CANCELED_FIELD_RE = re.compile( r"( canceled: Annotated\[\n )" r"Literal\[True\]" - r"(,\n Field\(\n description=[^\n]*\n \),\n \])" + r"(,\n Field\(\n description=[^\n]*\n \),\n)" + r"( \])" r" = True" ) +#: The widened form: optional, so ``None`` when omitted, and — as the schema +#: declares a boolean — never an explicit ``null`` (#1547). +_WIDENED_LITERAL_TRUE = r"\1Literal[True] | None\2 NoExplicitNull,\n\3 = None" + _UNCHANGED_FIELD_RE = re.compile( r"( unchanged: Annotated\[\n )" r"Literal\[True\]" - r"(,\n Field\(\n description=.*?\n \),\n \])" + r"(,\n Field\(\n description=.*?\n \),\n)" + r"( \])" r" = True", re.DOTALL, ) @@ -3454,9 +3755,11 @@ def fix_canceled_literal_defaults() -> None: source = py_file.read_text() new_source, count = _CANCELED_FIELD_RE.subn( - r"\1Literal[True] | None\2 = None", + _WIDENED_LITERAL_TRUE, source, ) + if count: + new_source = _ensure_no_explicit_null_import(new_source) if count == 0: print(f" {py_file.relative_to(OUTPUT_DIR)}: no destructive canceled defaults found") @@ -3497,9 +3800,11 @@ def fix_unchanged_literal_defaults() -> None: source = py_file.read_text() new_source, count = _UNCHANGED_FIELD_RE.subn( - r"\1Literal[True] | None\2 = None", + _WIDENED_LITERAL_TRUE, source, ) + if count: + new_source = _ensure_no_explicit_null_import(new_source) if count == 0: print(f" {py_file.relative_to(OUTPUT_DIR)}: no unchanged defaults found") @@ -3548,16 +3853,29 @@ def fix_optional_boolean_literal_defaults() -> None: if not isinstance(node.value, ast.Constant) or node.value.value is not literal_value: continue annotation = node.annotation + widened = f"Literal[{literal_value}] | None" if ( isinstance(annotation, ast.Subscript) and _subscript_base_name(annotation) == "Annotated" ): - annotation = _first_subscript_arg(annotation) - assert annotation is not None - for part, replacement in ( - (annotation, f"Literal[{literal_value}] | None".encode()), - (node.value, b"None"), - ): + # Widen the type and slot the marker in before the closing + # bracket, after the existing metadata. + first = _first_subscript_arg(annotation) + assert first is not None + closing = offsets[annotation.end_lineno - 1] + annotation.end_col_offset - 1 + assert source[closing : closing + 1] == b"]" + exploded = ( + b"\n" + in source[offsets[annotation.lineno - 1] + annotation.col_offset : closing] + ) + # The closing bracket of an exploded subscript already sits on + # its own indented line; add one element at the element indent. + marker = b" NoExplicitNull,\n " if exploded else b", NoExplicitNull" + edits.append((closing, closing, marker)) + replacements = [(first, widened.encode())] + else: + replacements = [(annotation, f"Annotated[{widened}, NoExplicitNull]".encode())] + for part, replacement in (*replacements, (node.value, b"None")): edits.append( ( offsets[part.lineno - 1] + part.col_offset, @@ -3569,7 +3887,8 @@ def fix_optional_boolean_literal_defaults() -> None: if edits: for start, end, replacement in sorted(edits, reverse=True): source = source[:start] + replacement + source[end:] - py_file.write_bytes(source) + text = _ensure_typing_import(source.decode(), "Annotated") + py_file.write_text(_ensure_no_explicit_null_import(text)) print( f" {py_file.relative_to(OUTPUT_DIR)}: fixed {len(edits) // 2} boolean const field(s)" ) @@ -4005,9 +4324,7 @@ def _require_identity_match_dimensions(self) -> TmpProviderRegistration1: print(" trusted_match/identity_match_response.py response class not found") return response_class = response_class_match.group(1) - source = ( - source.rstrip() - + f""" + source = source.rstrip() + f""" @model_validator(mode='after') def _validate_tmpx_provider_ids(self) -> {response_class}: @@ -4022,7 +4339,6 @@ def _validate_tmpx_provider_ids(self) -> {response_class}: raise ValueError('tmpx_providers keys must be valid provider_id values') return self """ - ) identity_match_response.write_text(source.rstrip() + "\n") print(" trusted_match/identity_match_response.py: added runtime validators") @@ -4857,9 +5173,7 @@ def restore_flattened_contract_field_types() -> None: if "@model_validator(mode='before')" not in source: source = ensure_pydantic_import(source, "model_validator") - source = ( - source.rstrip() - + """ + source = source.rstrip() + """ @model_validator(mode='before') @classmethod @@ -4874,7 +5188,6 @@ def _reject_seller_bound_manifest_fields(cls, data: Any) -> Any: ) return data """ - ) representation_target.write_text(source) print(" core/creative_representation.py: restored canonical format contract") @@ -4907,9 +5220,7 @@ def enforce_transformer_output_contract() -> None: if "class Transformer(" not in source: raise RuntimeError("transformer.py: Transformer class not found") source = ensure_pydantic_import(source, "model_validator") - target.write_text( - source.rstrip() - + """ + target.write_text(source.rstrip() + """ @model_validator(mode='after') def _require_output_format_declaration(self) -> Transformer: @@ -4924,8 +5235,7 @@ def _require_output_format_declaration(self) -> Transformer: 'one of output_capability_ids or deprecated output_format_ids is required' ) return self -""" - ) +""") print(" core/transformer.py: enforced output declaration requirement") @@ -5525,11 +5835,16 @@ def emit_response_class(self, class_name: str, arm: dict[str, Any]) -> str: ) if inherits_protocol_envelope: self.needs_protocol_envelope = True - lines = [f"class {class_name}({bases}):"] - if is_submitted: - lines.append(" model_config = ConfigDict(extra='allow', validate_default=True)") - else: - lines.append(" model_config = ConfigDict(extra='allow')") + # Submitted arms once paired ``validate_default=True`` with + # ``use_enum_values`` so the ``status`` default came out as its + # value; the enum is a ``StrEnum`` now and the default is already + # the literal's member. Validating defaults would also run every + # inherited ``= None`` default through ``NoExplicitNull`` (#1547), + # which refuses it, so the arms take the base's default policy. + lines = [ + f"class {class_name}({bases}):", + " model_config = ConfigDict(extra='allow')", + ] is_sync_media_buy_success = self.base in { "CreateMediaBuyResponse", "UpdateMediaBuyResponse", @@ -5847,8 +6162,7 @@ def fix_verify_brand_claim_models() -> None: bulk_response = OUTPUT_DIR / "brand" / "verify_brand_claims_response.py" if request.exists() and "claim_type:" not in request.read_text(): - request.write_text( - """# generated by datamodel-codegen: + request.write_text("""# generated by datamodel-codegen: # filename: brand/verify_brand_claim_request.json from __future__ import annotations @@ -5880,13 +6194,11 @@ class VerifyBrandClaimRequest(AdcpVersionEnvelope): dict[str, Any], Field(description='Claim payload. Shape varies by claim_type.'), ] -""" - ) +""") print(" brand/verify_brand_claim_request.py: restored claim fields") if response.exists() and "VerifyBrandClaimSuccessResponse" not in response.read_text(): - response.write_text( - """# generated by datamodel-codegen: + response.write_text("""# generated by datamodel-codegen: # filename: brand/verify_brand_claim_response.json from __future__ import annotations @@ -5945,8 +6257,7 @@ class VerifyBrandClaimErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope): VerifyBrandClaimResponse = VerifyBrandClaimSuccessResponse | VerifyBrandClaimErrorResponse -""" - ) +""") print(" brand/verify_brand_claim_response.py: restored response arms") response_schema = SCHEMA_DIR / "brand" / "verify-brand-claim-response.json" @@ -5955,8 +6266,7 @@ class VerifyBrandClaimErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope): and response_schema.exists() and '"signed_response"' in response_schema.read_text() ): - response.write_text( - """# generated by datamodel-codegen: + response.write_text("""# generated by datamodel-codegen: # filename: brand/verify_brand_claim_response.json from __future__ import annotations @@ -6107,8 +6417,7 @@ class VerifyBrandClaimErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope): VerifyBrandClaimResponse = VerifyBrandClaimSuccessResponse | VerifyBrandClaimErrorResponse -""" - ) +""") print(" brand/verify_brand_claim_response.py: restored signed response fields") bulk_response_schema = SCHEMA_DIR / "brand" / "verify-brand-claims-response.json" @@ -6117,8 +6426,7 @@ class VerifyBrandClaimErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope): and bulk_response_schema.exists() and '"signed_response"' in bulk_response_schema.read_text() ): - bulk_response.write_text( - """# generated by datamodel-codegen: + bulk_response.write_text("""# generated by datamodel-codegen: # filename: brand/verify_brand_claims_response.json from __future__ import annotations @@ -6301,8 +6609,7 @@ class VerifyBrandClaimsErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope): VerifyBrandClaimsResponse = VerifyBrandClaimsResponseBulk | VerifyBrandClaimsErrorResponse -""" - ) +""") print(" brand/verify_brand_claims_response.py: restored signed response fields") if bulk_response.exists(): @@ -6834,9 +7141,7 @@ def fix_legacy_purchase_accepted_losses() -> None: ) validator_marker = " def _accepted_losses_match_schema(" if validator_marker not in fixed and "field_validator" in fixed: - fixed = ( - fixed.rstrip() - + """ + fixed = fixed.rstrip() + """ @field_validator('selected_product_ids') @@ -6864,7 +7169,6 @@ def _accepted_losses_match_schema( raise ValueError('accepted_losses must include the required compatibility losses') return values """ - ) if fixed != source: target.write_text(fixed) print(" media_buy/legacy_purchase_continuation_input.py: narrowed accepted_losses") @@ -6995,9 +7299,7 @@ def _require_portable_bound(self) -> {class_name}: ) class_start = source.find("class MediaBuyChangeTerm(AdCPBaseModel):") if class_start >= 0 and "def _validate_constraint_action" not in source[class_start:]: - source = ( - source.rstrip() - + """ + source = source.rstrip() + """ @model_validator(mode='after') def _validate_constraint_action(self) -> MediaBuyChangeTerm: @@ -7017,9 +7319,7 @@ def _validate_constraint_action(self) -> MediaBuyChangeTerm: if action not in allowed.get(kind, set()): raise ValueError('constraint kind is incompatible with action') return self -""" - + "\n" - ) +""" + "\n" term_path.write_text(source) print(" media_buy/change_term.py: restored constraint/action compatibility") @@ -7032,9 +7332,7 @@ def _validate_constraint_action(self) -> MediaBuyChangeTerm: ) class_start = source.find("class CommercialTerms(AdCPBaseModel):") if class_start >= 0 and "def _validate_change_term_set" not in source[class_start:]: - source = ( - source.rstrip() - + """ + source = source.rstrip() + """ @model_validator(mode='after') def _validate_change_term_set(self) -> CommercialTerms: @@ -7083,9 +7381,7 @@ def _validate_change_term_set(self) -> CommercialTerms: ): raise ValueError('change-term earliest effective time exceeds latest time') return self -""" - + "\n" - ) +""" + "\n" terms_path.write_text(source) print(" media_buy/commercial_terms.py: restored change-term set invariants") @@ -7989,6 +8285,11 @@ def main(argv: list[str] | None = None) -> int: print("Applying post-generation fixes...") fixes = [ + # First: every later fix reads the field text the generator produced + # on the pinned flags, and this pass restores exactly that text (the + # mark's keyword is the only thing the generator added) plus the + # ``NoExplicitNull`` element it appends to a marked field. + point_omittable_fields_at_no_explicit_null, add_model_validator_to_product, fix_preview_render_self_reference, fix_brand_manifest_references, diff --git a/scripts/post_generation_manifest.json b/scripts/post_generation_manifest.json index 008136d54..3fdf39234 100644 --- a/scripts/post_generation_manifest.json +++ b/scripts/post_generation_manifest.json @@ -71,6 +71,7 @@ "preserve_format_reference_agent_url_wire_string": true, "insert_task_message_markers": true, "point_integer_fields_at_the_schema_integer_type": true, + "point_omittable_fields_at_no_explicit_null": true, "remove_imports_shadowed_by_a_local_class": true, "remove_unused_pydantic_field_imports": true, "emit_open_asset_unions": true, diff --git a/src/adcp/types/base.py b/src/adcp/types/base.py index 61f78d880..1bfc4832d 100644 --- a/src/adcp/types/base.py +++ b/src/adcp/types/base.py @@ -19,6 +19,7 @@ BaseModel, BeforeValidator, ConfigDict, + GetCoreSchemaHandler, SerializerFunctionWrapHandler, StrictInt, StrictStr, @@ -26,7 +27,7 @@ WithJsonSchema, model_serializer, ) -from pydantic_core import PydanticSerializationError +from pydantic_core import PydanticSerializationError, core_schema from adcp._deferred_adapters import deferred_adapter from adcp.types._geo_place_keys import GeoPlaceSystemKey as GeoPlaceSystemKey @@ -310,6 +311,110 @@ def _url_adapter() -> TypeAdapter[AnyUrl]: ] +def _without_nullable(schema: Any) -> core_schema.CoreSchema: + """The schema with its ``nullable`` wrapper removed, if it has one. + + The wrapper is usually outermost. A field another annotation already + rebuilt — the targeting-overlay bridge builds its own ``json_or_python`` + with validator functions around the type — carries it further in, and + only the JSON side of such a schema matters here. + """ + kind = schema["type"] + if kind == "nullable": + return cast(core_schema.CoreSchema, schema["schema"]) + if kind == "json-or-python": + return cast( + core_schema.CoreSchema, + {**schema, "json_schema": _without_nullable(schema["json_schema"])}, + ) + if kind in ("function-before", "function-after", "function-wrap", "json", "default"): + stripped = {**schema, "schema": _without_nullable(schema["schema"])} + # A validator function may publish a separate input schema; the JSON + # Schema is generated from that one when it is present. + if "json_schema_input_schema" in stripped: + stripped["json_schema_input_schema"] = _without_nullable( + stripped["json_schema_input_schema"] + ) + return cast(core_schema.CoreSchema, stripped) + return cast(core_schema.CoreSchema, schema) + + +class _NoExplicitNull: + """Annotated metadata: the property may be omitted, but a JSON ``null`` is refused. + + A JSON Schema property outside ``required`` whose type does not admit + ``null`` permits exactly one way of not supplying it — leaving it out. + The generator renders every optional property as ``T | None = None``, + and that annotation is the right Python surface: the attribute reads as + ``None`` when the property was omitted, and ``model_dump`` (which + defaults to ``exclude_none=True``) leaves it off the wire again. What + the annotation got wrong is *input*: it also admitted an explicit + ``null``, which the schema — and the SDK's own JSON-Schema arm, + :func:`adcp.validation.validate_request` — rejects. + + This marker closes that gap without changing the annotation. It builds + a ``json_or_python`` core schema: the JSON side is the type without its + null arm, so a document carrying ``null`` fails with the type's own + error (``list_type``, ``model_type``, ...) exactly as a required field + would, and ``model_json_schema()`` publishes no null arm; the Python + side keeps ``T | None``, so ``Model(field=None)`` and ``model_validate`` + on a dict still read ``None`` as the SDK's spelling of "not supplied", + symmetric with ``model_dump``. The ``= None`` default is not validated, + so omission reads back as ``None`` either way. No Python function sits + in the validation path, so nested validation keeps pydantic's native + JSON handling (duplicate-key diagnostics, error order). + + ``ADCP_STRICT_VALIDATION`` — the switch that also forbids unknown + fields — makes the Python side refuse ``None`` too, for suites that want + to catch a caller spelling absence as an explicit ``None``. A property + whose schema *does* admit ``null`` is generated without the marker and + keeps accepting it. + + The generator marks every such property from the schema + (``scripts/generate_types.py``, ``mark_no_explicit_null_properties``) + and ``scripts/post_generate_fixes.py`` points the mark here. See #1547. + """ + + __slots__ = () + + def __get_pydantic_core_schema__( + self, source: Any, handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + nullable = handler(source) + strict = _without_nullable(nullable) + return core_schema.json_or_python_schema( + json_schema=strict, + python_schema=strict if _EXTRA_POLICY == "forbid" else nullable, + ) + + def __repr__(self) -> str: + return "NoExplicitNull" + + # One marker, however a ``FieldInfo`` carrying it is copied: a canonical + # redeclaration takes ``copy.deepcopy`` of the generated field + # (``canonical_creative._inherit``), and a test asks ``NoExplicitNull in + # field.metadata``. + def __copy__(self) -> _NoExplicitNull: + return self + + def __deepcopy__(self, memo: dict[int, Any]) -> _NoExplicitNull: + return self + + def __reduce__(self) -> str: + return "NoExplicitNull" + + def __eq__(self, other: object) -> bool: + return isinstance(other, _NoExplicitNull) + + def __hash__(self) -> int: + return hash(_NoExplicitNull) + + +#: Marks an optional property whose schema does not admit ``null``: omit it or +#: supply a value. See :class:`_NoExplicitNull`. +NoExplicitNull = _NoExplicitNull() + + class AdCPBaseModel(BaseModel): """Base model for AdCP types with spec-compliant serialization. diff --git a/src/adcp/types/canonical_creative.py b/src/adcp/types/canonical_creative.py index 6c7809df3..6b70e76aa 100644 --- a/src/adcp/types/canonical_creative.py +++ b/src/adcp/types/canonical_creative.py @@ -101,7 +101,7 @@ PRODUCT_FORMAT_DECLARATION_SCHEMA, check_declaration_rules, ) -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types.domains.core.canonical_format_kind import CanonicalFormatKind from adcp.types.domains.core.creative_asset import CreativeAsset as _CanonicalCreativeWire from adcp.types.domains.core.creative_filters import CreativeFilters as _LegacyCreativeFilters @@ -318,6 +318,9 @@ def _has_boundary_serialization_metadata(annotation: Any) -> bool: """Find functional serializers, including ones inside container annotations.""" if isinstance(annotation, (PlainSerializer, WrapSerializer)): return True + if annotation is NoExplicitNull: + # Validation-only: it changes nothing about how a value is dumped. + return False if hasattr(annotation, "__get_pydantic_core_schema__"): is_adcp_model = isinstance(annotation, type) and issubclass(annotation, AdCPBaseModel) if not is_adcp_model and getattr(annotation, "__module__", "").split(".")[0] != "pydantic": diff --git a/src/adcp/types/domains/a2ui/component.py b/src/adcp/types/domains/a2ui/component.py index 6b4065cd3..140976431 100644 --- a/src/adcp/types/domains/a2ui/component.py +++ b/src/adcp/types/domains/a2ui/component.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: a2ui/component.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -16,7 +16,7 @@ class A2UiComponent(AdCPBaseModel): ) id: Annotated[str, Field(description='Unique identifier for this component within the surface')] parentId: Annotated[ - str | None, Field(description='ID of the parent component (null for root)') + str | None, Field(description='ID of the parent component (null for root)'), NoExplicitNull ] = None component: Annotated[ dict[str, dict[str, Any]], diff --git a/src/adcp/types/domains/a2ui/si_catalog.py b/src/adcp/types/domains/a2ui/si_catalog.py index 096662c7e..455b52dde 100644 --- a/src/adcp/types/domains/a2ui/si_catalog.py +++ b/src/adcp/types/domains/a2ui/si_catalog.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: a2ui/si_catalog.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, Field, StrictBool from . import bound_value @@ -32,7 +32,7 @@ class SiComponentCatalog(AdCPBaseModel): Literal['si-standard'], Field(description='SI standard component catalog') ] = 'si-standard' components: Annotated[ - list[Component] | None, Field(description='Available component types') + list[Component] | None, Field(description='Available component types'), NoExplicitNull ] = None @@ -45,7 +45,7 @@ class Variant(StrEnum): class Text(AdCPBaseModel): text: Annotated[bound_value.A2UiBoundValue, Field(description='Text content to display')] - variant: Variant | None = Variant.body + variant: Annotated[Variant | None, NoExplicitNull] = Variant.body class Action(AdCPBaseModel): @@ -53,6 +53,7 @@ class Action(AdCPBaseModel): context: Annotated[ dict[str, bound_value.A2UiBoundValue] | None, Field(description='Context data to include with action'), + NoExplicitNull, ] = None @@ -65,57 +66,73 @@ class Variant4(StrEnum): class Button(AdCPBaseModel): label: Annotated[bound_value.A2UiBoundValue, Field(description='Button label text')] action: Annotated[Action, Field(description='Action to trigger on click')] - variant: Variant4 | None = Variant4.primary - disabled: bound_value.A2UiBoundValue | None = None + variant: Annotated[Variant4 | None, NoExplicitNull] = Variant4.primary + disabled: Annotated[bound_value.A2UiBoundValue | None, NoExplicitNull] = None class Link(AdCPBaseModel): label: Annotated[bound_value.A2UiBoundValue, Field(description='Link text')] url: Annotated[bound_value.A2UiBoundValue, Field(description='URL to navigate to')] - external: Annotated[StrictBool | None, Field(description='Opens in new tab if true')] = True + external: Annotated[ + StrictBool | None, Field(description='Opens in new tab if true'), NoExplicitNull + ] = True class Image(AdCPBaseModel): src: Annotated[bound_value.A2UiBoundValue, Field(description='Image source URL')] alt: Annotated[bound_value.A2UiBoundValue, Field(description='Alt text for accessibility')] - width: Annotated[SchemaInt | None, Field(description='Image width in pixels')] = None - height: Annotated[SchemaInt | None, Field(description='Image height in pixels')] = None + width: Annotated[ + SchemaInt | None, Field(description='Image width in pixels'), NoExplicitNull + ] = None + height: Annotated[ + SchemaInt | None, Field(description='Image height in pixels'), NoExplicitNull + ] = None class Action23(AdCPBaseModel): name: str - context: dict[str, bound_value.A2UiBoundValue] | None = None + context: Annotated[dict[str, bound_value.A2UiBoundValue] | None, NoExplicitNull] = None class Card(AdCPBaseModel): title: Annotated[bound_value.A2UiBoundValue, Field(description='Card title')] - subtitle: Annotated[bound_value.A2UiBoundValue | None, Field(description='Card subtitle')] = ( - None - ) - image: Annotated[bound_value.A2UiBoundValue | None, Field(description='Card image URL')] = None + subtitle: Annotated[ + bound_value.A2UiBoundValue | None, Field(description='Card subtitle'), NoExplicitNull + ] = None + image: Annotated[ + bound_value.A2UiBoundValue | None, Field(description='Card image URL'), NoExplicitNull + ] = None badge: Annotated[ - bound_value.A2UiBoundValue | None, Field(description="Badge text (e.g., 'New', 'Sale')") + bound_value.A2UiBoundValue | None, + Field(description="Badge text (e.g., 'New', 'Sale')"), + NoExplicitNull, + ] = None + action: Annotated[ + Action23 | None, Field(description='Action to trigger on card click'), NoExplicitNull + ] = None + children: Annotated[ + list[str] | None, Field(description='Child component IDs'), NoExplicitNull ] = None - action: Annotated[Action23 | None, Field(description='Action to trigger on card click')] = None - children: Annotated[list[str] | None, Field(description='Child component IDs')] = None class ProductCard(AdCPBaseModel): title: Annotated[bound_value.A2UiBoundValue, Field(description='Product name')] price: Annotated[bound_value.A2UiBoundValue, Field(description='Price display string')] - image: Annotated[bound_value.A2UiBoundValue | None, Field(description='Product image URL')] = ( - None - ) + image: Annotated[ + bound_value.A2UiBoundValue | None, Field(description='Product image URL'), NoExplicitNull + ] = None description: Annotated[ - bound_value.A2UiBoundValue | None, Field(description='Product description') + bound_value.A2UiBoundValue | None, Field(description='Product description'), NoExplicitNull ] = None badge: Annotated[ - bound_value.A2UiBoundValue | None, Field(description="Badge text (e.g., 'Best Seller')") + bound_value.A2UiBoundValue | None, + Field(description="Badge text (e.g., 'Best Seller')"), + NoExplicitNull, ] = None ctaLabel: Annotated[ - bound_value.A2UiBoundValue | None, Field(description='CTA button label') + bound_value.A2UiBoundValue | None, Field(description='CTA button label'), NoExplicitNull ] = None - action: Action23 | None = None + action: Annotated[Action23 | None, NoExplicitNull] = None class Template(AdCPBaseModel): @@ -132,9 +149,11 @@ class List(AdCPBaseModel): items: Annotated[bound_value.A2UiBoundValue, Field(description='Path to array in data model')] template: Annotated[Template, Field(description='Template for list items')] emptyMessage: Annotated[ - bound_value.A2UiBoundValue | None, Field(description='Message to show when list is empty') + bound_value.A2UiBoundValue | None, + Field(description='Message to show when list is empty'), + NoExplicitNull, ] = None - layout: Layout | None = Layout.vertical + layout: Annotated[Layout | None, NoExplicitNull] = Layout.vertical class Align(StrEnum): @@ -154,15 +173,15 @@ class Justify(StrEnum): class Row(AdCPBaseModel): children: Annotated[list[str], Field(description='Child component IDs')] - gap: Annotated[str | None, Field(description='Gap between children')] = '8px' - align: Align | None = Align.center - justify: Justify | None = Justify.start + gap: Annotated[str | None, Field(description='Gap between children'), NoExplicitNull] = '8px' + align: Annotated[Align | None, NoExplicitNull] = Align.center + justify: Annotated[Justify | None, NoExplicitNull] = Justify.start class Column(AdCPBaseModel): children: Annotated[list[str], Field(description='Child component IDs')] - gap: Annotated[str | None, Field(description='Gap between children')] = '8px' - align: Align | None = Align.stretch + gap: Annotated[str | None, Field(description='Gap between children'), NoExplicitNull] = '8px' + align: Annotated[Align | None, NoExplicitNull] = Align.stretch class Type(StrEnum): @@ -174,17 +193,19 @@ class IntegrationAction(AdCPBaseModel): type: Annotated[Type, Field(description='Integration type')] label: Annotated[bound_value.A2UiBoundValue, Field(description='Button label')] url: Annotated[ - bound_value.A2UiBoundValue | None, Field(description='Integration endpoint URL') + bound_value.A2UiBoundValue | None, + Field(description='Integration endpoint URL'), + NoExplicitNull, ] = None highlighted: Annotated[ - StrictBool | None, Field(description='Visually emphasize this action') + StrictBool | None, Field(description='Visually emphasize this action'), NoExplicitNull ] = False class Apps(AdCPBaseModel): - appId: str | None = None - deepLink: str | None = None - url: AnyUrl | None = None + appId: Annotated[str | None, NoExplicitNull] = None + deepLink: Annotated[str | None, NoExplicitNull] = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None class AppHandoff(AdCPBaseModel): diff --git a/src/adcp/types/domains/a2ui/surface.py b/src/adcp/types/domains/a2ui/surface.py index 2bac4e883..3d33108aa 100644 --- a/src/adcp/types/domains/a2ui/surface.py +++ b/src/adcp/types/domains/a2ui/surface.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: a2ui/surface.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import component @@ -18,7 +18,7 @@ class A2UiSurface(AdCPBaseModel): ) surfaceId: Annotated[str, Field(description='Unique identifier for this surface')] catalogId: Annotated[ - str | None, Field(description='Component catalog to use for rendering') + str | None, Field(description='Component catalog to use for rendering'), NoExplicitNull ] = 'standard' components: Annotated[ list[component.A2UiComponent], @@ -27,7 +27,10 @@ class A2UiSurface(AdCPBaseModel): rootId: Annotated[ str | None, Field(description='ID of the root component (if not specified, first component is root)'), + NoExplicitNull, ] = None dataModel: Annotated[ - dict[str, Any] | None, Field(description='Application data that components can bind to') + dict[str, Any] | None, + Field(description='Application data that components can bind to'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/a2ui/user_action.py b/src/adcp/types/domains/a2ui/user_action.py index 44c5cf991..00494a516 100644 --- a/src/adcp/types/domains/a2ui/user_action.py +++ b/src/adcp/types/domains/a2ui/user_action.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: a2ui/user_action.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field @@ -18,7 +18,9 @@ class Action(AdCPBaseModel): str, Field(description="Action identifier (e.g., 'view_product', 'add_to_cart')") ] context: Annotated[ - dict[str, Any] | None, Field(description='Context data resolved from data model bindings') + dict[str, Any] | None, + Field(description='Context data resolved from data model bindings'), + NoExplicitNull, ] = None @@ -29,4 +31,6 @@ class A2UiUserAction(AdCPBaseModel): surfaceId: Annotated[str, Field(description='ID of the surface where the action occurred')] componentId: Annotated[str, Field(description='ID of the component that triggered the action')] action: Annotated[Action, Field(description='The action that was triggered')] - timestamp: Annotated[AwareDatetime | None, Field(description='When the action occurred')] = None + timestamp: Annotated[ + AwareDatetime | None, Field(description='When the action occurred'), NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/aao/agent_publishers.py b/src/adcp/types/domains/aao/agent_publishers.py index da5d89b10..a3c6b9485 100644 --- a/src/adcp/types/domains/aao/agent_publishers.py +++ b/src/adcp/types/domains/aao/agent_publishers.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: aao/agent_publishers.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool @@ -66,12 +66,14 @@ class PublisherEntry(AdCPBaseModel): Field( description="Canonical list of `property_id`s under THIS `publisher_domain` that the agent's selectors resolve to. Present iff the request included `?include=properties`; absent otherwise. The set is the same population `properties_authorized` counts, surfaced as IDs so consumers can run full set-diff against a federated fetch (count-equality is not set-equality — a publisher rotating N properties leaves the count unchanged but the set entirely different). Per-publisher scope; never network-wide. Order is unspecified; consumers should treat as a set." ), + NoExplicitNull, ] = None signing_keys_pinned: Annotated[ StrictBool | None, Field( description="Whether the publisher's adagents.json entry for this agent pins `signing_keys[]`. When true, the agent's signed responses MUST verify against the pinned key set regardless of the agent's own JWKS. Operators should treat true as a signal that their published JWKS must match the publisher's pin." ), + NoExplicitNull, ] = None status: Annotated[ Status, diff --git a/src/adcp/types/domains/account/get_account_financials_request.py b/src/adcp/types/domains/account/get_account_financials_request.py index 772d2141a..891625ffa 100644 --- a/src/adcp/types/domains/account/get_account_financials_request.py +++ b/src/adcp/types/domains/account/get_account_financials_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/get_account_financials_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -30,6 +31,7 @@ class GetAccountFinancialsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Date range for the spend summary. Defaults to the current billing cycle if omitted.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/list_account_changes_request.py b/src/adcp/types/domains/account/list_account_changes_request.py index 127523131..983bf97e5 100644 --- a/src/adcp/types/domains/account/list_account_changes_request.py +++ b/src/adcp/types/domains/account/list_account_changes_request.py @@ -1,16 +1,16 @@ # generated by datamodel-codegen: # filename: account/list_account_changes_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types.base import AdcpRequest, SchemaInt -from adcp.types._scalar import ScalarStr - from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field from ..core import account_ref @@ -40,6 +40,7 @@ class ListAccountChangesRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -49,6 +50,7 @@ class ListAccountChangesRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference, @@ -63,12 +65,14 @@ class ListAccountChangesRequest(AdcpRequest, AdcpVersionEnvelope): max_length=4096, min_length=1, ), + NoExplicitNull, ] = None starting_position: Annotated[ StartingPosition | None, Field( description='Initial position when cursor is absent. Mutually exclusive with cursor; sellers enforce this semantic rule at runtime so the emitted MCP input schema can remain a plain root object. earliest intentionally begins at the oldest retained change. latest returns a checkpoint at the current seller-ingestion high-water and is used before a race-free snapshot bootstrap.' ), + NoExplicitNull, ] = StartingPosition.earliest resource_types: Annotated[ list[ResourceType] | None, @@ -77,6 +81,7 @@ class ListAccountChangesRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None max_results: Annotated[ SchemaInt | None, @@ -85,6 +90,7 @@ class ListAccountChangesRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=100, ), + NoExplicitNull, ] = 50 - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/list_account_changes_response.py b/src/adcp/types/domains/account/list_account_changes_response.py index e6f932a82..5024f54e0 100644 --- a/src/adcp/types/domains/account/list_account_changes_response.py +++ b/src/adcp/types/domains/account/list_account_changes_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/list_account_changes_response.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, model_validator @@ -60,12 +60,14 @@ class SourceCoverageItem(AdCPBaseModel): Field( description='Earliest upstream time covered by this source after connection or retention limits.' ), + NoExplicitNull, ] = None observed_through: Annotated[ AwareDatetime | None, Field(description='Latest upstream point the seller has successfully observed.'), + NoExplicitNull, ] = None - last_successful_sync_at: AwareDatetime | None = None + last_successful_sync_at: Annotated[AwareDatetime | None, NoExplicitNull] = None stale_after_seconds: Annotated[ SchemaInt | None, Field( @@ -73,6 +75,7 @@ class SourceCoverageItem(AdCPBaseModel): ge=1, le=2592000, ), + NoExplicitNull, ] = None resource_types: Annotated[list[ResourceType], Field(max_length=50)] @@ -92,6 +95,7 @@ class ListAccountChangesResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve description='Matching changes in oldest-first total account order. Timestamps are descriptive and do not define this order.', max_length=100, ), + NoExplicitNull, ] = None cursor: Annotated[ str | None, @@ -100,24 +104,28 @@ class ListAccountChangesResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve max_length=4096, min_length=1, ), + NoExplicitNull, ] = None has_more: Annotated[ StrictBool | None, Field( description='True when more retained matching changes were available at generation time. False means caught up to seller ingestion, not necessarily to an unavailable or delayed connected source.' ), + NoExplicitNull, ] = None available_since: Annotated[ AwareDatetime | None, Field( description='Oldest time for which the seller currently retains change records for this account and caller. The capability guarantees at least 90 days after adoption; pre-adoption history is not fabricated.' ), + NoExplicitNull, ] = None generated_at: Annotated[ AwareDatetime | None, Field( description='Seller time when this page and its source-coverage watermarks were generated.' ), + NoExplicitNull, ] = None source_coverage: Annotated[ list[SourceCoverageItem] | None, @@ -125,10 +133,11 @@ class ListAccountChangesResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve description='Account-specific feed and connector coverage. Buyers use this to distinguish feed catch-up from upstream freshness. Omission means no additional connected-source coverage is declared.', max_length=50, ), + NoExplicitNull, ] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None status: Status22 @model_validator(mode='after') diff --git a/src/adcp/types/domains/account/list_accounts_request.py b/src/adcp/types/domains/account/list_accounts_request.py index ab82c8a2f..5ad26c8aa 100644 --- a/src/adcp/types/domains/account/list_accounts_request.py +++ b/src/adcp/types/domains/account/list_accounts_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/list_accounts_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -36,23 +37,27 @@ class ListAccountsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Optional exact account filter. Use `account_id` to retrieve one known seller/storefront account, or the complete natural key (`brand` + `operator` + optional `operator_unit`, fixed `currency`, buyer-selected account `timezone`, and `sandbox`) for buyer-declared accounts. When present, the seller returns only matching accounts visible to the authenticated caller.' ), + NoExplicitNull, ] = None status: Annotated[ Status | None, Field(description='Filter accounts by status. Omit to return accounts in all statuses.'), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None sandbox: Annotated[ StrictBool | None, Field( description='Filter by sandbox status. true returns only sandbox accounts, false returns only production accounts. Omit to return all accounts. Primarily used with account-id namespaces where sandbox accounts are pre-existing test accounts on the platform.' ), + NoExplicitNull, ] = None include_webhook_activity: Annotated[ StrictBool | None, Field( description='When true, request recent webhook delivery attempts for each returned account in account.webhook_activity[]. Sellers MAY omit webhook_activity if they do not expose this debug log; when present, three-state semantics match the shared webhook_activity[] contract.' ), + NoExplicitNull, ] = False webhook_activity_limit: Annotated[ SchemaInt | None, @@ -61,6 +66,7 @@ class ListAccountsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=200, ), + NoExplicitNull, ] = 50 - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/list_accounts_response.py b/src/adcp/types/domains/account/list_accounts_response.py index f0194d25e..1898146a8 100644 --- a/src/adcp/types/domains/account/list_accounts_response.py +++ b/src/adcp/types/domains/account/list_accounts_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/list_accounts_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_with_authorization @@ -30,8 +31,10 @@ class ListAccountsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): ), ] errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific errors and warnings') + list[error.Error] | None, + Field(description='Task-specific errors and warnings'), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/report_usage_request.py b/src/adcp/types/domains/account/report_usage_request.py index cb6fa0e5c..4d4569273 100644 --- a/src/adcp/types/domains/account/report_usage_request.py +++ b/src/adcp/types/domains/account/report_usage_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/report_usage_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import account_ref @@ -30,6 +30,7 @@ class UsageItem(AdCPBaseModel): Field( description='Seller-assigned media buy identifier. Links this usage record to a specific media buy.' ), + NoExplicitNull, ] = None vendor_cost: Annotated[ StrictFloat, @@ -44,10 +45,12 @@ class UsageItem(AdCPBaseModel): Field( description="Pricing option identifier from the vendor's discovery response (e.g., get_signals, list_content_standards). The vendor uses this to verify the correct rate was applied." ), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt | None, Field(description='Impressions delivered using this vendor service.', ge=0), + NoExplicitNull, ] = None media_spend: Annotated[ StrictFloat | None, @@ -55,6 +58,7 @@ class UsageItem(AdCPBaseModel): description='Media spend in currency for the period. Required when a percent_of_media pricing model was used, so the vendor can verify the applied rate.', ge=0.0, ), + NoExplicitNull, ] = None conversions: Annotated[ StrictFloat | None, @@ -62,6 +66,7 @@ class UsageItem(AdCPBaseModel): description='Number of attributed conversion events for the reporting period. Optional analytics context for revenue_share reconciliation.', ge=0.0, ), + NoExplicitNull, ] = None conversion_value: Annotated[ StrictFloat | None, @@ -69,6 +74,7 @@ class UsageItem(AdCPBaseModel): description='Total monetary value of attributed conversions for the reporting period, in currency. Optional analytics context for revenue_share reconciliation; it is not the billing basis.', ge=0.0, ), + NoExplicitNull, ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -76,52 +82,61 @@ class UsageItem(AdCPBaseModel): description='Settled attributed value eligible for commission, in currency. Required when pricing_option_id selects a revenue_share option. The receiver verifies vendor_cost = round_currency(commissionable_value × the selected commission_rate).', ge=0.0, ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str | None, Field(description='Signal identifier from get_signals. Required for signals agents.'), + NoExplicitNull, ] = None standards_id: Annotated[ str | None, Field( description='Content standards configuration identifier. Required for governance agents.' ), + NoExplicitNull, ] = None rights_id: Annotated[ str | None, Field( description='Rights grant identifier from acquire_rights. Required for brand/rights agents. Links usage records to specific rights grants for cap tracking, billing verification, and overage calculation.' ), + NoExplicitNull, ] = None creative_id: Annotated[ str | None, Field( description='Creative identifier from build_creative or list_creatives. Required for creative agents. Links usage records to specific creatives for billing verification.' ), + NoExplicitNull, ] = None build_variant_id: Annotated[ str | None, Field( description='Optional. When the reported creative_id was promoted from a specific build_creative variant leaf but the creative_id differs from the source build_variant_id, carry that source build_variant_id so billing reconciliation can link this usage record back to the exact produced leaf for audit (pricing_option_id alone is not unique across leaves). On the canonical path where creative_id is the build_variant_id, omit this field and use creative_id as the join key. Omit for creatives with no build-variant lineage.' ), + NoExplicitNull, ] = None property_list_id: Annotated[ str | None, Field( description='Property list identifier from list_property_lists. Required for property list agents. Links usage records to specific property lists for billing verification.' ), + NoExplicitNull, ] = None final: Annotated[ StrictBool | None, Field( description="Whether this usage record represents the reporter's final, billing-authoritative numbers for the reporting period. **Absent means unknown** — the reporter has not declared finality on this record. Set `true` only when the reporter has actually settled the numbers (e.g., 3PAS month-end close after SIVT scrubbing, conversion dedup, and view-through windows have closed; vendor file post-C7 for broadcast). Set `false` when pushing preliminary measurements (daily pacing pushes, intra-period progress) that are still settling. Receivers MUST NOT invoice on `final: false` records, and MUST NOT invoice on records where `final` is absent for buys whose `measurement_terms.billing_measurement` names this reporter as authoritative — request a final record first. Receivers MAY invoice on absent for buys with no `measurement_terms.billing_measurement` (3.0-style usage where the receiver treats reports as authoritative on receipt) and for non-media-buy variants (signals, governance, creative, brand — domains with no provisional state concept). When the same `(account, media_buy_id, reporting_period)` is later reported with `final: true`, that record supersedes any prior records for the period." ), + NoExplicitNull, ] = None finalized_at: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp at which the reporter considered these numbers final. Present only when `final: true`. Anchors any deadline declared in the buy's `measurement_terms.billing_measurement.finalization_deadline_hours`." ), + NoExplicitNull, ] = None measurement_window: Annotated[ str | None, @@ -130,6 +145,7 @@ class UsageItem(AdCPBaseModel): examples=['live', 'c3', 'c7', 'post_ivt', 'post_sivt', 'downloads_30d'], max_length=50, ), + NoExplicitNull, ] = None @@ -156,5 +172,5 @@ class ReportUsageRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/report_usage_response.py b/src/adcp/types/domains/account/report_usage_response.py index 97da5b656..cb73d9716 100644 --- a/src/adcp/types/domains/account/report_usage_response.py +++ b/src/adcp/types/domains/account/report_usage_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/report_usage_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -29,10 +30,12 @@ class ReportUsageResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): Field( description="Validation errors for individual records. The field property identifies which record failed (e.g., 'usage[1].pricing_option_id')." ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, the account is a sandbox account and no billing occurred.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/sync_accounts_request.py b/src/adcp/types/domains/account/sync_accounts_request.py index 4e077a0b5..e8d7ef4fb 100644 --- a/src/adcp/types/domains/account/sync_accounts_request.py +++ b/src/adcp/types/domains/account/sync_accounts_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/sync_accounts_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref, brand_ref, business_entity @@ -33,6 +33,7 @@ class Accounts(AdCPBaseModel): Field( description='Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`.' ), + NoExplicitNull, ] = None revision: Annotated[ SchemaInt | None, @@ -40,18 +41,21 @@ class Accounts(AdCPBaseModel): description='Expected current account revision for optimistic concurrency in settings-update mode. Required whenever operator_identity is present; optional for existing non-identity settings updates. The seller MUST compare it atomically with the write, reject a mismatch with CONFLICT, and leave the account unchanged. Obtain it from list_accounts or the most recent sync_accounts result. Reads, dry runs, validation failures, and exact idempotency replays do not increment revision; every persisted settings or identity-change state transition does. MUST be absent in provisioning mode.', ge=1, ), + NoExplicitNull, ] = None operator_identity: Annotated[ operator_identity_1.OperatorIdentity | None, Field( description='Complete desired operator identity for settings-update mode. Omit this field to leave operator identity unchanged. When present, omission of operator_unit within the object removes the existing unit. Changing only operator_unit_1.name updates display metadata; changing operator_unit_1.id or adding/removing a unit rekeys the same account within the current operator. Changing operator requests an inter-entity handoff and MUST enter pending_approval until the seller verifies the current account authority, verified brand authorization, destination-operator acceptance, and any operator-scoped billing and grant transition. The seller MUST preserve account_id and account-scoped historical resources, MUST reject collisions without merging, and MUST apply no identity change if continuity cannot be preserved. MUST be accompanied by revision and MUST be absent in provisioning mode.' ), + NoExplicitNull, ] = None destination_billing_entity: Annotated[ business_entity.BusinessEntity | None, Field( description="Complete staged billing identity for the requested destination operator during an operator-domain handoff on an account whose billing party is operator. This value is write-only while approval is pending and MUST NOT replace or be echoed as the account's canonical billing_entity until the handoff applies atomically. Required by the protocol when an operator-billed account changes operator; otherwise MUST be absent. Requires operator_identity and revision and MUST be absent in provisioning mode." ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference, @@ -71,6 +75,7 @@ class Accounts(AdCPBaseModel): Field( description='Optional operator-owned business unit, agency seat, or platform account for provisioning mode. operator_unit_1.id participates in the natural key; name is mapping/display metadata. MUST be absent in settings-update mode.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -78,6 +83,7 @@ class Accounts(AdCPBaseModel): description='Optional immutable ISO 4217 transaction currency for a currency-bound advertiser object. Consult `account.supported_account_currency_modes` before provisioning. When supplied, it participates in the natural key and all media buys on the account use it. Omit for per-media-buy currency selection. MUST be absent in settings-update mode.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, @@ -85,6 +91,7 @@ class Accounts(AdCPBaseModel): description='Immutable operational timezone selected for an account_fixed advertiser object. Required in provisioning mode when get_adcp_capabilities.account.timezone declares account_selection: buyer_selected, and the value MUST be one of supported_timezones. Omit for seller_fixed or seller_assigned modes. When supplied, it participates in the natural key. MUST be absent in settings-update mode.', min_length=1, ), + NoExplicitNull, ] = None billing: Annotated[ billing_party.BillingParty, @@ -97,24 +104,28 @@ class Accounts(AdCPBaseModel): Field( description='Business entity details for the party responsible for payment. The agent provides this so the seller has the legal name, tax IDs, address, and bank details needed for formal B2B invoicing. Permitted in both modes — sellers MAY accept refinements in settings-update mode (e.g., updated bank details).' ), + NoExplicitNull, ] = None payment_terms: Annotated[ payment_terms_1.PaymentTerms | None, Field( description='Payment terms for this account. The seller must either accept these terms or reject the account — terms are never silently remapped. When omitted, the seller applies its default terms. Permitted in both modes.' ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='When true, provision this as a sandbox account with no real platform calls or billing. Only applicable to buyer-declared accounts (require_operator_auth: false) in provisioning mode. For account-id namespaces, sandbox accounts are pre-existing test accounts discovered via list_accounts or supplied out-of-band.' ), + NoExplicitNull, ] = None preferred_reporting_protocol: Annotated[ cloud_storage_protocol.CloudStorageProtocol | None, Field( description="Buyer's preferred cloud storage protocol for offline reporting delivery. The seller provisions the account's reporting_bucket using this protocol if supported. When omitted, the seller chooses from its supported offline_delivery_protocols. Only meaningful when the seller's reporting_delivery_methods includes 'offline'." ), + NoExplicitNull, ] = None reporting_delivery_configs: Annotated[ list[reporting_delivery_config.ReportingDeliveryConfiguration] | None, @@ -122,6 +133,7 @@ class Accounts(AdCPBaseModel): description="Caller-owned desired state for durable reporting delivery on this account. Declarative replacement is scoped to (authenticated caller, resolved account): omission leaves that caller's set unchanged; [] deactivates that caller's set and starts grant revocation; another caller's entries MUST NOT be read, replaced, or deleted. Entries are keyed by immutable (delivery_config_id, delivery_config_version); duplicate tuples MUST reject the entire account entry, and reusing a tuple with changed content MUST be rejected. Each generation binds the exact report_definition_id advertised by its offering. destination.mode provision asks the seller to verify caller disclosure authority and destination/recipient control from non-secret provider coordinates; destination.mode existing reuses a caller-scoped immutable destination-generation reference, including one registered through sync_agent_configuration. The account configuration independently authorizes disclosure for this feed and scope, so possession of a reusable reference is never account authority. Unknown, unauthorized, and cross-caller refs MUST be indistinguishable. Credentials never transit AdCP, including nested extension fields. Permitted in both provisioning and settings-update modes. Sellers accepting this field MUST advertise media_buy.reporting_delivery in experimental_features and echo resolved secret-free state on sync_accounts and list_accounts.", max_length=16, ), + NoExplicitNull, ] = None notification_configs: Annotated[ list[notification_config.NotificationConfig] | None, @@ -129,6 +141,7 @@ class Accounts(AdCPBaseModel): description='Account-level webhook subscriptions for notifications whose lifecycle outlives any single media buy (`creative.status_changed`, optional `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, wholesale feed change payloads, and future account-anchored resource events after those event types are added to `notification-config.json`). Indicator and assignment registrations are prospective: activation does not replay current conditions, so buyers establish a complete baseline through `get_media_buys` by enumerating known IDs or requesting every status and exhausting pagination, without an indicator filter. Durable account lifecycle transitions such as later `payment_required`, `suspended`, `closed`, or recovery to `active` use `account.status_changed` on this surface; the one-shot `sync_accounts.push_notification_config` channel remains scoped to the async result of the original provisioning task. Declarative replace semantics: when this field is present, the buyer sends the full desired array and the seller replaces the account\'s current set with that array, keyed by account-scoped `subscriber_id`. Omit this field to leave existing subscribers unchanged; send `[]` to remove all subscribers. Re-sending an existing `subscriber_id` for the account replaces that subscriber\'s config rather than creating a duplicate; persisted entries whose `subscriber_id` does not appear in the sent array are removed, so the seller MUST NOT merge the new array with persisted state. Paused entries (`active: false`) use the same replacement semantics; a buyer that wants to preserve a paused subscriber MUST re-include it with `active: false`. Duplicate `subscriber_id` values within one submitted array are rejected. Permitted in both provisioning and settings-update modes. Each entry registers a URL, the event types the subscriber wants, and optional legacy auth — see [`notification-config.json`](/schemas/core/notification-config.json). The seller MUST echo applied state on the response and on `list_accounts` reads, with `authentication.credentials` omitted (write-only). Sellers MUST reject entries whose `event_types` include any type whose contract anchors at a media buy or below (today: `scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or at the agent (today: `capabilities.changed`) as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry — those events do not belong on this surface. Wholesale feed webhook registrations carry the actual change payload in `/schemas/core/wholesale-feed-webhook.json`; canonical product subscribers repair through `list_products(if_feed_version)`, legacy product subscribers through `get_products(if_wholesale_feed_version)`, and signal subscribers through `get_signals(if_wholesale_feed_version)`. Account status change registrations carry the invalidation payload in `/schemas/core/account-status-changed-webhook.json`; receivers use `list_accounts` to repair or reconcile. This is distinct from sync_catalogs, which manages buyer-provided campaign input feeds on a seller account.\n\nActivation proof: before activating a new or changed active subscriber, the seller MUST validate the URL, complete the account-level webhook proof-of-control challenge, and only then persist or expose the subscriber as `active: true`. For `account.status_changed`, sellers MUST assign `account_id` before completing proof so subsequent status transitions can identify the account and be repaired through `list_accounts`, even when external approval remains pending. A valid existing proof for the same `(account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types)` tuple MAY be reused; changing any element of that tuple requires fresh proof. The challenge POST itself MUST be signed with the seller\'s RFC 9421 webhook profile key and MUST include seller_agent_url, delivery_auth, and event_types so the receiver can verify the pending registration before echoing the challenge. New signers use `adcp_use: "request-signing"`; deprecated `webhook-signing` keys remain accepted during the compatibility window. Entries sent with `active: false` may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time, and those entries MUST NOT receive fires until reactivated. If proof fails or times out, the seller rejects the account entry with `action: "failed"`, leaves the prior notification_configs[] set unchanged, and reports `VALIDATION_ERROR` (or `INVALID_REQUEST` for malformed URLs) at the failing `notification_configs[j].url` field.\n\n**Cap rationale:** `maxItems: 16` is a practical fan-out cap (governance + buyer ingestion + audit bus + dx team + a few partner hooks). The cap exists to prevent unbounded subscriber arrays in storage and to bound the seller\'s per-event fan-out work. Sellers that hit the cap with legitimate subscribers should surface this on the protocol roadmap rather than work around it.', max_length=16, ), + NoExplicitNull, ] = None @@ -148,24 +161,28 @@ class Accounts1(AdCPBaseModel): description='Expected current account revision for optimistic concurrency in settings-update mode. Required whenever operator_identity is present; optional for existing non-identity settings updates. The seller MUST compare it atomically with the write, reject a mismatch with CONFLICT, and leave the account unchanged. Obtain it from list_accounts or the most recent sync_accounts result. Reads, dry runs, validation failures, and exact idempotency replays do not increment revision; every persisted settings or identity-change state transition does. MUST be absent in provisioning mode.', ge=1, ), + NoExplicitNull, ] = None operator_identity: Annotated[ operator_identity_1.OperatorIdentity | None, Field( description='Complete desired operator identity for settings-update mode. Omit this field to leave operator identity unchanged. When present, omission of operator_unit within the object removes the existing unit. Changing only operator_unit_1.name updates display metadata; changing operator_unit_1.id or adding/removing a unit rekeys the same account within the current operator. Changing operator requests an inter-entity handoff and MUST enter pending_approval until the seller verifies the current account authority, verified brand authorization, destination-operator acceptance, and any operator-scoped billing and grant transition. The seller MUST preserve account_id and account-scoped historical resources, MUST reject collisions without merging, and MUST apply no identity change if continuity cannot be preserved. MUST be accompanied by revision and MUST be absent in provisioning mode.' ), + NoExplicitNull, ] = None destination_billing_entity: Annotated[ business_entity.BusinessEntity | None, Field( description="Complete staged billing identity for the requested destination operator during an operator-domain handoff on an account whose billing party is operator. This value is write-only while approval is pending and MUST NOT replace or be echoed as the account's canonical billing_entity until the handoff applies atomically. Required by the protocol when an operator-billed account changes operator; otherwise MUST be absent. Requires operator_identity and revision and MUST be absent in provisioning mode." ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference identifying the advertiser. Required for **provisioning mode**; MUST be absent in settings-update mode. Only the BrandKey projection — `domain`, `brand_id`, and the canonicalized `countries[]` set — participates in account identity. Mutable or per-call BrandRef fields such as `industries`, `data_subject_contestation`, and `brand_kit_override` MUST NOT affect lookup, idempotency, or account creation. New 3.2 producers SHOULD send only the BrandKey fields; the broader BrandRef remains accepted on this existing 3.x task for compatibility.' ), + NoExplicitNull, ] = None operator: Annotated[ str | None, @@ -173,12 +190,14 @@ class Accounts1(AdCPBaseModel): description="Domain of the entity operating on the brand's behalf (e.g., 'pinnacle-media.com'). When the brand operates directly, this is the brand's domain. Verified against the brand's authorized_operators in brand.json. Required for **provisioning mode**; MUST be absent in settings-update mode.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None operator_unit: Annotated[ operator_unit_1.OperatorUnit | None, Field( description='Optional operator-owned business unit, agency seat, or platform account for provisioning mode. operator_unit_1.id participates in the natural key; name is mapping/display metadata. MUST be absent in settings-update mode.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -186,6 +205,7 @@ class Accounts1(AdCPBaseModel): description='Optional immutable ISO 4217 transaction currency for a currency-bound advertiser object. Consult `account.supported_account_currency_modes` before provisioning. When supplied, it participates in the natural key and all media buys on the account use it. Omit for per-media-buy currency selection. MUST be absent in settings-update mode.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, @@ -193,36 +213,42 @@ class Accounts1(AdCPBaseModel): description='Immutable operational timezone selected for an account_fixed advertiser object. Required in provisioning mode when get_adcp_capabilities.account.timezone declares account_selection: buyer_selected, and the value MUST be one of supported_timezones. Omit for seller_fixed or seller_assigned modes. When supplied, it participates in the natural key. MUST be absent in settings-update mode.', min_length=1, ), + NoExplicitNull, ] = None billing: Annotated[ billing_party.BillingParty | None, Field( description='Who the seller invoices for this buyer–storefront account relationship. Required for **provisioning mode**; MUST be absent in settings-update mode (the invoiced party is fixed at provisioning time and cannot be changed via settings-update). This field does not select a payment rail, clearing intermediary, or per-media-buy settlement route.' ), + NoExplicitNull, ] = None billing_entity: Annotated[ business_entity.BusinessEntity | None, Field( description='Business entity details for the party responsible for payment. The agent provides this so the seller has the legal name, tax IDs, address, and bank details needed for formal B2B invoicing. Permitted in both modes — sellers MAY accept refinements in settings-update mode (e.g., updated bank details).' ), + NoExplicitNull, ] = None payment_terms: Annotated[ payment_terms_1.PaymentTerms | None, Field( description='Payment terms for this account. The seller must either accept these terms or reject the account — terms are never silently remapped. When omitted, the seller applies its default terms. Permitted in both modes.' ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='When true, provision this as a sandbox account with no real platform calls or billing. Only applicable to buyer-declared accounts (require_operator_auth: false) in provisioning mode. For account-id namespaces, sandbox accounts are pre-existing test accounts discovered via list_accounts or supplied out-of-band.' ), + NoExplicitNull, ] = None preferred_reporting_protocol: Annotated[ cloud_storage_protocol.CloudStorageProtocol | None, Field( description="Buyer's preferred cloud storage protocol for offline reporting delivery. The seller provisions the account's reporting_bucket using this protocol if supported. When omitted, the seller chooses from its supported offline_delivery_protocols. Only meaningful when the seller's reporting_delivery_methods includes 'offline'." ), + NoExplicitNull, ] = None reporting_delivery_configs: Annotated[ list[reporting_delivery_config.ReportingDeliveryConfiguration] | None, @@ -230,6 +256,7 @@ class Accounts1(AdCPBaseModel): description="Caller-owned desired state for durable reporting delivery on this account. Declarative replacement is scoped to (authenticated caller, resolved account): omission leaves that caller's set unchanged; [] deactivates that caller's set and starts grant revocation; another caller's entries MUST NOT be read, replaced, or deleted. Entries are keyed by immutable (delivery_config_id, delivery_config_version); duplicate tuples MUST reject the entire account entry, and reusing a tuple with changed content MUST be rejected. Each generation binds the exact report_definition_id advertised by its offering. destination.mode provision asks the seller to verify caller disclosure authority and destination/recipient control from non-secret provider coordinates; destination.mode existing reuses a caller-scoped immutable destination-generation reference, including one registered through sync_agent_configuration. The account configuration independently authorizes disclosure for this feed and scope, so possession of a reusable reference is never account authority. Unknown, unauthorized, and cross-caller refs MUST be indistinguishable. Credentials never transit AdCP, including nested extension fields. Permitted in both provisioning and settings-update modes. Sellers accepting this field MUST advertise media_buy.reporting_delivery in experimental_features and echo resolved secret-free state on sync_accounts and list_accounts.", max_length=16, ), + NoExplicitNull, ] = None notification_configs: Annotated[ list[notification_config.NotificationConfig] | None, @@ -237,6 +264,7 @@ class Accounts1(AdCPBaseModel): description='Account-level webhook subscriptions for notifications whose lifecycle outlives any single media buy (`creative.status_changed`, optional `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, wholesale feed change payloads, and future account-anchored resource events after those event types are added to `notification-config.json`). Indicator and assignment registrations are prospective: activation does not replay current conditions, so buyers establish a complete baseline through `get_media_buys` by enumerating known IDs or requesting every status and exhausting pagination, without an indicator filter. Durable account lifecycle transitions such as later `payment_required`, `suspended`, `closed`, or recovery to `active` use `account.status_changed` on this surface; the one-shot `sync_accounts.push_notification_config` channel remains scoped to the async result of the original provisioning task. Declarative replace semantics: when this field is present, the buyer sends the full desired array and the seller replaces the account\'s current set with that array, keyed by account-scoped `subscriber_id`. Omit this field to leave existing subscribers unchanged; send `[]` to remove all subscribers. Re-sending an existing `subscriber_id` for the account replaces that subscriber\'s config rather than creating a duplicate; persisted entries whose `subscriber_id` does not appear in the sent array are removed, so the seller MUST NOT merge the new array with persisted state. Paused entries (`active: false`) use the same replacement semantics; a buyer that wants to preserve a paused subscriber MUST re-include it with `active: false`. Duplicate `subscriber_id` values within one submitted array are rejected. Permitted in both provisioning and settings-update modes. Each entry registers a URL, the event types the subscriber wants, and optional legacy auth — see [`notification-config.json`](/schemas/core/notification-config.json). The seller MUST echo applied state on the response and on `list_accounts` reads, with `authentication.credentials` omitted (write-only). Sellers MUST reject entries whose `event_types` include any type whose contract anchors at a media buy or below (today: `scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or at the agent (today: `capabilities.changed`) as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry — those events do not belong on this surface. Wholesale feed webhook registrations carry the actual change payload in `/schemas/core/wholesale-feed-webhook.json`; canonical product subscribers repair through `list_products(if_feed_version)`, legacy product subscribers through `get_products(if_wholesale_feed_version)`, and signal subscribers through `get_signals(if_wholesale_feed_version)`. Account status change registrations carry the invalidation payload in `/schemas/core/account-status-changed-webhook.json`; receivers use `list_accounts` to repair or reconcile. This is distinct from sync_catalogs, which manages buyer-provided campaign input feeds on a seller account.\n\nActivation proof: before activating a new or changed active subscriber, the seller MUST validate the URL, complete the account-level webhook proof-of-control challenge, and only then persist or expose the subscriber as `active: true`. For `account.status_changed`, sellers MUST assign `account_id` before completing proof so subsequent status transitions can identify the account and be repaired through `list_accounts`, even when external approval remains pending. A valid existing proof for the same `(account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types)` tuple MAY be reused; changing any element of that tuple requires fresh proof. The challenge POST itself MUST be signed with the seller\'s RFC 9421 webhook profile key and MUST include seller_agent_url, delivery_auth, and event_types so the receiver can verify the pending registration before echoing the challenge. New signers use `adcp_use: "request-signing"`; deprecated `webhook-signing` keys remain accepted during the compatibility window. Entries sent with `active: false` may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time, and those entries MUST NOT receive fires until reactivated. If proof fails or times out, the seller rejects the account entry with `action: "failed"`, leaves the prior notification_configs[] set unchanged, and reports `VALIDATION_ERROR` (or `INVALID_REQUEST` for malformed URLs) at the failing `notification_configs[j].url` field.\n\n**Cap rationale:** `maxItems: 16` is a practical fan-out cap (governance + buyer ingestion + audit bus + dx team + a few partner hooks). The cap exists to prevent unbounded subscriber arrays in storage and to bound the seller\'s per-event fan-out work. Sellers that hit the cap with legitimate subscribers should surface this on the protocol roadmap rather than work around it.', max_length=16, ), + NoExplicitNull, ] = None @@ -265,18 +293,21 @@ class SyncAccountsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='When true, accounts previously synced by this agent but not included in this request will be deactivated. Scoped to the authenticated agent — does not affect accounts managed by other agents. Use with caution.' ), + NoExplicitNull, ] = False dry_run: Annotated[ StrictBool | None, Field( description='When true, preview what would change without applying. Returns what would be created/updated/deactivated.' ), + NoExplicitNull, ] = False push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Webhook for async notifications when account status changes (e.g., pending_approval transitions to active).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/sync_governance_request.py b/src/adcp/types/domains/account/sync_governance_request.py index fc4eb9da3..06909f781 100644 --- a/src/adcp/types/domains/account/sync_governance_request.py +++ b/src/adcp/types/domains/account/sync_governance_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/sync_governance_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import account_ref @@ -86,5 +86,5 @@ class SyncGovernanceRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/account/sync_governance_response.py b/src/adcp/types/domains/account/sync_governance_response.py index 01c891b6a..fd533d4bd 100644 --- a/src/adcp/types/domains/account/sync_governance_response.py +++ b/src/adcp/types/domains/account/sync_governance_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/sync_governance_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import account_ref @@ -56,8 +56,8 @@ class SyncGovernanceResponse2(SyncGovernanceResponse): min_length=1, ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Account(AdCPBaseModel): @@ -80,12 +80,14 @@ class Account(AdCPBaseModel): max_length=1, min_length=1, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description="Per-account errors (only present when status is 'failed')", min_length=1 ), + NoExplicitNull, ] = None @@ -94,5 +96,5 @@ class SyncGovernanceResponse1(SyncGovernanceResponse): extra='allow', ) accounts: Annotated[list[Account], Field(description='Per-account sync results')] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/adagents.py b/src/adcp/types/domains/adagents.py index e2a9f0307..4bd2d200b 100644 --- a/src/adcp/types/domains/adagents.py +++ b/src/adcp/types/domains/adagents.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: adagents.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, RootModel, StrictBool, model_validator @@ -32,6 +32,7 @@ class AdcpAgentsAuthorization1(AdCPBaseModel): field_schema: Annotated[ str | None, Field(alias='$schema', description='JSON Schema identifier for this adagents.json file'), + NoExplicitNull, ] = None authoritative_location: Annotated[ AnyUrl, @@ -42,6 +43,7 @@ class AdcpAgentsAuthorization1(AdCPBaseModel): last_updated: Annotated[ AwareDatetime | None, Field(description='ISO 8601 timestamp indicating when this reference was last updated'), + NoExplicitNull, ] = None @@ -88,6 +90,7 @@ class Contact(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None domain: Annotated[ str | None, @@ -95,6 +98,7 @@ class Contact(AdCPBaseModel): description='Primary domain of the entity managing this file', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None seller_id: Annotated[ str | None, @@ -103,6 +107,7 @@ class Contact(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None tag_id: Annotated[ str | None, @@ -111,12 +116,14 @@ class Contact(AdCPBaseModel): max_length=100, min_length=1, ), + NoExplicitNull, ] = None privacy_policy_url: Annotated[ AnyUrl | None, Field( description="URL to the entity's privacy policy. Used for consumer consent flows when interacting with this sales agent." ), + NoExplicitNull, ] = None @@ -149,6 +156,7 @@ class RevokedPublisherDomain(AdCPBaseModel): Field( description='Reason for revocation. **Operator-internal self-classification for review routing — not a public accusation.** `relationship_ended` is the routine commercial case. `compliance_violation` SHOULD be used only when the network has itself determined the publisher is out of policy; for un-adjudicated third-party allegations (regulator inquiries, advertiser complaints, ongoing investigations), use `other` to avoid making a discoverable adverse statement. `publisher_request` is for publisher-initiated exits. Compare to sellers.json, which deliberately carries no reason field for the same exposure concern.' ), + NoExplicitNull, ] = None @@ -215,7 +223,9 @@ class PropertyFeature(AdCPBaseModel): ), ] publisher_id: Annotated[ - str | None, Field(description='Optional publisher identifier at this agent (for lookup)') + str | None, + Field(description='Optional publisher identifier at this agent (for lookup)'), + NoExplicitNull, ] = None @@ -262,6 +272,7 @@ class AuthorizedAgents1(AuthorizedAgentBaseFields): description="Optional collection constraints. When present, authorization only applies to inventory associated with these collections. A selector without collection_ids grants for all collections declared in that selector's publisher_domain adagents.json (the bulk-grant form for owner-sold carriage).", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -269,6 +280,7 @@ class AuthorizedAgents1(AuthorizedAgentBaseFields): description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.', min_length=1, ), + NoExplicitNull, ] = None placement_tags: Annotated[ list[str] | None, @@ -276,18 +288,21 @@ class AuthorizedAgents1(AuthorizedAgentBaseFields): description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.', min_length=1, ), + NoExplicitNull, ] = None delegation_type: Annotated[ DelegationType | None, Field( description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint." ), + NoExplicitNull, ] = None exclusive: Annotated[ StrictBool | None, Field( description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory." ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -295,13 +310,17 @@ class AuthorizedAgents1(AuthorizedAgentBaseFields): description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.', min_length=1, ), + NoExplicitNull, ] = None effective_from: Annotated[ AwareDatetime | None, Field(description='Optional start time for this authorization window.'), + NoExplicitNull, ] = None effective_until: Annotated[ - AwareDatetime | None, Field(description='Optional end time for this authorization window.') + AwareDatetime | None, + Field(description='Optional end time for this authorization window.'), + NoExplicitNull, ] = None @@ -326,6 +345,7 @@ class AuthorizedAgents2(AuthorizedAgentBaseFields): description="Optional collection constraints. When present, authorization only applies to inventory associated with these collections. A selector without collection_ids grants for all collections declared in that selector's publisher_domain adagents.json (the bulk-grant form for owner-sold carriage).", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -333,6 +353,7 @@ class AuthorizedAgents2(AuthorizedAgentBaseFields): description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.', min_length=1, ), + NoExplicitNull, ] = None placement_tags: Annotated[ list[str] | None, @@ -340,18 +361,21 @@ class AuthorizedAgents2(AuthorizedAgentBaseFields): description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.', min_length=1, ), + NoExplicitNull, ] = None delegation_type: Annotated[ DelegationType | None, Field( description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint." ), + NoExplicitNull, ] = None exclusive: Annotated[ StrictBool | None, Field( description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory." ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -359,13 +383,17 @@ class AuthorizedAgents2(AuthorizedAgentBaseFields): description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.', min_length=1, ), + NoExplicitNull, ] = None effective_from: Annotated[ AwareDatetime | None, Field(description='Optional start time for this authorization window.'), + NoExplicitNull, ] = None effective_until: Annotated[ - AwareDatetime | None, Field(description='Optional end time for this authorization window.') + AwareDatetime | None, + Field(description='Optional end time for this authorization window.'), + NoExplicitNull, ] = None @@ -392,6 +420,7 @@ class AuthorizedAgents4(AuthorizedAgentBaseFields): description="Optional collection constraints. When present, authorization only applies to inventory associated with these collections. A selector without collection_ids grants for all collections declared in that selector's publisher_domain adagents.json (the bulk-grant form for owner-sold carriage).", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -399,6 +428,7 @@ class AuthorizedAgents4(AuthorizedAgentBaseFields): description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.', min_length=1, ), + NoExplicitNull, ] = None placement_tags: Annotated[ list[str] | None, @@ -406,18 +436,21 @@ class AuthorizedAgents4(AuthorizedAgentBaseFields): description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.', min_length=1, ), + NoExplicitNull, ] = None delegation_type: Annotated[ DelegationType | None, Field( description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint." ), + NoExplicitNull, ] = None exclusive: Annotated[ StrictBool | None, Field( description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory." ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -425,13 +458,17 @@ class AuthorizedAgents4(AuthorizedAgentBaseFields): description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.', min_length=1, ), + NoExplicitNull, ] = None effective_from: Annotated[ AwareDatetime | None, Field(description='Optional start time for this authorization window.'), + NoExplicitNull, ] = None effective_until: Annotated[ - AwareDatetime | None, Field(description='Optional end time for this authorization window.') + AwareDatetime | None, + Field(description='Optional end time for this authorization window.'), + NoExplicitNull, ] = None @@ -612,6 +649,7 @@ class AuthorizedAgents3(AuthorizedAgentBaseFields): description="Optional collection constraints. When present, authorization only applies to inventory associated with these collections. A selector without collection_ids grants for all collections declared in that selector's publisher_domain adagents.json (the bulk-grant form for owner-sold carriage).", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -619,6 +657,7 @@ class AuthorizedAgents3(AuthorizedAgentBaseFields): description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.', min_length=1, ), + NoExplicitNull, ] = None placement_tags: Annotated[ list[str] | None, @@ -626,18 +665,21 @@ class AuthorizedAgents3(AuthorizedAgentBaseFields): description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.', min_length=1, ), + NoExplicitNull, ] = None delegation_type: Annotated[ DelegationType | None, Field( description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint." ), + NoExplicitNull, ] = None exclusive: Annotated[ StrictBool | None, Field( description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory." ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -645,13 +687,17 @@ class AuthorizedAgents3(AuthorizedAgentBaseFields): description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.', min_length=1, ), + NoExplicitNull, ] = None effective_from: Annotated[ AwareDatetime | None, Field(description='Optional start time for this authorization window.'), + NoExplicitNull, ] = None effective_until: Annotated[ - AwareDatetime | None, Field(description='Optional end time for this authorization window.') + AwareDatetime | None, + Field(description='Optional end time for this authorization window.'), + NoExplicitNull, ] = None @@ -763,12 +809,14 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): field_schema: Annotated[ str | None, Field(alias='$schema', description='JSON Schema identifier for this adagents.json file'), + NoExplicitNull, ] = None contact: Annotated[ Contact | None, Field( description='Contact information for the entity managing this adagents.json file (may be publisher or third-party operator)' ), + NoExplicitNull, ] = None catalog_etag: Annotated[ str | None, @@ -777,6 +825,7 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None catalog_role: Annotated[ Literal['community_format_registry'] | None, @@ -790,18 +839,21 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): description='Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.', min_length=1, ), + NoExplicitNull, ] = None revoked_publisher_domains: Annotated[ list[RevokedPublisherDomain] | None, Field( description="Publisher domains explicitly removed from this managed network. Validators MUST treat any publisher domain listed here as no-longer-authorized, taking precedence over any appearance of the same domain in `authorized_agents[].publisher_properties[].publisher_domain` / `.publisher_domains[]`, in `authorized_agents[].properties[].publisher_domain` (`inline_properties` authorization type), or in top-level `properties[].publisher_domain`. Lets a network propagate per-publisher revocations on the next refresh instead of waiting for the file-level 7-day cache cap. Validators MUST hold previously-observed publisher revocations, keyed by authority and publisher_domain, for 7 days from the validator's first observation; changing revoked_at MUST NOT reset that first observation, even if the entry vanishes from a subsequent fetch — this closes the rollback gap where an attacker re-serves a stale file with the revocation removed. Networks SHOULD retain entries for at least 7 days after `revoked_at` so validators that didn't observe the original entry still pick it up on refresh." ), + NoExplicitNull, ] = None collections: Annotated[ list[collection.Collection] | None, Field( description='Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.' ), + NoExplicitNull, ] = None placements: Annotated[ list[placement_definition.PlacementDefinition] | None, @@ -809,24 +861,28 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): description='Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.', min_length=1, ), + NoExplicitNull, ] = None superseded_by: Annotated[ AnyUrl | None, Field( description='Optional pointer indicating this adagents.json file has been superseded by another adagents.json at a different URL. Used by the AAO community-mirror lifecycle: when a platform (e.g., Meta) adopts AdCP and publishes its own adagents.json at `/.well-known/adagents.json`, the AAO mirror file at `creative.adcontextprotocol.org/translated//adagents.json` sets `superseded_by` to the platform-hosted URL. Buyer SDKs encountering a file with `superseded_by` SHOULD short-circuit and re-fetch from the named URL rather than serving stale content from the mirror. The mirror SHOULD continue serving with `superseded_by` set for ≥1 minor release after platform adoption so buyer caches keyed on the mirror URL get an explicit migration signal rather than a silent break.' ), + NoExplicitNull, ] = None tags: Annotated[ dict[str, Tags] | None, Field( description='Metadata for each tag referenced by properties. Provides human-readable context for property tag values.' ), + NoExplicitNull, ] = None placement_tags: Annotated[ dict[str, PlacementTags] | None, Field( description='Metadata for each tag referenced by placements. Provides human-readable context for publisher-defined placement tag values used in grouping and authorization.' ), + NoExplicitNull, ] = None authorized_agents: Annotated[ list[AuthorizedAgents], @@ -837,12 +893,14 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): last_updated: Annotated[ AwareDatetime | None, Field(description='ISO 8601 timestamp indicating when this file was last updated'), + NoExplicitNull, ] = None property_features: Annotated[ list[PropertyFeature] | None, Field( description='[AdCP 3.0] Optional list of agents that provide property feature data (certifications, scores, compliance status). Used for discovery - actual data is accessed through property list filters.' ), + NoExplicitNull, ] = None signals: Annotated[ list[signal_definition.SignalDefinition] | None, @@ -850,12 +908,14 @@ class AdcpAgentsAuthorization27(AdCPBaseModel): description="Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.", min_length=1, ), + NoExplicitNull, ] = None signal_tags: Annotated[ dict[str, SignalTags] | None, Field( description='Metadata for each tag referenced by signals. Provides human-readable context for signal tag values.' ), + NoExplicitNull, ] = None @@ -873,12 +933,14 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati field_schema: Annotated[ str | None, Field(alias='$schema', description='JSON Schema identifier for this adagents.json file'), + NoExplicitNull, ] = None contact: Annotated[ Contact | None, Field( description='Contact information for the entity managing this adagents.json file (may be publisher or third-party operator)' ), + NoExplicitNull, ] = None catalog_etag: Annotated[ str | None, @@ -887,6 +949,7 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati max_length=255, min_length=1, ), + NoExplicitNull, ] = None catalog_role: Annotated[ Literal['community_format_registry'] | None, @@ -900,18 +963,21 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati description='Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.', min_length=1, ), + NoExplicitNull, ] = None revoked_publisher_domains: Annotated[ list[RevokedPublisherDomain2] | None, Field( description="Publisher domains explicitly removed from this managed network. Validators MUST treat any publisher domain listed here as no-longer-authorized, taking precedence over any appearance of the same domain in `authorized_agents[].publisher_properties[].publisher_domain` / `.publisher_domains[]`, in `authorized_agents[].properties[].publisher_domain` (`inline_properties` authorization type), or in top-level `properties[].publisher_domain`. Lets a network propagate per-publisher revocations on the next refresh instead of waiting for the file-level 7-day cache cap. Validators MUST hold previously-observed publisher revocations, keyed by authority and publisher_domain, for 7 days from the validator's first observation; changing revoked_at MUST NOT reset that first observation, even if the entry vanishes from a subsequent fetch — this closes the rollback gap where an attacker re-serves a stale file with the revocation removed. Networks SHOULD retain entries for at least 7 days after `revoked_at` so validators that didn't observe the original entry still pick it up on refresh." ), + NoExplicitNull, ] = None collections: Annotated[ list[collection.Collection] | None, Field( description='Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.' ), + NoExplicitNull, ] = None placements: Annotated[ list[placement_definition.PlacementDefinition] | None, @@ -919,24 +985,28 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati description='Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.', min_length=1, ), + NoExplicitNull, ] = None superseded_by: Annotated[ AnyUrl | None, Field( description='Optional pointer indicating this adagents.json file has been superseded by another adagents.json at a different URL. Used by the AAO community-mirror lifecycle: when a platform (e.g., Meta) adopts AdCP and publishes its own adagents.json at `/.well-known/adagents.json`, the AAO mirror file at `creative.adcontextprotocol.org/translated//adagents.json` sets `superseded_by` to the platform-hosted URL. Buyer SDKs encountering a file with `superseded_by` SHOULD short-circuit and re-fetch from the named URL rather than serving stale content from the mirror. The mirror SHOULD continue serving with `superseded_by` set for ≥1 minor release after platform adoption so buyer caches keyed on the mirror URL get an explicit migration signal rather than a silent break.' ), + NoExplicitNull, ] = None tags: Annotated[ dict[str, Tags] | None, Field( description='Metadata for each tag referenced by properties. Provides human-readable context for property tag values.' ), + NoExplicitNull, ] = None placement_tags: Annotated[ dict[str, PlacementTags] | None, Field( description='Metadata for each tag referenced by placements. Provides human-readable context for publisher-defined placement tag values used in grouping and authorization.' ), + NoExplicitNull, ] = None authorized_agents: Annotated[ list[AuthorizedAgents14], @@ -947,12 +1017,14 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati last_updated: Annotated[ AwareDatetime | None, Field(description='ISO 8601 timestamp indicating when this file was last updated'), + NoExplicitNull, ] = None property_features: Annotated[ list[PropertyFeature] | None, Field( description='[AdCP 3.0] Optional list of agents that provide property feature data (certifications, scores, compliance status). Used for discovery - actual data is accessed through property list filters.' ), + NoExplicitNull, ] = None signals: Annotated[ list[signal_definition.SignalDefinition] | None, @@ -960,12 +1032,14 @@ class AdcpAgentsAuthorization29(AdcpAgentsAuthorization22, AdcpAgentsAuthorizati description="Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.", min_length=1, ), + NoExplicitNull, ] = None signal_tags: Annotated[ dict[str, SignalTags] | None, Field( description='Metadata for each tag referenced by signals. Provides human-readable context for signal tag values.' ), + NoExplicitNull, ] = None @@ -973,28 +1047,28 @@ class AdcpAgentsAuthorization210(AdcpAgentsAuthorization27): model_config = ConfigDict( extra='allow', ) - properties: Annotated[list[property.Property], Field(description='Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.', min_length=1), Field(min_length=1)] + properties: Annotated[list[property.Property], Field(description='Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.', min_length=1), NoExplicitNull, Field(min_length=1)] class AdcpAgentsAuthorization211(AdcpAgentsAuthorization27): model_config = ConfigDict( extra='allow', ) - placements: Annotated[list[placement_definition.PlacementDefinition], Field(description='Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.', min_length=1), Field(min_length=1)] + placements: Annotated[list[placement_definition.PlacementDefinition], Field(description='Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.', min_length=1), NoExplicitNull, Field(min_length=1)] class AdcpAgentsAuthorization212(AdcpAgentsAuthorization27): model_config = ConfigDict( extra='allow', ) - collections: Annotated[list[collection.Collection], Field(description='Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.'), Field(min_length=1)] + collections: Annotated[list[collection.Collection], Field(description='Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.'), NoExplicitNull, Field(min_length=1)] class AdcpAgentsAuthorization213(AdcpAgentsAuthorization27): model_config = ConfigDict( extra='allow', ) - signals: Annotated[list[signal_definition.SignalDefinition], Field(description="Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.", min_length=1), Field(min_length=1)] + signals: Annotated[list[signal_definition.SignalDefinition], Field(description="Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.", min_length=1), NoExplicitNull, Field(min_length=1)] class AdcpAgentsAuthorization2( diff --git a/src/adcp/types/domains/brand/acquire_rights_request.py b/src/adcp/types/domains/brand/acquire_rights_request.py index fe4ad0724..993c65aa0 100644 --- a/src/adcp/types/domains/brand/acquire_rights_request.py +++ b/src/adcp/types/domains/brand/acquire_rights_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand/acquire_rights_request.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from datetime import date from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -39,6 +39,7 @@ class Campaign(AdCPBaseModel): countries: Annotated[ list[Country] | None, Field(description='Countries where the campaign will run (ISO 3166-1 alpha-2)'), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -46,6 +47,7 @@ class Campaign(AdCPBaseModel): deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named formats that will be produced. Use `format_kinds` and optional `format_option_refs`.', ), + NoExplicitNull, ] = None format_kinds: Annotated[ list[str] | None, @@ -53,6 +55,7 @@ class Campaign(AdCPBaseModel): description='Canonical creative format kinds that will be produced under the rights grant.', min_length=1, ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -60,6 +63,7 @@ class Campaign(AdCPBaseModel): description='Exact canonical publisher or product format options covered by the planned rights use, when kind alone is insufficient.', min_length=1, ), + NoExplicitNull, ] = None estimated_impressions: Annotated[ SchemaInt | None, @@ -67,13 +71,17 @@ class Campaign(AdCPBaseModel): description="Estimated total impressions for the campaign. Required when the brand agent will project commitment against a governance plan AND the selected pricing_option has model: 'cpm' — projection equals (pricing_option.price / 1000) × estimated_impressions evaluated in pricing_option.currency. The brand agent will project commitment whenever the request is governance-aware via either (a) an intent-phase governance_context token on the protocol envelope, or (b) `account` resolving to an account that has a governance agent previously bound via sync_governance. Brand agents MUST reject with INVALID_REQUEST (field: campaign.estimated_impressions) in either path when CPM-priced rights are requested and this field is omitted or zero; implementer-chosen defaults are non-conformant. See the acquire_rights task reference for the full validation contract including currency-mismatch handling.", ge=0, ), + NoExplicitNull, + ] = None + start_date: Annotated[ + date | None, Field(description='Campaign start date (ISO 8601)'), NoExplicitNull ] = None - start_date: Annotated[date | None, Field(description='Campaign start date (ISO 8601)')] = None end_date: Annotated[ date | None, Field( description='Campaign end date (ISO 8601). Brand agents MUST reject with INVALID_REQUEST (field: campaign.end_date) when end_date is in the past at the time of the request — acquiring rights for an elapsed window produces a zero-duration grant and is almost always a buyer-side bug.' ), + NoExplicitNull, ] = None @@ -89,6 +97,7 @@ class AcquireRightsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None rights_id: Annotated[ str, Field(description='Rights offering identifier from get_rights response') @@ -102,6 +111,7 @@ class AcquireRightsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account context for this acquisition. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance. When both an inline governance_context token and a bound governance agent are present, the token is verified against that resolved relationship. An agent advertising adcp.governance_enforcement for acquire_rights MUST resolve an account and returns ACCOUNT_REQUIRED when neither the request nor an existing resource supplies one; it MUST NOT infer that a missing token means ungoverned. Legacy non-claiming agents may continue to treat omission of both fields as ungoverned during 3.x. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts.' ), + NoExplicitNull, ] = None campaign: Annotated[Campaign, Field(description='Campaign details for rights clearance')] revocation_webhook: Annotated[ @@ -115,6 +125,7 @@ class AcquireRightsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Webhook for async status updates if the acquisition requires approval. The rights agent sends a webhook notification when the status transitions to acquired or rejected.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -125,5 +136,5 @@ class AcquireRightsRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/creative_approval_request.py b/src/adcp/types/domains/brand/creative_approval_request.py index 1b5e5c17e..b7468825c 100644 --- a/src/adcp/types/domains/brand/creative_approval_request.py +++ b/src/adcp/types/domains/brand/creative_approval_request.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: brand/creative_approval_request.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import context as context_1 @@ -24,6 +25,7 @@ class CreativeApprovalRequest(AdcpVersionEnvelope): Field( description='Buyer-assigned creative identifier. Equivalent to OpenRTB crid. Used to track approval status across resubmissions.' ), + NoExplicitNull, ] = None creative_url: Annotated[ AnyUrl, Field(description='URL where the creative asset can be retrieved for review') @@ -31,15 +33,19 @@ class CreativeApprovalRequest(AdcpVersionEnvelope): creative_format: Annotated[ format_id.FormatReferenceStructuredObject | None, Field(description='Format of the creative being submitted'), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Description of the creative for reviewer context') + str | None, + Field(description='Description of the creative for reviewer context'), + NoExplicitNull, ] = None metadata: Annotated[ dict[str, Any] | None, Field( description='Additional creative metadata (duration, dimensions, target audience, etc.)' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -50,5 +56,5 @@ class CreativeApprovalRequest(AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/creative_approval_response.py b/src/adcp/types/domains/brand/creative_approval_response.py index 7593c775e..f0da03818 100644 --- a/src/adcp/types/domains/brand/creative_approval_response.py +++ b/src/adcp/types/domains/brand/creative_approval_response.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: brand/creative_approval_response.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..core import context as context_1 @@ -27,16 +28,17 @@ class CreativeApprovalResponse1(AdcpVersionEnvelope, ProtocolEnvelope): ] = 'approved' rights_id: str creative_id: Annotated[ - str | None, Field(description="Echo of the buyer's creative identifier") + str | None, Field(description="Echo of the buyer's creative identifier"), NoExplicitNull ] = None - creative_url: AnyUrl | None = None - approved_at: AwareDatetime | None = None + creative_url: Annotated[AnyUrl | None, NoExplicitNull] = None + approved_at: Annotated[AwareDatetime | None, NoExplicitNull] = None conditions: Annotated[ list[str] | None, Field(description="Conditions on the approval (e.g., 'approved for NL market only')"), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CreativeApprovalResponse2(AdcpVersionEnvelope, ProtocolEnvelope): @@ -46,18 +48,19 @@ class CreativeApprovalResponse2(AdcpVersionEnvelope, ProtocolEnvelope): approval_status: Annotated[Literal['rejected'], Field(description='Creative was rejected')] = 'rejected' rights_id: str creative_id: Annotated[ - str | None, Field(description="Echo of the buyer's creative identifier") + str | None, Field(description="Echo of the buyer's creative identifier"), NoExplicitNull ] = None - creative_url: AnyUrl | None = None + creative_url: Annotated[AnyUrl | None, NoExplicitNull] = None reason: Annotated[str, Field(description='Why the creative was rejected')] suggestions: Annotated[ list[str] | None, Field( description='Actionable feedback for revision. If present, the buyer can revise and resubmit the creative. If absent, the rejection is final for this creative concept.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CreativeApprovalResponse3(AdcpVersionEnvelope, ProtocolEnvelope): @@ -69,20 +72,23 @@ class CreativeApprovalResponse3(AdcpVersionEnvelope, ProtocolEnvelope): ] = 'pending_review' rights_id: str creative_id: Annotated[ - str | None, Field(description="Echo of the buyer's creative identifier") + str | None, Field(description="Echo of the buyer's creative identifier"), NoExplicitNull ] = None - creative_url: AnyUrl | None = None + creative_url: Annotated[AnyUrl | None, NoExplicitNull] = None estimated_response_time: Annotated[ - str | None, Field(description="Expected time for review (e.g., '24h', '2 business days')") + str | None, + Field(description="Expected time for review (e.g., '24h', '2 business days')"), + NoExplicitNull, ] = None status_url: Annotated[ AnyUrl | None, Field( description='URL to poll for updated approval status. GET this URL to receive a creative-approval-response. Poll at reasonable intervals (suggested: every 5 minutes, back off after 1 hour to every 30 minutes). Stop polling after estimated_response_time has elapsed and the approval_status is still pending_review.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CreativeApprovalResponse4(AdcpVersionEnvelope, ProtocolEnvelope): @@ -90,8 +96,8 @@ class CreativeApprovalResponse4(AdcpVersionEnvelope, ProtocolEnvelope): extra='allow', ) errors: Annotated[list[error.Error], Field(min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None CreativeApprovalResponse = Annotated[ diff --git a/src/adcp/types/domains/brand/get_brand_identity_request.py b/src/adcp/types/domains/brand/get_brand_identity_request.py index 564a740e9..1e4e31398 100644 --- a/src/adcp/types/domains/brand/get_brand_identity_request.py +++ b/src/adcp/types/domains/brand/get_brand_identity_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand/get_brand_identity_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -42,12 +43,14 @@ class GetBrandIdentityRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional identity sections to include in the response. When omitted, all sections the caller is authorized to see are returned. Core fields (brand_id, house, names) are always returned and do not need to be requested.', min_length=1, ), + NoExplicitNull, ] = None use_case: Annotated[ str | None, Field( description="Intended use case, so the agent can tailor the response. A 'voice_synthesis' use case returns voice configs; a 'likeness' use case returns high-res photos and appearance guidelines." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/get_rights_request.py b/src/adcp/types/domains/brand/get_rights_request.py index aac1f77b2..abe51f1c5 100644 --- a/src/adcp/types/domains/brand/get_rights_request.py +++ b/src/adcp/types/domains/brand/get_rights_request.py @@ -1,15 +1,15 @@ # generated by datamodel-codegen: # filename: brand/get_rights_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types.base import AdcpRequest -from adcp.types._scalar import ScalarStr - from typing import Annotated +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool from ..core import brand_ref @@ -49,32 +49,38 @@ class GetRightsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="The buyer's brand. The agent fetches the buyer's brand.json for compatibility filtering (e.g., dietary conflicts, competitor exclusions)." ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, Field( description='Countries where rights are needed (ISO 3166-1 alpha-2). Filters to rights available in these markets.' ), + NoExplicitNull, ] = None brand_id: Annotated[ str | None, Field( description="Search within a specific brand's rights. If omitted, searches across the agent's full roster." ), + NoExplicitNull, ] = None right_type: Annotated[ right_type_1.RightType | None, Field(description='Filter by type of rights (talent, music, stock_media, etc.)'), + NoExplicitNull, ] = None include_excluded: Annotated[ StrictBool | None, Field( description='Include filtered-out results in the excluded array with reasons. Defaults to false.' ), + NoExplicitNull, ] = False pagination: Annotated[ pagination_request.PaginationRequest | None, Field(description='Pagination parameters for large result sets'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/revocation_notification.py b/src/adcp/types/domains/brand/revocation_notification.py index e6424cfac..e501e9023 100644 --- a/src/adcp/types/domains/brand/revocation_notification.py +++ b/src/adcp/types/domains/brand/revocation_notification.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: brand/revocation_notification.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import context as context_1 @@ -42,6 +42,7 @@ class RevocationNotification(AdCPBaseModel): description='If present, only these uses are revoked (partial revocation). If absent, all uses under the grant are revoked.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/rights_pricing_option.py b/src/adcp/types/domains/brand/rights_pricing_option.py index 13e7695ff..25f0ab3c7 100644 --- a/src/adcp/types/domains/brand/rights_pricing_option.py +++ b/src/adcp/types/domains/brand/rights_pricing_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: brand/rights_pricing_option.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import ext as ext_1 @@ -44,16 +44,21 @@ class RightsPricingOption(AdCPBaseModel): period: Annotated[ rights_billing_period.RightsBillingPeriod | None, Field(description='Billing period for flat_rate and time-based models'), + NoExplicitNull, ] = None impression_cap: Annotated[ SchemaInt | None, Field(description='Maximum impressions included in this pricing option per period', ge=1), + NoExplicitNull, ] = None overage_cpm: Annotated[ StrictFloat | None, Field(description='CPM rate applied to impressions exceeding the impression_cap', ge=0.0), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of this pricing option') + str | None, + Field(description='Human-readable description of this pricing option'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/rights_terms.py b/src/adcp/types/domains/brand/rights_terms.py index 7e9bc7637..10c526c46 100644 --- a/src/adcp/types/domains/brand/rights_terms.py +++ b/src/adcp/types/domains/brand/rights_terms.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: brand/rights_terms.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from datetime import date from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictFloat @@ -23,8 +23,8 @@ class Exclusivity(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - scope: str | None = None - countries: list[Country] | None = None + scope: Annotated[str | None, NoExplicitNull] = None + countries: Annotated[list[Country] | None, NoExplicitNull] = None class RightsTerms(AdCPBaseModel): @@ -34,12 +34,12 @@ class RightsTerms(AdCPBaseModel): pricing_option_id: str amount: Annotated[StrictFloat, Field(ge=0.0)] currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] - period: rights_billing_period.RightsBillingPeriod | None = None + period: Annotated[rights_billing_period.RightsBillingPeriod | None, NoExplicitNull] = None uses: list[right_use.RightUse] - impression_cap: Annotated[SchemaInt | None, Field(ge=1)] = None - overage_cpm: Annotated[StrictFloat | None, Field(ge=0.0)] = None - start_date: date | None = None - end_date: date | None = None + impression_cap: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + overage_cpm: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + start_date: Annotated[date | None, NoExplicitNull] = None + end_date: Annotated[date | None, NoExplicitNull] = None exclusivity: Annotated[ - Exclusivity | None, Field(description='Exclusivity terms if applicable') + Exclusivity | None, Field(description='Exclusivity terms if applicable'), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/brand/search_brands_request.py b/src/adcp/types/domains/brand/search_brands_request.py index 605de3dc2..e55ff6263 100644 --- a/src/adcp/types/domains/brand/search_brands_request.py +++ b/src/adcp/types/domains/brand/search_brands_request.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: brand/search_brands_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarStr - from typing import Annotated +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field from ..core import brand_ref @@ -40,6 +40,7 @@ class SearchBrandsRequest(AdcpVersionEnvelope): description='Optional industry filter. The agent restricts results to brands operating in these industries. Values follow the advertiser-industry taxonomy; the agent SHOULD accept unknown values gracefully per the advertiser-industry extension policy.', min_length=1, ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -47,16 +48,19 @@ class SearchBrandsRequest(AdcpVersionEnvelope): description='Optional market filter. Returns brands with rights availability in at least one of the specified countries (ISO 3166-1 alpha-2).', min_length=1, ), + NoExplicitNull, ] = None buyer_brand: Annotated[ brand_ref.BrandReference | None, Field( description="The buyer's brand. When provided, the agent SHOULD apply the same compatibility filtering as get_rights — filtering out brands with category conflicts, competitor exclusions, or dietary restrictions that would prevent licensing. When omitted, results are returned without compatibility filtering." ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field(description='Pagination parameters for large result sets'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/search_brands_response.py b/src/adcp/types/domains/brand/search_brands_response.py index e6fa16795..c17283dbe 100644 --- a/src/adcp/types/domains/brand/search_brands_response.py +++ b/src/adcp/types/domains/brand/search_brands_response.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: brand/search_brands_response.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, model_validator @@ -79,10 +79,14 @@ class Logo(AdCPBaseModel): ) url: Annotated[AnyUrl, Field(description='URL to the logo asset')] orientation: Annotated[ - Orientation | None, Field(description='Logo aspect ratio orientation') + Orientation | None, Field(description='Logo aspect ratio orientation'), NoExplicitNull ] = None - background: Annotated[Background | None, Field(description='Background compatibility')] = None - variant: Annotated[Variant | None, Field(description='Logo variant type')] = None + background: Annotated[ + Background | None, Field(description='Background compatibility'), NoExplicitNull + ] = None + variant: Annotated[Variant | None, Field(description='Logo variant type'), NoExplicitNull] = ( + None + ) class Country(ScalarStr): @@ -98,16 +102,18 @@ class Rights(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - available_uses: list[right_use.RightUse] | None = None + available_uses: Annotated[list[right_use.RightUse] | None, NoExplicitNull] = None countries: Annotated[ list[Country] | None, Field( description='Countries where rights are available (ISO 3166-1 alpha-2). If omitted, rights are available worldwide.' ), + NoExplicitNull, ] = None excluded_countries: Annotated[ list[ExcludedCountry] | None, Field(description='Countries excluded from availability (ISO 3166-1 alpha-2)'), + NoExplicitNull, ] = None @@ -126,24 +132,28 @@ class SearchBrandResult(AdCPBaseModel): Field( description='The house (corporate entity) associated with this brand. Present when a house affiliation is on record. Use relationship_trust to determine whether this association has been reciprocally confirmed by the house.' ), + NoExplicitNull, ] = None relationship_trust: Annotated[ RelationshipTrust | None, Field( description="Trust verdict for the brand-to-house relationship, computed from the mutual-assertion model. inline = brand defined inside the house's own document; mutual = both sides publish the relationship (trust-extending); leaf_only = brand claims house but house has not reciprocated or declaration is expired; house_only = house claims brand, brand has not reciprocated; standalone = brand claims no house; unverifiable = claimed house could not be checked. When absent, trust has not yet been computed — absent MUST NOT be interpreted as standalone. Use get_brand_identity or verify_brand_claim for authoritative trust resolution." ), + NoExplicitNull, ] = None relationship_verified_at: Annotated[ AwareDatetime | None, Field( description='When the mutual-assertion edge was last confirmed by both sides. Present only when relationship_trust is mutual.' ), + NoExplicitNull, ] = None claimed_house_domain: Annotated[ str | None, Field( description='Canonical domain of the house this brand self-declares as its parent. Not trust-extending on its own. Present for leaf_only and unverifiable trust states where the brand has made a claim that could not be or has not been reciprocated.' ), + NoExplicitNull, ] = None names: Annotated[ list[dict[str, str]], @@ -151,30 +161,36 @@ class SearchBrandResult(AdCPBaseModel): description="Localized brand names with BCP 47 locale code keys (e.g., 'en_US', 'fr_CA')." ), ] - description: Annotated[str | None, Field(description='Brand description')] = None + description: Annotated[str | None, Field(description='Brand description'), NoExplicitNull] = ( + None + ) industries: Annotated[ list[advertiser_industry.AdvertiserIndustry] | None, Field( description='Brand industries, following the advertiser-industry taxonomy', min_length=1 ), + NoExplicitNull, ] = None keller_type: Annotated[ KellerType | None, Field( description='Brand architecture type: master (primary brand of house), sub_brand (carries parent name), endorsed (independent identity backed by parent), independent (operates separately)' ), + NoExplicitNull, ] = None logos: Annotated[ list[Logo] | None, Field( description='Standard-resolution logos for display during discovery. High-res variants are gated behind get_brand_identity with account authorization.' ), + NoExplicitNull, ] = None rights: Annotated[ Rights | None, Field( description='Rights availability summary. Signals whether this brand has licensable rights and in which markets. For detailed pricing and rights records, use get_rights.' ), + NoExplicitNull, ] = None @@ -182,16 +198,18 @@ class SearchBrandsResponse(AdcpVersionEnvelope, ProtocolEnvelope): brands: Annotated[ list[SearchBrandResult] | None, Field(description='Brand stubs matching the query, ranked by relevance'), + NoExplicitNull, ] = None pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description='Pagination metadata. Present when has_more is true or total_count is known.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - errors: Annotated[list[error.Error] | None, Field(min_length=1)] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, Field(min_length=1), NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> SearchBrandsResponse: diff --git a/src/adcp/types/domains/brand/update_rights_request.py b/src/adcp/types/domains/brand/update_rights_request.py index f9a11a174..0309d4dd1 100644 --- a/src/adcp/types/domains/brand/update_rights_request.py +++ b/src/adcp/types/domains/brand/update_rights_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand/update_rights_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from datetime import date from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -30,6 +31,7 @@ class UpdateRightsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None rights_id: Annotated[ str, Field(description='Rights grant identifier from acquire_rights response') @@ -39,12 +41,14 @@ class UpdateRightsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account context for this update. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance — update_rights is a modification-phase governance trigger (per `/docs/governance/campaign/specification#spend-commit-invocation`) and the brand agent consults the bound agent when computing the incremental commit delta. When both an inline governance_context token (on the protocol envelope) and a bound governance agent are present, the inline token wins. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. The estimated_impressions / commit-delta projection rule for governance-aware updates is tracked separately and not yet normative on this task.' ), + NoExplicitNull, ] = None end_date: Annotated[ date | None, Field( description='New end date for the rights grant (must be >= current end_date). Extending the grant may re-issue generation credentials with updated expiration.' ), + NoExplicitNull, ] = None impression_cap: Annotated[ SchemaInt | None, @@ -52,22 +56,26 @@ class UpdateRightsRequest(AdcpRequest, AdcpVersionEnvelope): description='New impression cap for the grant. Must be >= impressions already delivered.', ge=1, ), + NoExplicitNull, ] = None pricing_option_id: Annotated[ str | None, Field( description="Switch to a different pricing option from the original get_rights offering. The new option must be compatible with the existing grant's uses and countries." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field( description='Pause or resume the rights grant. When paused, generation credentials are suspended and creative delivery should stop. When resumed, credentials are re-activated.' ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field(description='Webhook for async update notifications if the update requires approval'), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -78,5 +86,5 @@ class UpdateRightsRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/brand/verify_brand_claims_request.py b/src/adcp/types/domains/brand/verify_brand_claims_request.py index 487612174..160679181 100644 --- a/src/adcp/types/domains/brand/verify_brand_claims_request.py +++ b/src/adcp/types/domains/brand/verify_brand_claims_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand/verify_brand_claims_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -34,9 +34,12 @@ class Claim(AdCPBaseModel): description='Stable brand identifier the leaf uses for itself. Optional but recommended.', pattern='^[a-z0-9_]+$', ), + NoExplicitNull, ] = None observed_at: Annotated[ - AwareDatetime | None, Field(description="When the caller observed the leaf's claim.") + AwareDatetime | None, + Field(description="When the caller observed the leaf's claim."), + NoExplicitNull, ] = None @@ -60,9 +63,11 @@ class Claim1(AdCPBaseModel): ), ] claimant_says: Annotated[ - str | None, Field(description='Optional context — what the claimant published or said.') + str | None, + Field(description='Optional context — what the claimant published or said.'), + NoExplicitNull, ] = None - observed_at: AwareDatetime | None = None + observed_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class ClaimEntry2(AdCPBaseModel): @@ -91,8 +96,8 @@ class Property(AdCPBaseModel): ) type: property_type.PropertyType identifier: str - store: Store | None = None - region: str | None = None + store: Annotated[Store | None, NoExplicitNull] = None + region: Annotated[str | None, NoExplicitNull] = None class Claim2(AdCPBaseModel): @@ -101,7 +106,9 @@ class Claim2(AdCPBaseModel): ) property: Annotated[Property, Field(description='Shape matches brand.json properties[] entry.')] use_case: Annotated[ - str | None, Field(description='Optional caller-declared use case.', max_length=100) + str | None, + Field(description='Optional caller-declared use case.', max_length=100), + NoExplicitNull, ] = None @@ -123,10 +130,10 @@ class Claim3(AdCPBaseModel): extra='allow', ) mark: Annotated[str, Field(max_length=200, min_length=1)] - registry: Annotated[str | None, Field(max_length=50)] = None - number: Annotated[str | None, Field(max_length=100)] = None - countries: list[Country] | None = None - use_case: Annotated[str | None, Field(max_length=100)] = None + registry: Annotated[str | None, Field(max_length=50), NoExplicitNull] = None + number: Annotated[str | None, Field(max_length=100), NoExplicitNull] = None + countries: Annotated[list[Country] | None, NoExplicitNull] = None + use_case: Annotated[str | None, Field(max_length=100), NoExplicitNull] = None class ClaimEntry4(AdCPBaseModel): diff --git a/src/adcp/types/domains/brand_discovery.py b/src/adcp/types/domains/brand_discovery.py index 40a69e556..6a61e1ab3 100644 --- a/src/adcp/types/domains/brand_discovery.py +++ b/src/adcp/types/domains/brand_discovery.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: _brand_discovery.json -# timestamp: 2026-10-11T15:55:30+00:00 +# timestamp: 2026-10-21T11:05:59+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Dict, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat, ScalarInt, ScalarStr from pydantic import ( AnyUrl, @@ -727,18 +727,18 @@ class DataSubjectContestation1(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - url: AnyUrl | None = None - email: EmailStr | None = None - languages: list[str] | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + email: Annotated[EmailStr | None, NoExplicitNull] = None + languages: Annotated[list[str] | None, NoExplicitNull] = None class Colors1(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None + primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None class BrandIdModel(ScalarStr): @@ -768,12 +768,14 @@ class AiTool(AdCPBaseModel): Field( description="Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version." ), + NoExplicitNull, ] = None provider: Annotated[ str | None, Field( description="Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" ), + NoExplicitNull, ] = None @@ -797,6 +799,7 @@ class DeclaredBy(AdCPBaseModel): agent_url: Annotated[ AnyUrl | None, Field(description='URL of the agent or service that declared this provenance'), + NoExplicitNull, ] = None role: Annotated[Role, Field(description='Role of the declaring party in the supply chain')] @@ -822,6 +825,7 @@ class VerifyAgent(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -840,6 +844,7 @@ class VerifyAgent1(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -858,7 +863,9 @@ class VerificationItem(AdCPBaseModel): ), ] verified_time: Annotated[ - AwareDatetime | None, Field(description='When the verification was performed (ISO 8601)') + AwareDatetime | None, + Field(description='When the verification was performed (ISO 8601)'), + NoExplicitNull, ] = None result: Annotated[Result, Field(description='Verification outcome')] confidence: Annotated[ @@ -866,9 +873,10 @@ class VerificationItem(AdCPBaseModel): Field( description='Confidence score of the verification result (0.0 to 1.0)', ge=0.0, le=1.0 ), + NoExplicitNull, ] = None details_url: Annotated[ - AnyUrl | None, Field(description='URL to the full verification report') + AnyUrl | None, Field(description='URL to the full verification report'), NoExplicitNull ] = None @@ -2010,6 +2018,7 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" ), + NoExplicitNull, ] = None provider: Annotated[ str, @@ -2022,9 +2031,12 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts)." ), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the provenance data was embedded (ISO 8601)') + AwareDatetime | None, + Field(description='When the provenance data was embedded (ISO 8601)'), + NoExplicitNull, ] = None @@ -2043,13 +2055,17 @@ class Watermark(AdCPBaseModel): Field( description="Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist)." ), + NoExplicitNull, ] = None c2pa_action: Annotated[ C2paWatermarkAction | None, Field(description='C2PA action classification for this watermark'), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the watermark was applied (ISO 8601)') + AwareDatetime | None, + Field(description='When the watermark was applied (ISO 8601)'), + NoExplicitNull, ] = None @@ -2059,6 +2075,7 @@ class RenderGuidance(AdCPBaseModel): Field( description='How long the disclosure must persist during content playback or display' ), + NoExplicitNull, ] = None min_duration_ms: Annotated[ SchemaInt | None, @@ -2066,6 +2083,7 @@ class RenderGuidance(AdCPBaseModel): description="Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.", ge=1, ), + NoExplicitNull, ] = None positions: Annotated[ list[DisclosurePosition] | None, @@ -2073,8 +2091,9 @@ class RenderGuidance(AdCPBaseModel): description='Preferred disclosure positions in priority order. The first position a format supports should be used.', min_length=1, ), + NoExplicitNull, ] = None - ext: Ext | None = None + ext: Annotated[Ext | None, NoExplicitNull] = None class Jurisdiction(AdCPBaseModel): @@ -2084,6 +2103,7 @@ class Jurisdiction(AdCPBaseModel): region: Annotated[ str | None, Field(description="Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)"), + NoExplicitNull, ] = None regulation: Annotated[ str, @@ -2096,12 +2116,14 @@ class Jurisdiction(AdCPBaseModel): Field( description='Required disclosure label text for this jurisdiction, in the local language' ), + NoExplicitNull, ] = None render_guidance: Annotated[ RenderGuidance | None, Field( description="How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed." ), + NoExplicitNull, ] = None @@ -2115,6 +2137,7 @@ class Disclosure(AdCPBaseModel): jurisdictions: Annotated[ list[Jurisdiction] | None, Field(description='Jurisdictions where disclosure obligations apply', min_length=1), + NoExplicitNull, ] = None @@ -2124,46 +2147,54 @@ class Provenance(AdCPBaseModel): Field( description='IPTC-aligned classification of AI involvement in producing this content' ), + NoExplicitNull, ] = None synthetic_depiction: Annotated[ StrictBool | None, Field( description='Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.' ), + NoExplicitNull, ] = None ai_tool: Annotated[ AiTool | None, Field( description='AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.' ), + NoExplicitNull, ] = None human_oversight: Annotated[ HumanOversight | None, Field( description='Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.' ), + NoExplicitNull, ] = None declared_by: Annotated[ DeclaredBy | None, Field( description='Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.' ), + NoExplicitNull, ] = None declared_at: Annotated[ AwareDatetime | None, Field( description='When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.' ), + NoExplicitNull, ] = None created_time: Annotated[ AwareDatetime | None, Field(description='When this content was created or generated (ISO 8601)'), + NoExplicitNull, ] = None c2pa: Annotated[ C2pa | None, Field( description='C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.' ), + NoExplicitNull, ] = None embedded_provenance: Annotated[ list[EmbeddedProvenanceItem] | None, @@ -2171,6 +2202,7 @@ class Provenance(AdCPBaseModel): description='Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None watermarks: Annotated[ list[Watermark] | None, @@ -2178,12 +2210,14 @@ class Provenance(AdCPBaseModel): description='Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None disclosure: Annotated[ Disclosure | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), + NoExplicitNull, ] = None verification: Annotated[ list[VerificationItem] | None, @@ -2191,8 +2225,9 @@ class Provenance(AdCPBaseModel): description='Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.', min_length=1, ), + NoExplicitNull, ] = None - ext: Ext | None = None + ext: Annotated[Ext | None, NoExplicitNull] = None class RightsAgent(AdCPBaseModel): @@ -2436,6 +2471,7 @@ class ImageAsset(AdCPBaseModel): description='Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.', ge=1, ), + NoExplicitNull, ] = None pixel_ratio: Annotated[ StrictFloat | None, @@ -2443,18 +2479,21 @@ class ImageAsset(AdCPBaseModel): description='Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.', gt=0.0, ), + NoExplicitNull, ] = None state_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None breakpoint_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None focal_point: Annotated[ list[FocalPointItem] | None, @@ -2463,16 +2502,22 @@ class ImageAsset(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None format: Annotated[ - str | None, Field(description='Image file format (jpg, png, gif, webp, etc.)') + str | None, + Field(description='Image file format (jpg, png, gif, webp, etc.)'), + NoExplicitNull, + ] = None + alt_text: Annotated[ + str | None, Field(description='Alternative text for accessibility'), NoExplicitNull ] = None - alt_text: Annotated[str | None, Field(description='Alternative text for accessibility')] = None provenance: Annotated[ Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None @@ -2480,17 +2525,20 @@ class BrandKitOverride(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - logo: Annotated[ImageAsset | None, Field(description='Override logo asset.')] = None - colors: Annotated[Colors1 | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) + logo: Annotated[ + ImageAsset | None, Field(description='Override logo asset.'), NoExplicitNull + ] = None + colors: Annotated[ + Colors1 | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull + ] = None voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class BrandRef(AdCPBaseModel): @@ -2509,6 +2557,7 @@ class BrandRef(AdCPBaseModel): Field( description='Brand identifier within the house portfolio. Optional for single-brand domains.' ), + NoExplicitNull, ] = None countries: Annotated[ list[Country1] | None, @@ -2516,24 +2565,28 @@ class BrandRef(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation1 | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/bundled/protocol/get_adcp_capabilities_response.py b/src/adcp/types/domains/bundled/protocol/get_adcp_capabilities_response.py index 5ad07370b..9ccc22545 100644 --- a/src/adcp/types/domains/bundled/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/domains/bundled/protocol/get_adcp_capabilities_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: bundled/protocol/get_adcp_capabilities_response.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Dict, Literal -from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, SchemaInt +from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat, ScalarInt, ScalarStr from pydantic import ( AnyUrl, @@ -102,18 +102,21 @@ class Issue(AdCPBaseModel): Field( description="Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." ), + NoExplicitNull, ] = None schema_id: Annotated[ str | None, Field( description="Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` — either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element — the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." ), + NoExplicitNull, ] = None discriminator: Annotated[ list[DiscriminatorItem] | None, Field( description="Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload — emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx." ), + NoExplicitNull, ] = None @@ -146,14 +149,18 @@ class AdcpError(AdCPBaseModel): Field( description='Optional buyer-actionable classification of the failure. Use this when the enclosing error code or message is too coarse or contains producer-internal context that must not cross the buyer trust boundary. `code` reuses the comprehensive standard vocabulary and recovery classifications published in `enums/error-code.json`, including policy, governance, account, commercial, inventory, and creative failures. The wire field remains open for forward compatibility. `message` MUST be safe to show to the buyer and MUST NOT contain vendor identifiers, ad-server type names, internal object names, internal IDs, stack traces, or other producer-private implementation details. When present, the enclosing `error.recovery` MUST classify the buyer-actionable reason. If both the enclosing code and buyer reason are registered, their standard recovery classifications MUST agree with each other and with `error.recovery`; sellers MUST choose the closest compatible enclosing code rather than retain a conflicting upstream transport wrapper. One buyer_reason classifies one error object. If a rejection contains independently actionable failures from different classes, sellers MUST emit separate error objects or omit buyer_reason rather than select a misleading single class.' ), + NoExplicitNull, ] = None field: Annotated[ str | None, Field( description="Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' → 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." ), + NoExplicitNull, + ] = None + suggestion: Annotated[ + str | None, Field(description='Suggested fix for the error'), NoExplicitNull ] = None - suggestion: Annotated[str | None, Field(description='Suggested fix for the error')] = None retry_after: Annotated[ StrictFloat | None, Field( @@ -161,36 +168,42 @@ class AdcpError(AdCPBaseModel): ge=1.0, le=3600.0, ), + NoExplicitNull, ] = None issues: Annotated[ list[Issue] | None, Field( description='Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only — translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` → `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers — the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.' ), + NoExplicitNull, ] = None details: Annotated[ dict[str, Any] | None, Field( description='Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n "code": "INVALID_PRICING_OPTION",\n "message": "Pricing option not found: po_prism_abandoner_cpm",\n "field": "pricing_option_id",\n "details": {\n "rejected_value": "po_prism_abandoner_cpm",\n "accepted_values": ["po_prism_cart_cpm", "po_prism_view_cpm"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what\'s accepted for *this caller in this context* (account, brand, scope) — leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.' ), + NoExplicitNull, ] = None recovery: Annotated[ Recovery | None, Field( description='Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). AdCP 3.2 producers MUST populate `recovery` on every error; 3.1 producers SHOULD populate it. The shared 3.x schema intentionally does not add `recovery` to `required` so retained and live errors from earlier 3.x producers remain decodable. When `buyer_reason` is present, `recovery` is required and MUST classify that buyer-actionable reason. If the enclosing code and buyer reason are both registered, their standard recovery classifications MUST agree with each other and with this field. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. When a legacy error omits `recovery`, receivers use the registered classification for a known code and fall back to `transient` for an unknown code, subject to the bounded retry budget. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known top-level and buyer-reason codes; `error.recovery` on the wire is authoritative when present.' ), + NoExplicitNull, ] = None source: Annotated[ Source | None, Field( description='Who emitted this error entry. `producer` (default when absent): emitted by the response\'s authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn\'t project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network — responses commonly traverse multiple SDKs (e.g., sales agent → interchange → DSP → buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry\'s `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: "sdk"`) are authoritative for what the response\'s authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can\'t tell whether a code came from the seller or an intermediate SDK, which corrupts attribution.' ), + NoExplicitNull, ] = None sdk_id: Annotated[ str | None, Field( description='Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: "sdk"`; MUST be absent when `source: "producer"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK\'s registry version doesn\'t).' ), + NoExplicitNull, ] = None @@ -235,6 +248,7 @@ class PushNotificationConfig(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None token: Annotated[ str | None, @@ -243,6 +257,7 @@ class PushNotificationConfig(AdCPBaseModel): max_length=4096, min_length=16, ), + NoExplicitNull, ] = None authentication: Annotated[ Authentication | None, @@ -250,6 +265,7 @@ class PushNotificationConfig(AdCPBaseModel): deprecated=True, description='Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt \'try 9421 first, fall back to HMAC\' verification — signature mode is determined solely by whether this block was present at registration time. The seller\'s baseline 9421 webhook key is published at its brand.json `agents[]` `jwks_uri` using `adcp_use: "request-signing"` (deprecated `webhook-signing` keys remain accepted during the compatibility window); it does not override this selector and is only used when `authentication` is omitted. See docs/building/by-layer/L1/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook\'s signing mode does not match the registered mode).', ), + NoExplicitNull, ] = None @@ -287,12 +303,14 @@ class Idempotency(AdCPBaseModel): ge=1, le=604800, ), + NoExplicitNull, ] = None account_id_is_opaque: Annotated[ StrictBool | None, Field( description="When true, the seller derives `account_id` via an HKDF-based one-way transform of the buyer's natural account key rather than echoing the natural key on the wire. Buyers MUST NOT attempt to invert the opaque id and MUST treat it as a blind handle scoped to this seller. Absent or false, callers should assume `account_id` is the natural key (or a server-assigned but non-opaque id). This flag does not change the wire shape, but it DOES change buyer behavior — buyers MUST NOT cache, log, or treat `account_id` as a natural-key analog when this flag is true. Migration note for sellers already returning an opaque id without this flag: set it to true at the next capabilities refresh so buyers stop inferring natural-key semantics; until set, new-buyer replay/retry logic will misclassify these ids as natural keys." ), + NoExplicitNull, ] = False @@ -356,6 +374,7 @@ class ReportingDestinationOffering(AdCPBaseModel): description='Physical file formats the seller can produce. Only meaningful for file_transfer.', min_length=1, ), + NoExplicitNull, ] = None access_modes: Annotated[ list[AccessMode] | None, @@ -363,6 +382,7 @@ class ReportingDestinationOffering(AdCPBaseModel): description='Dataset-share access families the seller can provision. Only meaningful for dataset_share.', min_length=1, ), + NoExplicitNull, ] = None verification_profiles: Annotated[ list[VerificationProfile], @@ -462,6 +482,7 @@ class Principal(AdCPBaseModel): ge=1, le=64, ), + NoExplicitNull, ] = None reporting_destination_offerings: Annotated[ list[ReportingDestinationOffering] | None, @@ -470,6 +491,7 @@ class Principal(AdCPBaseModel): max_length=3, min_length=1, ), + NoExplicitNull, ] = None suspension_interval_seconds: Annotated[ SchemaInt | None, @@ -478,6 +500,7 @@ class Principal(AdCPBaseModel): ge=1, le=86400, ), + NoExplicitNull, ] = None caller_event_types: Annotated[ list[CallerEventType] | None, @@ -485,6 +508,7 @@ class Principal(AdCPBaseModel): description='Notification types this seller can emit against caller-level subscribers, so buyers select from the offering instead of probing by rejection. Account-anchored entries fire with fire-time authorization scoping. When absent, only capabilities.changed is assumed. A caller-level subscriber requesting a type outside this set fails the sync request with UNSUPPORTED_FEATURE.', min_length=1, ), + NoExplicitNull, ] = None optimistic_concurrency: Annotated[ StrictBool, @@ -529,6 +553,7 @@ class Notifications(AdCPBaseModel): ge=0, le=86400, ), + NoExplicitNull, ] = None @@ -555,12 +580,14 @@ class CapabilityChanges(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None last_modified: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp when the seller last changed the advertised capability document. This is the scoped replacement for legacy top-level `last_updated` when buyers need cache validation semantics.' ), + NoExplicitNull, ] = None cache_ttl_seconds: Annotated[ SchemaInt | None, @@ -569,12 +596,14 @@ class CapabilityChanges(AdCPBaseModel): ge=60, le=604800, ), + NoExplicitNull, ] = None notifications: Annotated[ Notifications | Notifications5 | None, Field( description='Whether the seller supports agent-level capability-change webhooks. When supported, interested consumers register endpoint subscribers with the declared registration_task; sync_principal is preferred when the broader connection surface is available, while sync_agent_notification_configs remains the specialized compatibility task. Each capabilities.changed fire is a small invalidation payload, and consumers repair by re-reading get_adcp_capabilities.' ), + NoExplicitNull, ] = None @@ -744,6 +773,7 @@ class GovernanceEnforcement(AdCPBaseModel): description='Seller-wide advisory default for governance agents this enforcing service accepts as binding counterparties. A candidate satisfying any matcher is accepted by this declaration. The per-account sync_governance response is authoritative and may apply stricter account-specific criteria. Omission means accept any, preserving legacy behavior.', title='Accepted Governance Agents', ), + NoExplicitNull, ] = None @@ -762,9 +792,9 @@ class DataSubjectContestation(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - url: AnyUrl | None = None - email: EmailStr | None = None - languages: list[str] | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + email: Annotated[EmailStr | None, NoExplicitNull] = None + languages: Annotated[list[str] | None, NoExplicitNull] = None class FocalPointItem(ScalarFloat): @@ -776,9 +806,9 @@ class Colors(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None + primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None class Issuer1(AdCPBaseModel): @@ -803,6 +833,7 @@ class Issuer1(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -828,6 +859,7 @@ class Issuer2(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -864,14 +896,15 @@ class AcceptedVerifier(AdCPBaseModel): extra='forbid', ) agent_url: AnyUrl - claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None - proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None + claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1), NoExplicitNull] = None + proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1), NoExplicitNull] = None ext: Annotated[ dict[str, Any] | None, Field( description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -927,12 +960,14 @@ class Timezone(AdCPBaseModel): description='Seller-wide timezone used by every account. Required only for seller_fixed. Use UTC or an IANA timezone identifier.', min_length=1, ), + NoExplicitNull, ] = None account_selection: Annotated[ AccountSelection | None, Field( description='How an account_fixed timezone is established. seller_assigned covers an existing upstream account or seller onboarding choice; buyer_selected requires timezone in buyer-declared sync_accounts provisioning.' ), + NoExplicitNull, ] = None supported_timezones: Annotated[ list[SupportedTimezone] | None, @@ -940,6 +975,7 @@ class Timezone(AdCPBaseModel): description='Exact timezone values accepted during buyer-selected account provisioning. Required when account_selection is buyer_selected so buyers can validate the choice before sync_accounts.', min_length=1, ), + NoExplicitNull, ] = None @@ -975,6 +1011,7 @@ class Notifications6(AdCPBaseModel): Field( description='Whether `list_accounts` can return recent delivery attempts in `accounts[].webhook_activity[]` when the buyer requests `include_webhook_activity: true`.' ), + NoExplicitNull, ] = False @@ -1068,12 +1105,14 @@ class Account(AdCPBaseModel): Field( description="Whether the seller requires operator-level credentials. This declares who must authenticate; it does not by itself declare whether OAuth is used, whether list_accounts is exposed, or which sync_accounts modes are supported. When true, operators authenticate independently with the seller and account-scoped calls use seller/storefront-assigned account_id values because the seller or upstream platform owns the canonical account namespace. If a credential may access more than one account, the seller MUST expose list_accounts and buyers MUST resolve an explicit account_id before the first account-scoped request. If a credential is bound to exactly one account, the seller SHOULD expose list_accounts returning that singleton; a seller MAY omit list_accounts only when it provides the same explicit account_id through another declared path or out-of-band onboarding. When false (default, buyer-declared accounts), the seller trusts the agent's identity claims and account-scoped calls use the advertiser natural key: brand + operator + optional operator_unit, fixed currency, optional buyer-selected account timezone, and sandbox. operator_unit.id is owned by the operator and is distinct from the seller's account_id. The seller normally provisions through sync_accounts, but MAY lazily provision on the first account-scoped request when billing and other required settings are unambiguous from capabilities or onboarding defaults. A lazy-provisioning seller MUST keep accepting the natural key and MUST expose list_accounts for recovery; if buyer input is needed before use, the seller MUST expose sync_accounts." ), + NoExplicitNull, ] = False authorization_endpoint: Annotated[ AnyUrl | None, Field( description='OAuth authorization endpoint for obtaining operator-level credentials. Present when the seller supports OAuth for operator authentication. The agent directs the operator to this URL to authenticate and obtain a bearer token. If absent and require_operator_auth is true, operators obtain credentials out-of-band (e.g., seller portal, API key).' ), + NoExplicitNull, ] = None supported_billing: Annotated[ list[SupportedBillingEnum], @@ -1088,6 +1127,7 @@ class Account(AdCPBaseModel): description='Required for sellers implementing AdCP 3.2 advertiser-account provisioning, but optional in this shared 3.x response schema so existing 3.0 and 3.1 capability responses remain valid. Declares whether advertiser accounts are bound to one immutable currency (`fixed`), select currency independently per proposal or media buy (`per_media_buy`), or support both models. When only `fixed` is advertised, buyer-declared provisioning entries MUST include `currency`. When only `per_media_buy` is advertised, they MUST omit it. When both are advertised, presence of `currency` selects a fixed-currency account and omission selects per-media-buy currency. Buyers MUST treat absence as an older seller whose currency mode is not discoverable, not as support for either mode.', min_length=1, ), + NoExplicitNull, ] = None timezone: Annotated[ Timezone | None, @@ -1095,42 +1135,49 @@ class Account(AdCPBaseModel): description='Required for sellers implementing AdCP 3.2 advertiser-account provisioning, but optional in the shared 3.x response schema for compatibility. Declares whether the account timezone is seller-wide or fixed per account and whether a buyer must select it during sync_accounts provisioning. Account timezone is the default for account-scoped calendar semantics; feature-specific capability fields explicitly declare exceptions.', title='Account Timezone Capability', ), + NoExplicitNull, ] = None required_for_products: Annotated[ StrictBool | None, Field( description='Whether an account reference is required for get_products. When true, the buyer must establish an account before browsing products. When false (default), the buyer can browse products without an account — useful for price comparison and discovery before committing to a seller. Before an account is provisioned, the buyer omits account rather than sending a natural key that does not resolve yet. See /docs/accounts/overview#account-references-before-provisioning.' ), + NoExplicitNull, ] = False account_financials: Annotated[ StrictBool | None, Field( description='Whether this seller exposes the `get_account_financials` task for querying account-level financial status (spend, credit, invoices). Acts as a **pre-call discriminator** — buyers MUST consult this field before issuing `get_account_financials`; when `false` (or absent), sellers MAY reject the call with an `UNSUPPORTED_FEATURE` / `OPERATION_NOT_SUPPORTED` error. Companion pattern to `creative.bills_through_adcp` (issue #2881) — both fields let buyers gate optional capability calls on a single declared boolean rather than probing for support. Only applicable to operator-billed accounts; sellers using buyer-billed flows omit or set to `false`.' ), + NoExplicitNull, ] = False notifications: Annotated[ Notifications6 | Notifications7 | None, Field( description='Whether the seller supports durable account-lifecycle webhooks through account-level `notification_configs[]`. This capability is specifically for account status changes such as approval, rejection, payment-required, suspension, recovery, and closure. When supported, buyers register subscribers with `sync_accounts.accounts[].notification_configs[]`; each `account.status_changed` fire is an invalidation payload, and buyers repair by re-reading `list_accounts` for the account_id.' ), + NoExplicitNull, ] = None change_feed: Annotated[ ChangeFeed | ChangeFeed3 | None, Field( description='Whether the seller exposes a durable, ordered feed of material changes to authoritative account-scoped state. This is distinct from webhook_activity transport diagnostics and from current-state reads. Sellers claiming support MUST retain changes for at least 90 days after recording and MUST produce records regardless of whether a mutation originated through AdCP, a seller surface, another authorized principal, seller automation, or a connected platform within declared coverage. Experimental in 3.2 (RFC #6810): sellers advertising supported: true MUST list account.change_feed in experimental_features.' ), + NoExplicitNull, ] = None identity_updates: Annotated[ IdentityUpdates | IdentityUpdates3 | None, Field( description='Whether the seller accepts buyer-desired operator identity reconciliation through sync_accounts settings-update entries. Sellers declaring support expose the exact identity transitions they implement, MUST return account revisions from sync_accounts and list_accounts, and MUST return identity_change_preview for dry-run identity updates.' ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='Whether this seller supports sandbox accounts for testing. Buyer-declared accounts use sandbox: true in sync_accounts or, for an unambiguous lazy-provisioning seller, in the natural-key account reference. Sellers with account_id namespaces expose sandbox accounts as pre-existing test accounts through list_accounts or supply them out-of-band. Requests using a sandbox account perform no real platform calls or spend.' ), + NoExplicitNull, ] = False @@ -1159,6 +1206,7 @@ class AcceptancePolicyDiscovery(AdCPBaseModel): description='Local or registry-referenced catalog profiles that apply seller-wide unless a product adds further profiles. IDs MUST resolve uniquely across profiles and registry_profiles; all referenced profiles compose restrictively.', min_length=1, ), + NoExplicitNull, ] = None @@ -1218,6 +1266,7 @@ class ProposalRefinement(AdCPBaseModel): ge=2, le=10, ), + NoExplicitNull, ] = None @@ -1230,6 +1279,7 @@ class PerformanceFeedback(AdCPBaseModel): Field( description='Whether provide_performance_feedback responses include an honest application_status of accepted, applied, or not_applied. accepted is explicitly not an application claim. Sellers that cannot determine optimizer disposition omit this claim or set it false.' ), + NoExplicitNull, ] = False @@ -1287,6 +1337,7 @@ class ReportingProfile(AdCPBaseModel): max_length=128, min_length=1, ), + NoExplicitNull, ] = None canonicalization_contract_version: Literal['1.0'] = '1.0' canonicalization_media_type: Literal['application/vnd.adcp.reporting-canonicalization+json'] = ( @@ -1297,6 +1348,7 @@ class ReportingProfile(AdCPBaseModel): Field( description='Retrievable exact canonicalization contract on the authenticated seller/provider or AdCP-registry origin. SDKs apply the same bounded, redirect-free SSRF controls as schema_uri and verify canonicalization_sha256 before use.' ), + NoExplicitNull, ] = None canonicalization_sha256: Annotated[ str | None, @@ -1304,6 +1356,7 @@ class ReportingProfile(AdCPBaseModel): description='Digest of the exact canonicalization contract identified by canonicalization_id.', pattern='^[A-Fa-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -1347,15 +1400,19 @@ class Schedule(AdCPBaseModel): Field( description='For billing_cycle only. fixed requires the advertised anchor and timezone; configurable lets each authorized account configuration select them.' ), + NoExplicitNull, ] = None period_timezone_policy: Annotated[ PeriodTimezonePolicy | None, Field( description="For source_timezone only. fixed advertises one exact upstream IANA timezone; account_resolved requires the seller to resolve and echo the account's upstream reporting timezone during configuration." ), + NoExplicitNull, ] = None - period_anchor: AwareDatetime | None = None - period_timezone: Annotated[str | None, Field(max_length=255, min_length=1)] = None + period_anchor: Annotated[AwareDatetime | None, NoExplicitNull] = None + period_timezone: Annotated[str | None, Field(max_length=255, min_length=1), NoExplicitNull] = ( + None + ) delivery_sla: Annotated[ str, Field( @@ -1405,8 +1462,8 @@ class ProducerIdentity(AdCPBaseModel): ) provider: Provider identity: Annotated[str, Field(max_length=512, min_length=1)] - cloud: Annotated[Cloud | None, Field(title='Reporting Cloud')] = None - region: Annotated[str | None, Field(max_length=128, min_length=1)] = None + cloud: Annotated[Cloud | None, Field(title='Reporting Cloud'), NoExplicitNull] = None + region: Annotated[str | None, Field(max_length=128, min_length=1), NoExplicitNull] = None class ReaderCompatibilityItem(ScalarStr): @@ -1425,6 +1482,7 @@ class OperationsContact(AdCPBaseModel): Field( description='HTTPS page a human uses to open or track a reporting issue, such as a support portal or status page. Same hardened origin shape as the offering document URIs: never an IP literal, userinfo URL, loopback host, AdCP task endpoint, webhook target, or credentialed link.' ), + NoExplicitNull, ] = None email: Annotated[ EmailStr | None, @@ -1432,6 +1490,7 @@ class OperationsContact(AdCPBaseModel): description='Monitored operations mailbox for reporting escalations. A role address, not an individual.', max_length=254, ), + NoExplicitNull, ] = None @@ -1458,8 +1517,10 @@ class Evidence(AdCPBaseModel): ) basis: Basis generated_at: AwareDatetime - attestation_uri: AnyUrl | None = None - attestation_sha256: Annotated[str | None, Field(pattern='^[A-Fa-f0-9]{64}$')] = None + attestation_uri: Annotated[AnyUrl | None, NoExplicitNull] = None + attestation_sha256: Annotated[ + str | None, Field(pattern='^[A-Fa-f0-9]{64}$'), NoExplicitNull + ] = None class PropagationSurface(StrEnum): @@ -1525,12 +1586,14 @@ class RelationshipNotifications(AdCPBaseModel): max_length=1, min_length=1, ), + NoExplicitNull, ] = None supports_webhook_activity: Annotated[ StrictBool | None, Field( description='Whether the declared repair tasks can return recent delivery attempts for these account-level invalidations when include_webhook_activity is true.' ), + NoExplicitNull, ] = False @@ -1549,7 +1612,9 @@ class Surface(StrEnum): class TrustedMatch(AdCPBaseModel): surfaces: Annotated[ - list[Surface] | None, Field(description='Surface types this seller supports via TMP.') + list[Surface] | None, + Field(description='Surface types this seller supports via TMP.'), + NoExplicitNull, ] = None @@ -1648,18 +1713,21 @@ class GeoRegions(AdCPBaseModel): description='Optional opaque ISO subdivision catalog or seller-mapping version that identifies the support snapshot. It is provenance, not part of subdivision identity.', min_length=1, ), + NoExplicitNull, ] = None as_of: Annotated[ date | None, Field( description='Optional date on which the seller last validated this support declaration.' ), + NoExplicitNull, ] = None max_values_per_package: Annotated[ SchemaInt | None, Field( description='Optional maximum number of region values selectable on one package.', ge=1 ), + NoExplicitNull, ] = None max_packages: Annotated[ SchemaInt | None, @@ -1667,6 +1735,7 @@ class GeoRegions(AdCPBaseModel): description='Optional maximum number of independently region-targeted packages created from this configured product.', ge=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -1674,6 +1743,7 @@ class GeoRegions(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -1702,18 +1772,21 @@ class GeoRegionsExclude(AdCPBaseModel): description='Optional opaque ISO subdivision catalog or seller-mapping version that identifies the support snapshot. It is provenance, not part of subdivision identity.', min_length=1, ), + NoExplicitNull, ] = None as_of: Annotated[ date | None, Field( description='Optional date on which the seller last validated this support declaration.' ), + NoExplicitNull, ] = None max_values_per_package: Annotated[ SchemaInt | None, Field( description='Optional maximum number of region values selectable on one package.', ge=1 ), + NoExplicitNull, ] = None max_packages: Annotated[ SchemaInt | None, @@ -1721,6 +1794,7 @@ class GeoRegionsExclude(AdCPBaseModel): description='Optional maximum number of independently region-targeted packages created from this configured product.', ge=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -1728,6 +1802,7 @@ class GeoRegionsExclude(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -1735,10 +1810,10 @@ class GeoMetros(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - nielsen_dma: StrictBool | None = None - uk_itl1: StrictBool | None = None - uk_itl2: StrictBool | None = None - eurostat_nuts2: StrictBool | None = None + nielsen_dma: Annotated[StrictBool | None, NoExplicitNull] = None + uk_itl1: Annotated[StrictBool | None, NoExplicitNull] = None + uk_itl2: Annotated[StrictBool | None, NoExplicitNull] = None + eurostat_nuts2: Annotated[StrictBool | None, NoExplicitNull] = None class ME(StrEnum): @@ -1768,28 +1843,28 @@ class GeoPostalAreas(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - US: Annotated[list[ME] | None, Field(min_length=1)] = None - GB: Annotated[list[GBEnum] | None, Field(min_length=1)] = None - CA: Annotated[list[CAEnum] | None, Field(min_length=1)] = None - DE: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - CH: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - AT: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - FR: Annotated[list[Literal['code_postal']] | None, Field(min_length=1)] = None - AU: Annotated[list[Literal['postcode']] | None, Field(min_length=1)] = None - BR: Annotated[list[Literal['cep']] | None, Field(min_length=1)] = None - IN: Annotated[list[Literal['pin']] | None, Field(min_length=1)] = None - ZA: Annotated[list[Literal['postal_code']] | None, Field(min_length=1)] = None - us_zip: Annotated[StrictBool | None, Field(deprecated=True)] = None - us_zip_plus_four: Annotated[StrictBool | None, Field(deprecated=True)] = None - gb_outward: Annotated[StrictBool | None, Field(deprecated=True)] = None - gb_full: Annotated[StrictBool | None, Field(deprecated=True)] = None - ca_fsa: Annotated[StrictBool | None, Field(deprecated=True)] = None - ca_full: Annotated[StrictBool | None, Field(deprecated=True)] = None - de_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None - fr_code_postal: Annotated[StrictBool | None, Field(deprecated=True)] = None - au_postcode: Annotated[StrictBool | None, Field(deprecated=True)] = None - ch_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None - at_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None + US: Annotated[list[ME] | None, Field(min_length=1), NoExplicitNull] = None + GB: Annotated[list[GBEnum] | None, Field(min_length=1), NoExplicitNull] = None + CA: Annotated[list[CAEnum] | None, Field(min_length=1), NoExplicitNull] = None + DE: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + CH: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + AT: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + FR: Annotated[list[Literal['code_postal']] | None, Field(min_length=1), NoExplicitNull] = None + AU: Annotated[list[Literal['postcode']] | None, Field(min_length=1), NoExplicitNull] = None + BR: Annotated[list[Literal['cep']] | None, Field(min_length=1), NoExplicitNull] = None + IN: Annotated[list[Literal['pin']] | None, Field(min_length=1), NoExplicitNull] = None + ZA: Annotated[list[Literal['postal_code']] | None, Field(min_length=1), NoExplicitNull] = None + us_zip: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + us_zip_plus_four: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + gb_outward: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + gb_full: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ca_fsa: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ca_full: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + de_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + fr_code_postal: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + au_postcode: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ch_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + at_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None @@ -1873,6 +1948,7 @@ class Catalog(AdCPBaseModel): Field( description='Optional catalog dataset or derivative-source identifier. This records provenance/coverage and does not change the identifier namespace in system.' ), + NoExplicitNull, ] = None current_version: Annotated[ str, @@ -1949,11 +2025,14 @@ class VerificationMethod(StrEnum): class AgeRestriction(AdCPBaseModel): supported: Annotated[ - StrictBool | None, Field(description='Whether seller supports age restrictions') + StrictBool | None, + Field(description='Whether seller supports age restrictions'), + NoExplicitNull, ] = None verification_methods: Annotated[ list[VerificationMethod] | None, Field(description='Age verification methods this seller supports'), + NoExplicitNull, ] = None @@ -1987,12 +2066,14 @@ class Language(AdCPBaseModel): supported: Annotated[ StrictBool | None, Field(description='Whether seller supports language-preference targeting'), + NoExplicitNull, ] = None supported_languages: Annotated[ list[SupportedLanguage] | None, Field( description='Exact canonical BCP 47 language ranges accepted in targeting.language. Omission means the seller makes no exhaustive language-list declaration.' ), + NoExplicitNull, ] = None @@ -2009,18 +2090,21 @@ class GeoProximity(AdCPBaseModel): Field( description='Whether seller supports simple radius targeting (distance circle from a point)' ), + NoExplicitNull, ] = None travel_time: Annotated[ StrictBool | None, Field( description='Whether seller supports travel time isochrone targeting (requires a routing engine)' ), + NoExplicitNull, ] = None geometry: Annotated[ StrictBool | None, Field( description='Whether seller supports pre-computed GeoJSON geometry (buyer provides the polygon)' ), + NoExplicitNull, ] = None transport_modes: Annotated[ list[TransportMode] | None, @@ -2028,6 +2112,7 @@ class GeoProximity(AdCPBaseModel): description='Transport modes supported for travel_time isochrones. Only relevant when travel_time is true.', min_length=1, ), + NoExplicitNull, ] = None @@ -2123,13 +2208,16 @@ class ConsumerIdentity(AdCPBaseModel): extra='forbid', ) cloud: Annotated[ - Cloud | None, Field(description="Cloud family hosting this identity's account.") + Cloud | None, + Field(description="Cloud family hosting this identity's account."), + NoExplicitNull, ] = None region: Annotated[ str | None, Field( description="Vendor/cloud region identifier. Format follows the cloud family's region naming; not constrained by this schema." ), + NoExplicitNull, ] = None identity: Annotated[ str, @@ -2144,8 +2232,8 @@ class MatchingLatencyHours(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[SchemaInt | None, Field(ge=0)] = None - max: Annotated[SchemaInt | None, Field(ge=0)] = None + min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class SupportedOptimizationMetric(StrEnum): @@ -2178,6 +2266,7 @@ class VendorMetricOptimization(AdCPBaseModel): description='Target kinds this seller can support for vendor_metric optimization goals on at least one product. Values match optimization_goals[].target.kind for kind: vendor_metric. A target-less vendor_metric goal maximizes the metric within budget and does not require a target-kind declaration.', min_length=1, ), + NoExplicitNull, ] = None @@ -2263,6 +2352,7 @@ class Features(AdCPBaseModel): deprecated=True, description='DEPRECATED. Legacy wire flag for structured signal_ref references to provider-published signal definitions. New agents SHOULD omit this flag; callers MUST NOT require it before using signal_ref with the Signals protocol.', ), + NoExplicitNull, ] = None @@ -2274,6 +2364,7 @@ class Signals(AdCPBaseModel): Field( description='Whether this agent accepts get_signals discovery without caller credentials in brief or wholesale mode. true means an anonymous discovery request can produce a successful response, but the response may be a public subset and may differ from results for an authenticated principal or selected account. false means get_signals requires an authenticated principal. Absence means unspecified legacy behavior, so callers probe and handle AUTH_MISSING. A valid authenticated request is never rejected merely because credentials were supplied.' ), + NoExplicitNull, ] = None data_provider_domains: Annotated[ list[DataProviderDomain] | None, @@ -2281,6 +2372,7 @@ class Signals(AdCPBaseModel): description="Data provider domains this signals agent is authorized to resell. Buyers should fetch each data provider's adagents.json for published signal definitions and to verify authorization.", min_length=1, ), + NoExplicitNull, ] = None discovery_modes: Annotated[ list[DiscoveryMode] | None, @@ -2288,9 +2380,10 @@ class Signals(AdCPBaseModel): description="Discovery modes this signals agent supports on get_signals. 'brief' (default — every signals agent supports this): semantic discovery driven by signal_spec or signal_refs, with deprecated signal_ids accepted for older clients. 'wholesale': raw wholesale signals feed enumeration — caller omits signal_spec, signal_refs, and signal_ids and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries. Agents that do not declare 'wholesale' MAY return INVALID_REQUEST for wholesale calls. Absent declaration is treated as ['brief'].", min_length=1, ), + NoExplicitNull, ] = [DiscoveryMode.brief] features: Annotated[ - Features | None, Field(description='Optional signals features supported') + Features | None, Field(description='Optional signals features supported'), NoExplicitNull ] = None @@ -2345,19 +2438,26 @@ class PropertyFeature(AdCPBaseModel): ), ] range: Annotated[ - Range | None, Field(description='For quantitative features, the valid range') + Range | None, + Field(description='For quantitative features, the valid range'), + NoExplicitNull, ] = None categories: Annotated[ - list[str] | None, Field(description='For categorical features, the valid values') + list[str] | None, + Field(description='For categorical features, the valid values'), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of what this feature measures') + str | None, + Field(description='Human-readable description of what this feature measures'), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='URL to documentation explaining how this feature is calculated or measured. Helps buyers understand and compare methodologies across vendors.' ), + NoExplicitNull, ] = None @@ -2375,19 +2475,26 @@ class CreativeFeature(AdCPBaseModel): ), ] range: Annotated[ - Range | None, Field(description='For quantitative features, the valid range') + Range | None, + Field(description='For quantitative features, the valid range'), + NoExplicitNull, ] = None categories: Annotated[ - list[str] | None, Field(description='For categorical features, the valid values') + list[str] | None, + Field(description='For categorical features, the valid values'), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of what this feature measures') + str | None, + Field(description='Human-readable description of what this feature measures'), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='URL to documentation explaining how this feature is calculated or measured.' ), + NoExplicitNull, ] = None @@ -2397,6 +2504,7 @@ class Governance(AdCPBaseModel): Field( description='Signal-activation policy for the portable attestation support declared in adcp.attestations. Presence means check_governance accepts runtime_attestations[] for purchase_type signal_activation. The claim_types list MUST be a subset of adcp.attestations.accepted_claim_types; issuer, resolver, verifier, proof-format, and delivery allowlists remain authoritative in the shared adcp.attestations block rather than being duplicated here.' ), + NoExplicitNull, ] = None aggregation_window_days: Annotated[ SchemaInt | None, @@ -2405,18 +2513,21 @@ class Governance(AdCPBaseModel): ge=1, le=365, ), + NoExplicitNull, ] = None property_features: Annotated[ list[PropertyFeature] | None, Field( description='Property features this governance agent can evaluate. Each feature describes a score, rating, or certification the agent can provide for properties.' ), + NoExplicitNull, ] = None creative_features: Annotated[ list[CreativeFeature] | None, Field( description='Creative features this governance agent can evaluate. Each feature describes a score, rating, or assessment the agent can provide for creatives (e.g., security scanning, creative quality, content categorization).' ), + NoExplicitNull, ] = None @@ -2442,7 +2553,9 @@ class Endpoint(AdCPBaseModel): ), ] preferred: Annotated[ - Type9 | None, Field(description='Preferred transport when host supports multiple') + Type9 | None, + Field(description='Preferred transport when host supports multiple'), + NoExplicitNull, ] = None @@ -2451,9 +2564,11 @@ class Voice(AdCPBaseModel): extra='allow', ) provider: Annotated[ - str | None, Field(description='TTS provider (elevenlabs, openai, etc.)') + str | None, Field(description='TTS provider (elevenlabs, openai, etc.)'), NoExplicitNull ] = None - voice_id: Annotated[str | None, Field(description='Brand voice identifier')] = None + voice_id: Annotated[str | None, Field(description='Brand voice identifier'), NoExplicitNull] = ( + None + ) class Video(AdCPBaseModel): @@ -2461,10 +2576,12 @@ class Video(AdCPBaseModel): extra='allow', ) formats: Annotated[ - list[str] | None, Field(description='Supported video formats (mp4, webm, etc.)') + list[str] | None, + Field(description='Supported video formats (mp4, webm, etc.)'), + NoExplicitNull, ] = None max_duration_seconds: Annotated[ - SchemaInt | None, Field(description='Maximum video duration') + SchemaInt | None, Field(description='Maximum video duration'), NoExplicitNull ] = None @@ -2473,9 +2590,13 @@ class Avatar(AdCPBaseModel): extra='allow', ) provider: Annotated[ - str | None, Field(description='Avatar provider (d-id, heygen, synthesia, etc.)') + str | None, + Field(description='Avatar provider (d-id, heygen, synthesia, etc.)'), + NoExplicitNull, + ] = None + avatar_id: Annotated[ + str | None, Field(description='Brand avatar identifier'), NoExplicitNull ] = None - avatar_id: Annotated[str | None, Field(description='Brand avatar identifier')] = None class Modalities(AdCPBaseModel): @@ -2483,16 +2604,22 @@ class Modalities(AdCPBaseModel): extra='allow', ) conversational: Annotated[ - StrictBool | None, Field(description='Pure text exchange - the baseline modality') + StrictBool | None, + Field(description='Pure text exchange - the baseline modality'), + NoExplicitNull, ] = True voice: Annotated[ - StrictBool | Voice | None, Field(description='Audio-based interaction using brand voice') + StrictBool | Voice | None, + Field(description='Audio-based interaction using brand voice'), + NoExplicitNull, ] = None video: Annotated[ - StrictBool | Video | None, Field(description='Brand video content playback') + StrictBool | Video | None, Field(description='Brand video content playback'), NoExplicitNull ] = None avatar: Annotated[ - StrictBool | Avatar | None, Field(description='Animated video presence with brand avatar') + StrictBool | Avatar | None, + Field(description='Animated video presence with brand avatar'), + NoExplicitNull, ] = None @@ -2512,10 +2639,12 @@ class Components(AdCPBaseModel): standard: Annotated[ list[StandardEnum] | None, Field(description='Standard components that all SI hosts must render'), + NoExplicitNull, ] = None extensions: Annotated[ dict[str, Any] | None, Field(description='Platform-specific extensions (chatgpt_apps_sdk, maps, forms, etc.)'), + NoExplicitNull, ] = None @@ -2526,6 +2655,7 @@ class Commerce(AdCPBaseModel): acp_checkout: Annotated[ StrictBool | None, Field(description='Supports ACP (Agentic Commerce Protocol) checkout handoff'), + NoExplicitNull, ] = None @@ -2534,11 +2664,12 @@ class A2ui(AdCPBaseModel): extra='allow', ) supported: Annotated[ - StrictBool | None, Field(description='Supports A2UI surface rendering') + StrictBool | None, Field(description='Supports A2UI surface rendering'), NoExplicitNull ] = False catalogs: Annotated[ list[str] | None, Field(description="Supported A2UI component catalogs (e.g., 'si-standard', 'standard')"), + NoExplicitNull, ] = None @@ -2547,16 +2678,21 @@ class Capabilities(AdCPBaseModel): extra='allow', ) modalities: Annotated[ - Modalities | None, Field(description='Interaction modalities supported') + Modalities | None, Field(description='Interaction modalities supported'), NoExplicitNull + ] = None + components: Annotated[ + Components | None, Field(description='Visual components supported'), NoExplicitNull + ] = None + commerce: Annotated[ + Commerce | None, Field(description='Commerce capabilities'), NoExplicitNull + ] = None + a2ui: Annotated[ + A2ui | None, Field(description='A2UI (Agent-to-UI) capabilities'), NoExplicitNull ] = None - components: Annotated[Components | None, Field(description='Visual components supported')] = ( - None - ) - commerce: Annotated[Commerce | None, Field(description='Commerce capabilities')] = None - a2ui: Annotated[A2ui | None, Field(description='A2UI (Agent-to-UI) capabilities')] = None mcp_apps: Annotated[ StrictBool | None, Field(description='Supports MCP Apps for rendering A2UI surfaces in iframes'), + NoExplicitNull, ] = False @@ -2569,7 +2705,9 @@ class SponsoredIntelligence(AdCPBaseModel): ), ] brand_url: Annotated[ - AnyUrl | None, Field(description='URL to brand.json with colors, fonts, logos, tone') + AnyUrl | None, + Field(description='URL to brand.json with colors, fonts, logos, tone'), + NoExplicitNull, ] = None @@ -2605,23 +2743,29 @@ class Brand(AdCPBaseModel): Field( description='Supports get_rights and acquire_rights for rights discovery and clearance' ), + NoExplicitNull, ] = False right_types: Annotated[ - list[RightType] | None, Field(description='Types of rights available through this agent') + list[RightType] | None, + Field(description='Types of rights available through this agent'), + NoExplicitNull, ] = None available_uses: Annotated[ list[AvailableUs] | None, Field(description="Rights uses available across this agent's roster"), + NoExplicitNull, ] = None generation_providers: Annotated[ list[str] | None, Field(description='LLM/generation providers this agent can issue credentials for'), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description="Description of the agent's brand protocol capabilities", max_length=5000 ), + NoExplicitNull, ] = None @@ -2675,16 +2819,19 @@ class Multiplicity(AdCPBaseModel): Field( description='When true, build_creative accepts max_creatives (one distinct creative per catalog item).' ), + NoExplicitNull, ] = False max_creatives_limit: Annotated[ SchemaInt | None, Field(description='Ceiling on max_creatives. Omitted means no advertised ceiling.', ge=1), + NoExplicitNull, ] = None supports_signal_fanout: Annotated[ StrictBool | None, Field( description='Experimental (x-status: experimental) — agents setting this true MUST also list `creative.signal_fanout` in `experimental_features`; the surface MAY change between 3.x releases with notice (see docs/reference/experimental-status). When true, build_creative accepts signal_conditions[] (one distinct creative group per signal condition, keep-all). Mirrors supports_catalog_fanout.' ), + NoExplicitNull, ] = False max_signal_conditions_limit: Annotated[ SchemaInt | None, @@ -2692,26 +2839,31 @@ class Multiplicity(AdCPBaseModel): description='Ceiling on len(signal_conditions). Over-limit requests are CLAMPED (not rejected), like max_creatives_limit. Omitted means no advertised ceiling.', ge=1, ), + NoExplicitNull, ] = None supports_variants: Annotated[ StrictBool | None, Field( description='When true, build_creative accepts max_variants > 1 / variant_axis (alternatives per creative).' ), + NoExplicitNull, ] = False max_variants_limit: Annotated[ SchemaInt | None, Field(description='Ceiling on max_variants. Omitted means no advertised ceiling.', ge=1), + NoExplicitNull, ] = None variant_dimensions: Annotated[ list[VariantDimension] | None, Field(description='Which variant_axis.dimension values this agent supports.'), + NoExplicitNull, ] = None selection_strategies: Annotated[ list[SelectionStrategy] | None, Field( description='Which selection_strategy values this agent supports when sampling max_creatives < items_total. Sibling to variant_dimensions. Part of the experimental signal-fanout surface (feature id `creative.signal_fanout`).' ), + NoExplicitNull, ] = None @@ -2770,6 +2922,7 @@ class V1FormatRefItem(AdCPBaseModel): description='Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -2777,6 +2930,7 @@ class V1FormatRefItem(AdCPBaseModel): description='Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.', ge=1, ), + NoExplicitNull, ] = None duration_ms: Annotated[ StrictFloat | None, @@ -2784,6 +2938,7 @@ class V1FormatRefItem(AdCPBaseModel): description='Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.', ge=1.0, ), + NoExplicitNull, ] = None pixel_ratio: Annotated[ StrictFloat | None, @@ -2791,6 +2946,7 @@ class V1FormatRefItem(AdCPBaseModel): description='Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: "display_image", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.', gt=0.0, ), + NoExplicitNull, ] = None @@ -2886,6 +3042,7 @@ class Localization(AdCPBaseModel): ge=0, le=50, ), + NoExplicitNull, ] = None locale_matching: Annotated[ Literal['rfc4647_lookup'], @@ -2952,6 +3109,7 @@ class RequestSigning(AdCPBaseModel): Field( description="Policy for content-digest coverage in request signatures. In AdCP 3.2 and later, an agent with request_signing.supported=true MUST explicitly emit 'required': every accepted signature on a request with a body covers content-digest, and a body-unbound signature is rejected with request_signature_components_incomplete. Omission retains the legacy effective default of 'either' only for a 3.0/3.1 response; 3.2 responses require this field explicitly. 'either' and 'forbidden' are deprecated legacy 3.0/3.1 postures retained only for version negotiation with pre-3.2 peers; they MUST NOT be advertised as a 3.2 signing posture and are removed in 4.0. A shared endpoint MUST select the verifier policy from trusted endpoint configuration and negotiated capabilities before dispatch, never from an unbound request-body version field." ), + NoExplicitNull, ] = None required_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, @@ -2959,6 +3117,7 @@ class RequestSigning(AdCPBaseModel): description="AdCP protocol operation names (e.g., 'create_media_buy') for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. A transport-specific rename of an operation is not a valid entry (MCP tools and the AdCP A2A profile use the operation name verbatim, so those names coincide with it) — verifiers MUST NOT accept operation names that are not defined by the AdCP protocol spec. Over MCP the verifier matches the tools/call params.name; over A2A it matches the skill of the sole DataPart of the message (SendMessage, SendStreamingMessage, message/send, message/stream, and the HTTP+JSON and gRPC equivalents). The A2A profile uses the AdCP operation name verbatim as skill, so that is the same name space. A request that does not resolve to exactly one operation is rejected with request_body_malformed; see docs/building/by-layer/L1/security.mdx#operation-resolution-over-a2a. JSON-RPC protocol method names like `tasks/cancel` belong in `protocol_methods_required_for`, not here. Empty in 3.0 by default; sellers populate selectively during per-counterparty pilots. In 4.0 this list MUST include all spend-committing operations the agent supports (create_media_buy, acquire_*, etc.). Every operation listed MUST also appear in `supported_for`; see `x-adcp-validation`.", validate_default=True, ), + NoExplicitNull, ] = [] warn_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, @@ -2966,12 +3125,14 @@ class RequestSigning(AdCPBaseModel): description='AdCP protocol operation names for shadow-mode verification. The verifier records missing signatures and well-formed signatures that fail verification or body binding, but MUST NOT establish verified-signer identity from a failed signature; processing continues only when an independent bearer, API-key, or mTLS authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. Used as a bridge between supported_for and required_for. Precedence: required_for > warn_for > supported_for. An operation MUST NOT appear in both warn_for and required_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] supported_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, Field( description='AdCP protocol operation names for which this agent verifies signatures when present but does not require them. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects even though an unsigned request may use configured fallback authentication. Typically a superset of required_for and warn_for.' ), + NoExplicitNull, ] = None protocol_methods_supported_for: Annotated[ list[ProtocolMethodsSupportedForItem] | None, @@ -2979,6 +3140,7 @@ class RequestSigning(AdCPBaseModel): description="JSON-RPC protocol method names for which this agent verifies signatures when present. Values MUST use exact, case-sensitive equality after JSON decoding: slash-path names such as 'tasks/cancel' and 'tasks/pushNotificationConfig/set' for A2A 0.3, or PascalCase names such as 'CancelTask' and 'CreateTaskPushNotificationConfig' for A2A 1.0. A dual-stack agent lists each supported wire name independently; implementations MUST NOT translate or normalize between protocol versions. The reserved MCP envelope method 'tools/call' is forbidden because its AdCP operation identity is params.name and belongs in supported_for. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects. Disjoint from supported_for, which carries lower_snake_case AdCP operation names only.", validate_default=True, ), + NoExplicitNull, ] = [] protocol_methods_warn_for: Annotated[ list[ProtocolMethodsWarnForItem] | None, @@ -2986,6 +3148,7 @@ class RequestSigning(AdCPBaseModel): description='Exact JSON-RPC protocol method names for shadow-mode verification, mirroring warn_for in the AdCP-operation namespace. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Missing signatures and well-formed signatures that fail verification or body binding are recorded but MUST NOT establish verified-signer identity; processing continues only when an independent authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. An item MUST NOT appear in both protocol_methods_warn_for and protocol_methods_required_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] protocol_methods_required_for: Annotated[ list[ProtocolMethodsRequiredForItem] | None, @@ -2993,6 +3156,7 @@ class RequestSigning(AdCPBaseModel): description='Exact JSON-RPC protocol method names for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Separate namespace from required_for: this bucket binds against the JSON-RPC method field, not tools/call params.name. Every listed method MUST also appear in protocol_methods_supported_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] @@ -3013,6 +3177,7 @@ class WebhookSigning(AdCPBaseModel): Field( description='Identifier of the webhook-signing profile version the agent emits. Value MUST match the `tag=` parameter emitted in the RFC 9421 `Signature-Input` header (see docs/building/implementation/webhooks.mdx) so receivers can statically validate the declared profile against the on-wire tag. Closed enum; future profile revisions will extend this enum in a follow-up schema bump.' ), + NoExplicitNull, ] = None algorithms: Annotated[ list[Algorithm] | None, @@ -3020,6 +3185,7 @@ class WebhookSigning(AdCPBaseModel): description="Signature algorithms this agent uses on outbound webhooks. 3.0 profile permits 'ed25519' and 'ecdsa-p256-sha256' only; other values are reserved for future profile versions and MUST NOT be emitted under adcp/webhook-signing/v1.", min_length=1, ), + NoExplicitNull, ] = None legacy_hmac_fallback: Annotated[ StrictBool | None, @@ -3027,6 +3193,7 @@ class WebhookSigning(AdCPBaseModel): deprecated=True, description='Whether this agent will fall back to HMAC-SHA256 on the legacy push_notification_config.authentication, accounts[].notification_configs[].authentication, sync_principal.configuration.notification_configs[].authentication, or sync_agent_notification_configs.notification_configs[].authentication paths for receivers that have not adopted RFC 9421. Deprecated; removed in AdCP 4.0.', ), + NoExplicitNull, ] = False delivery_retry_horizon_seconds: Annotated[ SchemaInt | None, @@ -3035,6 +3202,7 @@ class WebhookSigning(AdCPBaseModel): ge=86400, le=604800, ), + NoExplicitNull, ] = None @@ -3045,21 +3213,26 @@ class KeyOrigins(AdCPBaseModel): governance_signing: Annotated[ AnyUrl | None, Field(description='Origin (scheme + host) serving the governance-signing JWKS.'), + NoExplicitNull, ] = None request_signing: Annotated[ - AnyUrl | None, Field(description='Origin (scheme + host) serving the request-signing JWKS.') + AnyUrl | None, + Field(description='Origin (scheme + host) serving the request-signing JWKS.'), + NoExplicitNull, ] = None webhook_signing: Annotated[ AnyUrl | None, Field( description='Origin (scheme + host) serving the JWKS used for webhook delivery. Webhooks are signed with `adcp_use: "request-signing"` keys; the deprecated `adcp_use: "webhook-signing"` value remains accepted during the backward-compatibility window.' ), + NoExplicitNull, ] = None tmp_signing: Annotated[ AnyUrl | None, Field( description='Origin (scheme + host) serving the TMP-signing JWKS, when this operator participates in TMP.' ), + NoExplicitNull, ] = None @@ -3070,12 +3243,14 @@ class CompromiseNotification(AdCPBaseModel): emits: Annotated[ StrictBool | None, Field(description='Whether this agent emits `identity.compromise_notification` events.'), + NoExplicitNull, ] = False accepts: Annotated[ StrictBool | None, Field( description='Whether this agent subscribes to `identity.compromise_notification` events from counterparties it verifies signatures from.' ), + NoExplicitNull, ] = False @@ -3088,24 +3263,28 @@ class Identity(AdCPBaseModel): Field( description="HTTPS URL of the operator's brand.json (typically `https://{operator-domain}/.well-known/brand.json`). Trust-root pointer for this agent's signing keys. See [security.mdx §Discovering an agent's signing keys via `brand_json_url`](https://adcontextprotocol.org/docs/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url) for the verifier algorithm and `x-adcp-validation` for structured constraints. Distinct from `sponsored_intelligence.brand_url`, which is a rendering pointer for SI agent visuals — verifiers MUST use this field for key discovery and MUST NOT fall back to `sponsored_intelligence.brand_url` as a trust-root pointer." ), + NoExplicitNull, ] = None per_principal_key_isolation: Annotated[ StrictBool | None, Field( description="When true, this multi-principal operator scopes signing keys per-principal so a single principal's key compromise does not silently re-scope across principals served by the same operator. `kid` values remain opaque to verifiers per RFC 7517; any operator-side naming convention (e.g., `{operator}:{principal}:{key_version}`) is internal bookkeeping and MUST NOT be parsed by verifiers. See docs/building/understanding/security-model.mdx." ), + NoExplicitNull, ] = False key_origins: Annotated[ KeyOrigins | None, Field( description='Map of signing-key surface/purpose → publishing origin, so counterparties can verify origin separation (e.g., governance keys served from a separate origin than transport/webhook keys) at onboarding. Absent means the operator has not declared a separation scheme; receivers SHOULD assume shared-origin. Every entry listed MUST have a corresponding signing posture declared elsewhere — `request_signing` requires non-empty `request_signing.supported_for`/`required_for`/`protocol_methods_supported_for`/`protocol_methods_required_for`; `webhook_signing` requires `webhook_signing.supported === true` and names the webhook delivery surface, not a required live `adcp_use: "webhook-signing"` key purpose — otherwise the consistency check at signature-verification time has nothing to anchor against. See `x-adcp-validation` and docs/building/by-layer/L1/security.mdx §Origin separation.' ), + NoExplicitNull, ] = None compromise_notification: Annotated[ CompromiseNotification | None, Field( description='Whether this agent emits the `identity.compromise_notification` webhook event on key revocation due to known or suspected compromise (as opposed to scheduled rotation). Subscribers use this to bound the window between compromise detected and verifiers converging on revocation. See docs/building/implementation/webhooks.mdx §identity.compromise_notification.' ), + NoExplicitNull, ] = None @@ -3143,18 +3322,21 @@ class Accreditation(AdCPBaseModel): Field( description="Optional identifier for the certification in the accrediting body's records (when one exists; many bodies do not issue stable IDs)." ), + NoExplicitNull, ] = None valid_until: Annotated[ date | None, Field( description="Optional ISO 8601 date when the current accreditation expires. Buyers MAY treat post-expiry data as un-accredited. Absence means the vendor does not assert an expiry — buyers SHOULD verify currency at the accrediting body's directory." ), + NoExplicitNull, ] = None evidence_url: Annotated[ AnyUrl | None, Field( description="Optional URL pointing at the accrediting body's public listing for this certification (the buyer's path to verify the claim independently)." ), + NoExplicitNull, ] = None @@ -3184,12 +3366,14 @@ class Metric(AdCPBaseModel): Field( description='Optional URI pointing at the published standard this metric IMPLEMENTS (e.g., IAB Attention Measurement Guidelines, MRC Viewable Impression Measurement, GARM emissions framework). Distinct from `accreditations[]` — `standard_reference` is what the metric is built against; `accreditations[]` is third-party certification that the implementation actually conforms. Buyer agents normalizing across vendors SHOULD apply the AdCP URL canonicalization rules before comparing — vendors implementing the same standard MAY use different URL forms for the same canonical document.' ), + NoExplicitNull, ] = None accreditations: Annotated[ list[Accreditation] | None, Field( description="Third-party accreditations this metric holds (MRC, ARF, JIC, ABC, BARB, AGOF, etc.). Distinct from `standard_reference`: a metric can implement a standard without being independently accredited. Buyers asking 'is this MRC-accredited?' SHOULD check this array, not just `standard_reference`. Each entry names the accrediting body and optionally pins a certification ID, validity date, and evidence URL." ), + NoExplicitNull, ] = None unit: Annotated[ str | None, @@ -3197,18 +3381,21 @@ class Metric(AdCPBaseModel): description='Unit of the metric value when reported via `vendor_metric_values.value` (e.g., `score`, `seconds`, `persons`, `gCO2e`, `lift_percent`, `USD`). Buyers SHOULD render the unit alongside the value rather than computing units locally; sellers populating `vendor_metric_values.unit` MUST match this declaration when present.', examples=['score', 'seconds', 'persons', 'gCO2e', 'lift_percent', 'index', 'USD'], ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Human-readable description of what this metric measures and any relevant methodology notes. Surfaced in buyer-agent UX when explaining the metric to humans.' ), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description="URL to the vendor's full methodology documentation for this metric. Buyers SHOULD link or fetch this when human review of the methodology is in scope (compliance, RFP review, accreditation audit). Field name mirrors `governance.property_features[].methodology_url`." ), + NoExplicitNull, ] = None methodology_version: Annotated[ str | None, @@ -3216,6 +3403,7 @@ class Metric(AdCPBaseModel): description='Optional version identifier (semver, ISO date, or vendor-defined version string) for the methodology this metric currently implements. When present, buyer agents pin the contracted version via `committed_metrics[].methodology_version` (vendor scope) so silent vendor methodology changes are detectable; absence means the vendor does not version their methodology and buyers MUST treat any change as untracked.', examples=['v2.1', '2026-Q1', '1.0'], ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -3223,6 +3411,7 @@ class Metric(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -3232,6 +3421,7 @@ class Measurement(AdCPBaseModel): Field( description="Whether this measurement agent produces compact provide_performance_feedback assertions for a buyer-controlled orchestrator gateway. In the first experimental tier the provider reads delivery through the gateway's get_media_buy_delivery task and returns assertions through its provide_performance_feedback task. The gateway decides what to forward to each seller." ), + NoExplicitNull, ] = False metrics: Annotated[ list[Metric], @@ -3323,12 +3513,14 @@ class WholesaleFeedVersioning(AdCPBaseModel): Field( description="Whether the agent tracks pricing_version independently of wholesale_feed_version. When true, the agent returns both tokens and honors if_pricing_version separately — useful for rate-card sweeps that don't change product and signal metadata. When false or absent, the agent collapses both into wholesale_feed_version; callers SHOULD NOT send if_pricing_version (it will be ignored and may produce INVALID_REQUEST when sent without if_wholesale_feed_version per the dependencies rule)." ), + NoExplicitNull, ] = None cache_scope_account: Annotated[ StrictBool | None, Field( description="Whether the agent ever returns cache_scope: 'account' (i.e., publishes per-account overlays distinct from the public rate card). When true, buyers MUST be prepared to maintain account-overlay caches alongside the public layer. When false or absent, all responses are cache_scope: 'public' regardless of whether account was provided — the agent's rate card is universal. Confidentiality note: declaring true advertises that the agent runs custom-pricing deals (low-grade market-posture signal); agents preferring not to disclose this MAY omit the field and let consumers detect-on-call via cache_scope on response." ), + NoExplicitNull, ] = None @@ -3354,14 +3546,18 @@ class Error(AdCPBaseModel): Field( description='Optional buyer-actionable classification of the failure. Use this when the enclosing error code or message is too coarse or contains producer-internal context that must not cross the buyer trust boundary. `code` reuses the comprehensive standard vocabulary and recovery classifications published in `enums/error-code.json`, including policy, governance, account, commercial, inventory, and creative failures. The wire field remains open for forward compatibility. `message` MUST be safe to show to the buyer and MUST NOT contain vendor identifiers, ad-server type names, internal object names, internal IDs, stack traces, or other producer-private implementation details. When present, the enclosing `error.recovery` MUST classify the buyer-actionable reason. If both the enclosing code and buyer reason are registered, their standard recovery classifications MUST agree with each other and with `error.recovery`; sellers MUST choose the closest compatible enclosing code rather than retain a conflicting upstream transport wrapper. One buyer_reason classifies one error object. If a rejection contains independently actionable failures from different classes, sellers MUST emit separate error objects or omit buyer_reason rather than select a misleading single class.' ), + NoExplicitNull, ] = None field: Annotated[ str | None, Field( description="Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' → 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." ), + NoExplicitNull, + ] = None + suggestion: Annotated[ + str | None, Field(description='Suggested fix for the error'), NoExplicitNull ] = None - suggestion: Annotated[str | None, Field(description='Suggested fix for the error')] = None retry_after: Annotated[ StrictFloat | None, Field( @@ -3369,36 +3565,42 @@ class Error(AdCPBaseModel): ge=1.0, le=3600.0, ), + NoExplicitNull, ] = None issues: Annotated[ list[Issue2] | None, Field( description='Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only — translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` → `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers — the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.' ), + NoExplicitNull, ] = None details: Annotated[ dict[str, Any] | None, Field( description='Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n "code": "INVALID_PRICING_OPTION",\n "message": "Pricing option not found: po_prism_abandoner_cpm",\n "field": "pricing_option_id",\n "details": {\n "rejected_value": "po_prism_abandoner_cpm",\n "accepted_values": ["po_prism_cart_cpm", "po_prism_view_cpm"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what\'s accepted for *this caller in this context* (account, brand, scope) — leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.' ), + NoExplicitNull, ] = None recovery: Annotated[ Recovery | None, Field( description='Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). AdCP 3.2 producers MUST populate `recovery` on every error; 3.1 producers SHOULD populate it. The shared 3.x schema intentionally does not add `recovery` to `required` so retained and live errors from earlier 3.x producers remain decodable. When `buyer_reason` is present, `recovery` is required and MUST classify that buyer-actionable reason. If the enclosing code and buyer reason are both registered, their standard recovery classifications MUST agree with each other and with this field. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. When a legacy error omits `recovery`, receivers use the registered classification for a known code and fall back to `transient` for an unknown code, subject to the bounded retry budget. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known top-level and buyer-reason codes; `error.recovery` on the wire is authoritative when present.' ), + NoExplicitNull, ] = None source: Annotated[ Source | None, Field( description='Who emitted this error entry. `producer` (default when absent): emitted by the response\'s authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn\'t project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network — responses commonly traverse multiple SDKs (e.g., sales agent → interchange → DSP → buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry\'s `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: "sdk"`) are authoritative for what the response\'s authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can\'t tell whether a code came from the seller or an intermediate SDK, which corrupts attribution.' ), + NoExplicitNull, ] = None sdk_id: Annotated[ str | None, Field( description='Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: "sdk"`; MUST be absent when `source: "producer"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK\'s registry version doesn\'t).' ), + NoExplicitNull, ] = None @@ -3430,6 +3632,7 @@ class WholesaleFeedWebhooks(AdCPBaseModel): description='Wholesale feed webhook event types this agent can emit. Sales agents emit product.* events and MUST expose list_products or the deprecated 3.x wholesale get_products compatibility path. Signals agents emit signal.* events and MUST support wholesale get_signals. wholesale_feed.bulk_change requires at least one corresponding repair path.', min_length=1, ), + NoExplicitNull, ] = None @@ -3572,15 +3775,16 @@ class MaxImpressionsConstraints(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - minimum: Annotated[SchemaInt | None, Field(ge=1)] = None - maximum: Annotated[SchemaInt | None, Field(ge=1)] = None - allowed_values: Annotated[list[AllowedValue] | None, Field(min_length=1)] = None + minimum: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + maximum: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + allowed_values: Annotated[list[AllowedValue] | None, Field(min_length=1), NoExplicitNull] = None ext: Annotated[ dict[str, Any] | None, Field( description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -3665,12 +3869,14 @@ class AiTool(AdCPBaseModel): Field( description="Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version." ), + NoExplicitNull, ] = None provider: Annotated[ str | None, Field( description="Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" ), + NoExplicitNull, ] = None @@ -3694,6 +3900,7 @@ class DeclaredBy(AdCPBaseModel): agent_url: Annotated[ AnyUrl | None, Field(description='URL of the agent or service that declared this provenance'), + NoExplicitNull, ] = None role: Annotated[Role, Field(description='Role of the declaring party in the supply chain')] @@ -3724,6 +3931,7 @@ class VerifyAgent(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -3740,6 +3948,7 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" ), + NoExplicitNull, ] = None provider: Annotated[ str, @@ -3752,9 +3961,12 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts)." ), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the provenance data was embedded (ISO 8601)') + AwareDatetime | None, + Field(description='When the provenance data was embedded (ISO 8601)'), + NoExplicitNull, ] = None @@ -3780,6 +3992,7 @@ class VerifyAgent4(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -3806,6 +4019,7 @@ class Watermark(AdCPBaseModel): Field( description="Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist)." ), + NoExplicitNull, ] = None c2pa_action: Annotated[ C2paAction | None, @@ -3813,9 +4027,12 @@ class Watermark(AdCPBaseModel): description='C2PA action classification for this watermark', title='C2PA Watermark Action', ), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the watermark was applied (ISO 8601)') + AwareDatetime | None, + Field(description='When the watermark was applied (ISO 8601)'), + NoExplicitNull, ] = None @@ -3843,6 +4060,7 @@ class RenderGuidance(AdCPBaseModel): description='How long the disclosure must persist during content playback or display', title='Disclosure Persistence', ), + NoExplicitNull, ] = None min_duration_ms: Annotated[ SchemaInt | None, @@ -3850,6 +4068,7 @@ class RenderGuidance(AdCPBaseModel): description="Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.", ge=1, ), + NoExplicitNull, ] = None positions: Annotated[ list[Position] | None, @@ -3857,6 +4076,7 @@ class RenderGuidance(AdCPBaseModel): description='Preferred disclosure positions in priority order. The first position a format supports should be used.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -3864,6 +4084,7 @@ class RenderGuidance(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -3874,6 +4095,7 @@ class Jurisdiction(AdCPBaseModel): region: Annotated[ str | None, Field(description="Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)"), + NoExplicitNull, ] = None regulation: Annotated[ str, @@ -3886,12 +4108,14 @@ class Jurisdiction(AdCPBaseModel): Field( description='Required disclosure label text for this jurisdiction, in the local language' ), + NoExplicitNull, ] = None render_guidance: Annotated[ RenderGuidance | None, Field( description="How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed." ), + NoExplicitNull, ] = None @@ -3905,6 +4129,7 @@ class Disclosure(AdCPBaseModel): jurisdictions: Annotated[ list[Jurisdiction] | None, Field(description='Jurisdictions where disclosure obligations apply', min_length=1), + NoExplicitNull, ] = None @@ -3923,7 +4148,9 @@ class VerificationItem(AdCPBaseModel): ), ] verified_time: Annotated[ - AwareDatetime | None, Field(description='When the verification was performed (ISO 8601)') + AwareDatetime | None, + Field(description='When the verification was performed (ISO 8601)'), + NoExplicitNull, ] = None result: Annotated[Result, Field(description='Verification outcome')] confidence: Annotated[ @@ -3931,9 +4158,10 @@ class VerificationItem(AdCPBaseModel): Field( description='Confidence score of the verification result (0.0 to 1.0)', ge=0.0, le=1.0 ), + NoExplicitNull, ] = None details_url: Annotated[ - AnyUrl | None, Field(description='URL to the full verification report') + AnyUrl | None, Field(description='URL to the full verification report'), NoExplicitNull ] = None @@ -3944,46 +4172,54 @@ class Provenance(AdCPBaseModel): description='IPTC-aligned classification of AI involvement in producing this content', title='Digital Source Type', ), + NoExplicitNull, ] = None synthetic_depiction: Annotated[ StrictBool | None, Field( description='Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.' ), + NoExplicitNull, ] = None ai_tool: Annotated[ AiTool | None, Field( description='AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.' ), + NoExplicitNull, ] = None human_oversight: Annotated[ HumanOversight | None, Field( description='Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.' ), + NoExplicitNull, ] = None declared_by: Annotated[ DeclaredBy | None, Field( description='Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.' ), + NoExplicitNull, ] = None declared_at: Annotated[ AwareDatetime | None, Field( description='When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.' ), + NoExplicitNull, ] = None created_time: Annotated[ AwareDatetime | None, Field(description='When this content was created or generated (ISO 8601)'), + NoExplicitNull, ] = None c2pa: Annotated[ C2pa | None, Field( description='C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.' ), + NoExplicitNull, ] = None embedded_provenance: Annotated[ list[EmbeddedProvenanceItem] | None, @@ -3991,6 +4227,7 @@ class Provenance(AdCPBaseModel): description='Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None watermarks: Annotated[ list[Watermark] | None, @@ -3998,12 +4235,14 @@ class Provenance(AdCPBaseModel): description='Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None disclosure: Annotated[ Disclosure | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), + NoExplicitNull, ] = None verification: Annotated[ list[VerificationItem] | None, @@ -4011,6 +4250,7 @@ class Provenance(AdCPBaseModel): description='Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -4018,6 +4258,7 @@ class Provenance(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -4145,6 +4386,7 @@ class Logo(AdCPBaseModel): description='Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.', ge=1, ), + NoExplicitNull, ] = None pixel_ratio: Annotated[ StrictFloat | None, @@ -4152,18 +4394,21 @@ class Logo(AdCPBaseModel): description='Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.', gt=0.0, ), + NoExplicitNull, ] = None state_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None breakpoint_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None focal_point: Annotated[ list[FocalPointItem] | None, @@ -4172,16 +4417,22 @@ class Logo(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None format: Annotated[ - str | None, Field(description='Image file format (jpg, png, gif, webp, etc.)') + str | None, + Field(description='Image file format (jpg, png, gif, webp, etc.)'), + NoExplicitNull, + ] = None + alt_text: Annotated[ + str | None, Field(description='Alternative text for accessibility'), NoExplicitNull ] = None - alt_text: Annotated[str | None, Field(description='Alternative text for accessibility')] = None provenance: Annotated[ Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None @@ -4189,19 +4440,20 @@ class BrandKitOverride(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - logo: Annotated[Logo | None, Field(description='Override logo asset.', title='Image Asset')] = ( - None - ) - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) + logo: Annotated[ + Logo | None, Field(description='Override logo asset.', title='Image Asset'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull + ] = None voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class Brand1(AdCPBaseModel): @@ -4223,6 +4475,7 @@ class Brand1(AdCPBaseModel): pattern='^[a-z0-9_]+$', title='Brand ID', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -4230,24 +4483,28 @@ class Brand1(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None @@ -4277,6 +4534,7 @@ class Issuer(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -4306,6 +4564,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.', min_length=1, ), + NoExplicitNull, ] = None proof_formats: Annotated[ list[AnyUrl] | None, @@ -4313,6 +4572,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.', min_length=1, ), + NoExplicitNull, ] = None credential_origins: Annotated[ list[CredentialOrigin] | None, @@ -4320,6 +4580,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Canonical HTTPS origins from which credential_uri locators may be fetched for this issuer. Exact origin matching happens after URL canonicalization and before DNS resolution. Paths in the credential URI may vary; userinfo is forbidden.', min_length=1, ), + NoExplicitNull, ] = None resolvers: Annotated[ list[Resolver] | None, @@ -4327,6 +4588,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Evaluator-approved resolver endpoints for issuer_credential_id delivery. Presentations carry only resolver_id; they cannot replace url or authentication policy.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -4334,6 +4596,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -4375,6 +4638,7 @@ class Attestations(AdCPBaseModel): description="Verifier agents the evaluator may call. A presenter's verify_agent nomination must match one of these canonicalized URLs, but the evaluator remains verifier-of-record and chooses whether to use the nominated agent, another accepted agent, or local verification.", min_length=1, ), + NoExplicitNull, ] = None max_embedded_credential_bytes: Annotated[ SchemaInt | None, @@ -4383,6 +4647,7 @@ class Attestations(AdCPBaseModel): ge=1024, le=1048576, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -4390,6 +4655,7 @@ class Attestations(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -4409,6 +4675,7 @@ class Adcp(AdCPBaseModel): examples=[['3.0'], ['3.0', '3.1'], ['3.0', '3.1-beta']], min_length=1, ), + NoExplicitNull, ] = None build_version: Annotated[ str | None, @@ -4423,6 +4690,7 @@ class Adcp(AdCPBaseModel): ], pattern='^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$', ), + NoExplicitNull, ] = None idempotency: Annotated[ Idempotency | Idempotency3, @@ -4435,18 +4703,21 @@ class Adcp(AdCPBaseModel): Field( description='Caller-scoped durable connection configuration accepted by this agent. This is the buyer-to-seller configuration half of negotiation, not a second seller capability document: the seller advertises its objective offering here, while each authenticated caller submits its desired webhooks and reusable destinations through sync_principal and reads them back through get_principal. Per-account authority and feed selection remain in account/reporting configuration. Sellers exposing this block MUST list protocol.principal in experimental_features.' ), + NoExplicitNull, ] = None capability_changes: Annotated[ CapabilityChanges | None, Field( description='Freshness metadata and optional invalidation webhooks for this `get_adcp_capabilities` document. Buyers and registries MAY cache capabilities for up to `cache_ttl_seconds` when present, SHOULD compare `capabilities_version` across refreshes when present, and SHOULD re-run `get_adcp_capabilities` after receiving a `capabilities.changed` webhook. This block describes the agent-wide capability document, not per-caller authorization or account-scoped settings. A material capability change is any externally advertised contract change that can affect routing, validation, conformance coverage, task availability, auth/account handling, sandbox support, billing support, reporting delivery methods, creative-library support, targeting support, protocol versions, or other buyer-visible feature gates. Non-contract operational changes that do not alter the response body do not require a revision or webhook fire.' ), + NoExplicitNull, ] = None governance_enforcement: Annotated[ GovernanceEnforcement | None, Field( description='Cross-role declaration that this agent enforces buyer-provided governance authorization when performing consequential tasks. This is distinct from the top-level `governance` capability block, which describes an agent that provides governance services. An agent can enforce governance without implementing the governance protocol itself. Absence means the agent makes no governance-enforcement conformance claim.' ), + NoExplicitNull, ] = None attestations: Annotated[ Attestations | None, @@ -4476,6 +4747,7 @@ class Adcp(AdCPBaseModel): ], title='Attestation Capabilities', ), + NoExplicitNull, ] = None @@ -4493,18 +4765,21 @@ class Method3(AdCPBaseModel): ), ] destination_modes: Annotated[list[DestinationMode], Field(min_length=1)] - provider: Provider | None = None - format: Format | None = None + provider: Annotated[Provider | None, NoExplicitNull] = None + format: Annotated[Format | None, NoExplicitNull] = None access_mode: Annotated[ - str | None, Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$') + str | None, + Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$'), + NoExplicitNull, ] = None producer_identity: Annotated[ ProducerIdentity | None, Field( description='Seller principal a buyer grants access to for this exact buyer-hosted destination offering.' ), + NoExplicitNull, ] = None - reader_compatibility: list[ReaderCompatibilityItem] | None = None + reader_compatibility: Annotated[list[ReaderCompatibilityItem] | None, NoExplicitNull] = None class Offering(AdCPBaseModel): @@ -4570,6 +4845,7 @@ class Offering(AdCPBaseModel): Field( description='Managed delivery method for this offering. Omit for a Core (API-delivered) offering: rows flow through existing get_media_buy_delivery and reporting_webhook transports and no destination is involved. Present only when the seller advertises managed_delivery.' ), + NoExplicitNull, ] = None @@ -4612,12 +4888,14 @@ class ReliabilityStatistic(AdCPBaseModel): Field( description='Observed seconds from official finalized_at to the first adjustment, required when official_revisions_adjusted is nonzero.' ), + NoExplicitNull, ] = None adjustment_magnitude: Annotated[ list[AdjustmentMagnitudeItem] | None, Field( description='Optional absolute adjustment-magnitude percentiles for comparable control totals such as spend.' ), + NoExplicitNull, ] = None evidence: Evidence @@ -4638,12 +4916,14 @@ class ReportingDelivery(AdCPBaseModel): Field( description='Tier flag: this seller supports managed file, dataset-share, or warehouse delivery. Offerings whose method names a delivery pattern require this tier. When false or absent, every offering is API-delivered and Core-only.' ), + NoExplicitNull, ] = None reconciled_billing: Annotated[ StrictBool | None, Field( description='Tier flag: this seller supports canonical-digest verification and authenticated consumer receipts for both report materializations and post-official adjustments through receipt_task. Offerings with reconciliation_mode consumer_receipt and billing-grade canonicalization require this tier.' ), + NoExplicitNull, ] = None configuration_task: Literal['sync_accounts'] | None = None status_task: Literal['get_reporting_status'] | None = None @@ -4710,18 +4990,21 @@ class ReportingDelivery(AdCPBaseModel): description="Maximum interval after a CONSUMER_STATUS_MISMATCH issue's opened_at during which the seller may keep that issue at a non-escalated recommended_action. After it, the issue MUST be action_required with a contact_ recommended_action naming the diagnosed responsible_party. Declaring it requires operations_contact so the escalation has a destination. Absence means the seller publishes no escalation commitment; it never means an unbounded one.", ge=0, ), + NoExplicitNull, ] = None operations_contact: Annotated[ OperationsContact | None, Field( description='Optional non-secret human escalation path for reporting issues the protocol cannot resolve. It is display metadata for an operator, not an AdCP endpoint: agents MUST NOT dereference, probe, or send protocol traffic to these values, and they carry no authorization. Required when consumer_mismatch_escalation_seconds is advertised.' ), + NoExplicitNull, ] = None reliability_statistics: Annotated[ list[ReliabilityStatistic] | None, Field( description='Optional evidence-scoped observed performance for advertised offerings. offering_id values MUST be unique and name offerings in this capability block.' ), + NoExplicitNull, ] = None resource_retention_days: Annotated[ SchemaInt | None, @@ -4729,14 +5012,16 @@ class ReportingDelivery(AdCPBaseModel): description='Minimum period after publication for which at least one verified exact materialization remains readable to every still-authorized intended consumer.', ge=1, ), + NoExplicitNull, ] = None - supports_webhook_activity: StrictBool | None = None + supports_webhook_activity: Annotated[StrictBool | None, NoExplicitNull] = None authorization_revocation_seconds: Annotated[ SchemaInt | None, Field( description="Maximum delay after caller/account authorization ends before seller-controlled transport access, provider grants, and write credentials are revoked. It cannot revoke a buyer's access to data already written into a buyer-owned destination.", ge=0, ), + NoExplicitNull, ] = None @model_validator(mode='after') @@ -4762,11 +5047,11 @@ class TranslationTarget(AdCPBaseModel): ) token: Annotated[str, Field(min_length=1)] dialect: MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: MacroMappingStatus - universal_semantic: UniversalMacro | None = None + universal_semantic: Annotated[UniversalMacro | None, NoExplicitNull] = None next_operation: Literal['resolve_value'] = 'resolve_value' performed_by: MacroProcessingActor encoding: Annotated[ @@ -4783,11 +5068,11 @@ class MacroResolutionCapability(AdCPBaseModel): extra='forbid', ) dialect: MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: MappingStatus - universal_semantic: UniversalMacro | None = None + universal_semantic: Annotated[UniversalMacro | None, NoExplicitNull] = None operation: Operation performed_by: MacroProcessingActor supported_contexts: Annotated[list[MacroValueContext], Field(min_length=1)] @@ -4797,6 +5082,7 @@ class MacroResolutionCapability(AdCPBaseModel): description='Exact supported encoding profiles, not a maximum depth. Required for value resolution.', min_length=1, ), + NoExplicitNull, ] = None translation_target: Annotated[ TranslationTarget | None, @@ -4804,6 +5090,7 @@ class MacroResolutionCapability(AdCPBaseModel): description='The exact downstream-native token produced by a `translate_to_native` operation. The output asset replaces the source declaration with a declaration for this token and its next operation.', title='Macro Translation Target', ), + NoExplicitNull, ] = None @@ -4814,6 +5101,7 @@ class CreativeSpecs(AdCPBaseModel): description='Seller-wide VAST execution ceiling. Each product format option declares its binding accepted subset in `params.vast_versions`.', min_length=1, ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[MacroResolutionCapability] | None, @@ -4821,22 +5109,29 @@ class CreativeSpecs(AdCPBaseModel): description='Seller-wide ceiling for exact macro processing tuples (dialect identity/revision, semantic mapping, operation, actor, context, and encoding). It never proves a product execution path supports the same tuple and does not claim tracker firing; inspect the selected format option and, when standardized, product tracker capabilities.', min_length=1, ), + NoExplicitNull, ] = None mraid_versions: Annotated[ list[MraidVersion] | None, Field(description='MRAID versions supported for rich media mobile creatives'), + NoExplicitNull, ] = None vpaid: Annotated[ - StrictBool | None, Field(description='VPAID support for interactive video ads') + StrictBool | None, + Field(description='VPAID support for interactive video ads'), + NoExplicitNull, ] = None simid: Annotated[ - StrictBool | None, Field(description='SIMID support for interactive video ads') + StrictBool | None, + Field(description='SIMID support for interactive video ads'), + NoExplicitNull, ] = None vast_validation: Annotated[ VastValidation | None, Field( description="Level of VAST asset validation the seller performs at sync_creatives (including dry_run): 'structural' checks manifest shape and format requirements only and never inspects the VAST document; 'document' additionally parses the VAST document and can return VAST_PARSE_FAILED / VAST_VERSION_MISMATCH; 'wrapper' additionally resolves the wrapper chain and can return VAST_WRAPPER_DEPTH_EXCEEDED. Absent means 'structural'. See the VAST Validation section of the video channel documentation for the normative checks at each level." ), + NoExplicitNull, ] = VastValidation.structural @@ -4864,24 +5159,28 @@ class Targeting(AdCPBaseModel): geo_countries: Annotated[ StrictBool | None, Field(description='Country-level targeting using ISO 3166-1 alpha-2 codes'), + NoExplicitNull, ] = None geo_regions: Annotated[ StrictBool | GeoRegions | None, Field( description='ISO 3166-2 subdivision inclusion targeting. A legacy boolean is a coarse seller-wide declaration. Structured country/value entries are individually supported within the response scope, but do not promise joint composability or availability through the same execution route or account. Only Product.overlay_support supplies the binding set of executable targeting permissions for a configured Product.' ), + NoExplicitNull, ] = None geo_regions_exclude: Annotated[ StrictBool | GeoRegionsExclude | None, Field( description='ISO 3166-2 subdivision exclusion targeting, declared independently from inclusion. Structured country/value entries are individually supported within the response scope, but do not promise joint composability or availability through the same execution route or account. Only Product.overlay_support supplies the binding set of executable targeting permissions for a configured Product; absence means buyers cannot infer exclusion support from geo_regions alone.' ), + NoExplicitNull, ] = None geo_metros: Annotated[ GeoMetros | None, Field( description='Metro area targeting. Properties indicate which classification systems are supported.' ), + NoExplicitNull, ] = None geo_postal_areas: Annotated[ GeoPostalAreas | None, @@ -4889,6 +5188,7 @@ class Targeting(AdCPBaseModel): description='Declares supported postal area systems. Native support is keyed by ISO 3166-1 alpha-2 country with arrays of country-local postal systems. Deprecated country-fused boolean aliases may be included during migration.', title='Postal Area Support', ), + NoExplicitNull, ] = None geo_places: Annotated[ dict[GeoPlaceSystemKey, GeoPlaces] | None, @@ -4896,70 +5196,82 @@ class Targeting(AdCPBaseModel): description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.', min_length=1, ), + NoExplicitNull, ] = None age_restriction: Annotated[ AgeRestriction | None, Field(description='Age restriction capabilities for compliance (alcohol, gambling)'), + NoExplicitNull, ] = None demographics: Annotated[ Demographics | None, Field( description='Seller-wide discovery rollup for canonical demographic targeting. supported=true means at least one product implements the demographic targeting contract; it does not authorize demographic targeting on every product. Buyers MUST inspect Product.demographic_targeting for exact execution modes, bounds, intervals, and unknown-age behavior.' ), + NoExplicitNull, ] = None language: Annotated[ StrictBool | Language | None, Field( description="Language-preference targeting support. A legacy boolean is a coarse declaration. The structured form can enumerate the exact canonical BCP 47 targeting ranges the seller accepts. Buyers MUST treat supported_languages as exact selectable values: a declared 'fr' does not by itself authorize a request for 'fr-CA'. Sellers MUST reject unsupported requested values rather than silently widening or dropping them." ), + NoExplicitNull, ] = None keyword_targets: Annotated[ KeywordTargets | None, Field( description='Keyword targeting capabilities. Presence indicates support for targeting_overlay.keyword_targets and keyword_targets_add/remove in update_media_buy.' ), + NoExplicitNull, ] = None negative_keywords: Annotated[ NegativeKeywords | None, Field( description='Negative keyword capabilities. Presence indicates support for targeting_overlay.negative_keywords and negative_keywords_add/remove in update_media_buy.' ), + NoExplicitNull, ] = None placement_selection: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.placement_selection. False or absence makes no product-level promise. Product.overlay_support and Product.placements remain authoritative for a selected product.' ), + NoExplicitNull, ] = None property_list: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.property_list inclusion targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None property_list_exclude: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.property_list_exclude targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None collection_list: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.collection_list inclusion targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None collection_list_exclude: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.collection_list_exclude targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None geo_proximity: Annotated[ GeoProximity | None, Field( description='Proximity targeting capabilities from arbitrary coordinates via targeting_overlay.geo_proximity.' ), + NoExplicitNull, ] = None @@ -4969,6 +5281,7 @@ class Execution(AdCPBaseModel): Field( description='Trusted Match Protocol (TMP) support. Presence of this object indicates the seller has TMP infrastructure deployed. Check individual products via get_products for per-product TMP capabilities.' ), + NoExplicitNull, ] = None axe_integrations: Annotated[ list[AnyUrl] | None, @@ -4976,15 +5289,17 @@ class Execution(AdCPBaseModel): deprecated=True, description='Deprecated. Legacy AXE integrations. Use trusted_match for new integrations.', ), + NoExplicitNull, ] = None creative_specs: Annotated[ - CreativeSpecs | None, Field(description='Creative specification support') + CreativeSpecs | None, Field(description='Creative specification support'), NoExplicitNull ] = None targeting: Annotated[ Targeting | None, Field( description='Targeting capabilities. If declared true/supported, buyer can use these targeting parameters and seller MUST honor them.' ), + NoExplicitNull, ] = None @@ -4997,18 +5312,19 @@ class BrandKitOverride2(AdCPBaseModel): extra='allow', ) logo: Annotated[ - Logo1 | None, Field(description='Override logo asset.', title='Image Asset') + Logo1 | None, Field(description='Override logo asset.', title='Image Asset'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull ] = None - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class Vendor(AdCPBaseModel): @@ -5030,6 +5346,7 @@ class Vendor(AdCPBaseModel): pattern='^[a-z0-9_]+$', title='Brand ID', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -5037,24 +5354,28 @@ class Vendor(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride2 | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None @@ -5070,6 +5391,7 @@ class SupportedActivationMethods2(AdCPBaseModel): description='Supported transfer directions. buyer_to_seller: buyer writes to a seller-hosted bucket. seller_to_buyer: seller reads from a buyer-hosted bucket. Absent means unspecified (resolve during account setup), not neither.', min_length=1, ), + NoExplicitNull, ] = None vendor: Annotated[ Vendor, @@ -5095,18 +5417,19 @@ class BrandKitOverride3(AdCPBaseModel): extra='allow', ) logo: Annotated[ - Logo2 | None, Field(description='Override logo asset.', title='Image Asset') + Logo2 | None, Field(description='Override logo asset.', title='Image Asset'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull ] = None - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class Vendor1(AdCPBaseModel): @@ -5128,6 +5451,7 @@ class Vendor1(AdCPBaseModel): pattern='^[a-z0-9_]+$', title='Brand ID', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -5135,24 +5459,28 @@ class Vendor1(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride3 | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None @@ -5180,6 +5508,7 @@ class SupportedActivationMethods3(AdCPBaseModel): description="Principals the buyer grants access to in the vendor's system. identity is always required. cloud and region are optional, paired deployment metadata: omit both for global principals (for example, an IAM principal or federated identity). Their operational meaning is vendor-specific — they may constrain direct-share reachability or select a fulfillment route, or may be routing and cost hints only. They are not compliance boundaries; data-transfer assessments key on the recipient entity's jurisdiction, not the grantee account's region. Optional: sellers MAY instead communicate identities during account setup.", min_length=1, ), + NoExplicitNull, ] = None @@ -5192,18 +5521,19 @@ class BrandKitOverride4(AdCPBaseModel): extra='allow', ) logo: Annotated[ - Logo3 | None, Field(description='Override logo asset.', title='Image Asset') + Logo3 | None, Field(description='Override logo asset.', title='Image Asset'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull ] = None - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class Vendor2(AdCPBaseModel): @@ -5225,6 +5555,7 @@ class Vendor2(AdCPBaseModel): pattern='^[a-z0-9_]+$', title='Brand ID', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -5232,24 +5563,28 @@ class Vendor2(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride4 | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None @@ -5282,18 +5617,19 @@ class BrandKitOverride5(AdCPBaseModel): extra='allow', ) logo: Annotated[ - Logo4 | None, Field(description='Override logo asset.', title='Image Asset') + Logo4 | None, Field(description='Override logo asset.', title='Image Asset'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull ] = None - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class Vendor3(AdCPBaseModel): @@ -5315,6 +5651,7 @@ class Vendor3(AdCPBaseModel): pattern='^[a-z0-9_]+$', title='Brand ID', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -5322,24 +5659,28 @@ class Vendor3(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride5 | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None @@ -5368,6 +5709,7 @@ class SupportedActivationMethods5(AdCPBaseModel): max_length=256, min_length=1, ), + NoExplicitNull, ] = None bind_expiry_days: Annotated[ SchemaInt | None, @@ -5375,6 +5717,7 @@ class SupportedActivationMethods5(AdCPBaseModel): description="Window after which the seller MAY expire an unfulfilled platform_segment bind (per-audience action: failed on sync_audiences). Initial vendor distribution is days-scale; windows shorter than the vendor's documented distribution latency are non-conformant.", ge=1, ), + NoExplicitNull, ] = None @@ -5394,6 +5737,7 @@ class AudienceTargeting(AdCPBaseModel): Field( description="Whether the seller accepts the buyer's CRM/loyalty ID as a matchable identifier. Only applicable when the seller operates a closed ecosystem with a shared ID namespace (e.g., a retailer matching against their loyalty program). When true, buyers can include platform_customer_id values in AudienceMember.identifiers for matching against the seller's identity graph. Reporting on matched platform_customer_ids typically requires a clean room or the seller's own reporting surface." ), + NoExplicitNull, ] = None supported_uid_types: Annotated[ list[UIDType] | None, @@ -5401,6 +5745,7 @@ class AudienceTargeting(AdCPBaseModel): description='Universal ID types accepted for audience matching (MAIDs, RampID, UID2, etc.). MAID support varies significantly by platform — check this field before sending uids with type: maid.', min_length=1, ), + NoExplicitNull, ] = None minimum_audience_size: Annotated[ SchemaInt, @@ -5423,12 +5768,14 @@ class AudienceTargeting(AdCPBaseModel): description="Union of audience_activation.methods across the seller's products. Fast-fail discovery: a buyer reads this once and skips the catalog walk when nothing overlaps its pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Operational coordinates are account-scoped: the union MAY omit consumer_identities and destination_ref until bilateral account setup establishes them, and MUST NOT expose another account's coordinates. Absence of this field with media_buy.audience_activation listed in experimental_features means walk the catalog; only a present, non-overlapping union is a fast-fail signal. Experimental (x-status: experimental): sellers implementing audience activation declarations MUST list media_buy.audience_activation in experimental_features. Per docs/reference/experimental-status, this surface MAY change between 3.x releases with notice.", min_length=1, ), + NoExplicitNull, ] = None matching_latency_hours: Annotated[ MatchingLatencyHours | None, Field( description='Expected matching latency range in hours after upload. Use to calibrate polling cadence and set appropriate expectations before configuring push_notification_config.' ), + NoExplicitNull, ] = None @@ -5436,7 +5783,7 @@ class AttributionWindow(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - event_type: EventType | None = None + event_type: Annotated[EventType | None, NoExplicitNull] = None post_click: Annotated[ list[PostClickItem], Field( @@ -5450,6 +5797,7 @@ class AttributionWindow(AdCPBaseModel): description='Available post-view attribution windows (e.g. [{"interval": 1, "unit": "days"}])', min_length=1, ), + NoExplicitNull, ] = None @@ -5462,12 +5810,14 @@ class ConversionTracking(AdCPBaseModel): Field( description='Whether this seller can deduplicate conversion events across multiple event sources within a single goal. When true, the seller honors the deduplication semantics in optimization_goals event_sources arrays — the same event_id from multiple sources counts once. When false or absent, buyers should use a single event source per goal; multi-source arrays will be treated as first-source-wins. Most social platforms cannot deduplicate across independently-managed pixel and CAPI sources.' ), + NoExplicitNull, ] = None per_creative_attribution: Annotated[ StrictBool | None, Field( description='Whether the seller can attribute conversions to specific creatives within a package and surface that breakdown via media_buy_deliveries[].by_package[].by_creative[].conversions in get_media_buy_delivery. Defaults to false when omitted. Sellers that report conversions only at the line / package / placement / campaign granularity (retail-media, MMP-mediated mobile, CTV performance) declare false (or omit) and the per-creative scenario grades not_applicable for them. Sellers that surface ad-level conversion attribution (most social platforms) declare true and the scenario asserts the breakdown is populated end-to-end. Defaults to false to preserve backward compatibility.' ), + NoExplicitNull, ] = None supported_event_types: Annotated[ list[EventType] | None, @@ -5475,6 +5825,7 @@ class ConversionTracking(AdCPBaseModel): description='Event types this seller can track and attribute. If omitted, all standard event types are supported.', min_length=1, ), + NoExplicitNull, ] = None supported_targets: Annotated[ list[SupportedTarget3] | None, @@ -5482,10 +5833,12 @@ class ConversionTracking(AdCPBaseModel): description='Event-goal target kinds this seller can compute against. Buyers should only submit event-kind optimization goals whose target.kind is listed here — sellers MUST reject goals with unlisted target kinds. When omitted, only target-less event goals (maximize conversion count within budget) are guaranteed; sellers MAY accept specific target kinds but buyers should not rely on it. Named to parallel `metric_optimization.supported_targets` at the product level — same concept (which target kinds are supported), one at seller-capability granularity and one at product granularity.', min_length=1, ), + NoExplicitNull, ] = None supported_uid_types: Annotated[ list[UIDType] | None, Field(description='Universal ID types accepted for user matching', min_length=1), + NoExplicitNull, ] = None supported_hashed_identifiers: Annotated[ list[SupportedIdentifierType] | None, @@ -5493,16 +5846,19 @@ class ConversionTracking(AdCPBaseModel): description='Hashed PII types accepted for user matching. Buyers must hash before sending (SHA-256, normalized).', min_length=1, ), + NoExplicitNull, ] = None supported_action_sources: Annotated[ list[SupportedActionSource] | None, Field(description='Action sources this seller accepts events from', min_length=1), + NoExplicitNull, ] = None attribution_windows: Annotated[ list[AttributionWindow] | None, Field( description='Attribution windows available from this seller. Single-element arrays indicate fixed windows; multi-element arrays indicate configurable options the buyer can choose from via attribution_window on optimization goals.' ), + NoExplicitNull, ] = None @@ -5516,6 +5872,7 @@ class FrequencyCapping(AdCPBaseModel): description='Entity granularities the seller can enforce caps against. Values from the reach-unit enum. Omit to indicate all reach-unit values are supported.', min_length=1, ), + NoExplicitNull, ] = None supported_window_units: Annotated[ list[str] | None, @@ -5523,6 +5880,7 @@ class FrequencyCapping(AdCPBaseModel): description="Duration units the seller supports for frequency cap windows. Values must match the duration.json unit enum (e.g., 'hours', 'days', 'campaign'). Omit to indicate all duration units are supported.", min_length=1, ), + NoExplicitNull, ] = None @@ -5556,12 +5914,14 @@ class BudgetCapping(AdCPBaseModel): description='Feature-specific default cap-day timezone used when timezone_basis is fixed. Use UTC or an IANA timezone identifier.', min_length=1, ), + NoExplicitNull, ] = None buyer_timezone_override: Annotated[ StrictBool | None, Field( description='When true, buyers MAY override the default cap-day boundary via media-buy-level budget_cap_timezone. The accepted timezone applies to aggregate and package caps alike. When false or absent, a submitted override is rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None @@ -5571,6 +5931,7 @@ class ContentStandards(AdCPBaseModel): Field( description="Whether the seller runs a local evaluation model. When false, all artifacts will have local_verdict: 'unevaluated' and the failures_only filter on get_media_buy_artifacts is not useful." ), + NoExplicitNull, ] = None supported_channels: Annotated[ list[MediaChannel] | None, @@ -5578,12 +5939,14 @@ class ContentStandards(AdCPBaseModel): description='Channels for which the seller can provide content artifacts. Helps buyers understand which parts of a mixed-channel buy will have content standards coverage.', min_length=1, ), + NoExplicitNull, ] = None supports_webhook_delivery: Annotated[ StrictBool | None, Field( description='Whether the seller supports push-based artifact delivery via artifact_webhook configured at buy creation time.' ), + NoExplicitNull, ] = None @@ -5600,18 +5963,21 @@ class Portfolio(AdCPBaseModel): Field( description="Complete list of AdCP media channels for which this sales agent accepts and can meaningfully answer product-discovery briefs. When present, this is an exhaustive brief-routing allowlist: buyers MAY skip the agent when a brief's requested channels do not intersect it. Omission means channel scope is unknown and MUST NOT be interpreted as support for every channel. This is a routing pre-filter, not a promise of current product availability." ), + NoExplicitNull, ] = None primary_countries: Annotated[ list[PrimaryCountry] | None, Field( description="Complete list of ISO 3166-1 alpha-2 countries for which this sales agent accepts and can meaningfully answer product-discovery briefs. When present, this is an exhaustive brief-routing allowlist: buyers MAY skip the agent when a brief's requested countries do not intersect it. Omission means country scope is unknown and MUST NOT be interpreted as global coverage. This is a routing pre-filter, not a promise of current product availability and not an executable geo-targeting declaration; media_buy.execution.targeting geo capabilities and each product's overlay_support remain authoritative for targeting execution." ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Markdown-formatted description of the inventory portfolio', max_length=5000 ), + NoExplicitNull, ] = None advertising_policies: Annotated[ str | None, @@ -5619,6 +5985,7 @@ class Portfolio(AdCPBaseModel): description='Advertising content policies, restrictions, and guidelines', max_length=10000, ), + NoExplicitNull, ] = None @@ -5628,11 +5995,11 @@ class TranslationTarget1(AdCPBaseModel): ) token: Annotated[str, Field(min_length=1)] dialect: MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: MacroMappingStatus - universal_semantic: UniversalMacro | None = None + universal_semantic: Annotated[UniversalMacro | None, NoExplicitNull] = None next_operation: Literal['resolve_value'] = 'resolve_value' performed_by: MacroProcessingActor encoding: Annotated[ @@ -5649,11 +6016,11 @@ class MacroResolutionCapability2(AdCPBaseModel): extra='forbid', ) dialect: MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: MappingStatus - universal_semantic: UniversalMacro | None = None + universal_semantic: Annotated[UniversalMacro | None, NoExplicitNull] = None operation: Operation performed_by: MacroProcessingActor supported_contexts: Annotated[list[MacroValueContext], Field(min_length=1)] @@ -5663,6 +6030,7 @@ class MacroResolutionCapability2(AdCPBaseModel): description='Exact supported encoding profiles, not a maximum depth. Required for value resolution.', min_length=1, ), + NoExplicitNull, ] = None translation_target: Annotated[ TranslationTarget1 | None, @@ -5670,6 +6038,7 @@ class MacroResolutionCapability2(AdCPBaseModel): description='The exact downstream-native token produced by a `translate_to_native` operation. The output asset replaces the source declaration with a declaration for this token and its next operation.', title='Macro Translation Target', ), + NoExplicitNull, ] = None @@ -5679,6 +6048,7 @@ class Format5(AdCPBaseModel): Field( description='Stable publisher- or product-declaration identity that this creative operation can satisfy. When publisher_domain is present, this field is required.' ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -5686,6 +6056,7 @@ class Format5(AdCPBaseModel): description='Publisher namespace for format_option_id when this operation claims compatibility with a publisher declaration.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[MacroResolutionCapability2] | None, @@ -5693,23 +6064,27 @@ class Format5(AdCPBaseModel): description='Creative-route processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. These capabilities describe build, validation, or preview processing and do not grant seller production authority.', min_length=1, ), + NoExplicitNull, ] = None - technical_requirements_complete: StrictBool | None = None - display_name: str | None = None - sample_render_url: AnyUrl | None = None - applies_to_channels: list[MediaChannel] | None = None - seller_preference: SellerPreference | None = None + technical_requirements_complete: Annotated[StrictBool | None, NoExplicitNull] = None + display_name: Annotated[str | None, NoExplicitNull] = None + sample_render_url: Annotated[AnyUrl | None, NoExplicitNull] = None + applies_to_channels: Annotated[list[MediaChannel] | None, NoExplicitNull] = None + seller_preference: Annotated[SellerPreference | None, NoExplicitNull] = None locale_policy: Annotated[ LocalePolicy | None, Field( description='Seller-declared locale eligibility for one product or placement format declaration. Each accepted_language_ranges entry is a concrete canonical BCP 47 language range evaluated with RFC 4647 section 3.3.1 Basic Filtering: fr accepts fr, fr-CA, and fr-FR, while fr-CA accepts only fr-CA and its more-specific descendants. Ranges are ORed and wildcards are not supported. Presence is a hard assignment constraint, not a preference: at least one materialized creative locale variant must match. The seller filters the creative to eligible variants before applying buyer-declared strict Lookup, locale_fallbacks, or default behavior; buyer policy can never select a seller-ineligible variant.', title='Creative Locale Policy', ), + NoExplicitNull, ] = None - canonical_formats_only: StrictBool | None = False - experimental: StrictBool | None = False - format_shape: str | None = None - v1_format_ref: Annotated[list[V1FormatRefItem] | None, Field(min_length=1)] = None + canonical_formats_only: Annotated[StrictBool | None, NoExplicitNull] = False + experimental: Annotated[StrictBool | None, NoExplicitNull] = False + format_shape: Annotated[str | None, NoExplicitNull] = None + v1_format_ref: Annotated[list[V1FormatRefItem] | None, Field(min_length=1), NoExplicitNull] = ( + None + ) format_schema: Annotated[ FormatSchema | None, Field( @@ -5722,6 +6097,7 @@ class Format5(AdCPBaseModel): ], title='Platform Extension Reference', ), + NoExplicitNull, ] = None format_kind: Annotated[ FormatKind, @@ -5748,6 +6124,7 @@ class SupportedFormat(AdCPBaseModel): description="Stable identifier for this format capability within the creative agent (e.g., 'audio_narration_30s'). The value MUST be unique across this agent's supported_formats catalog. Route the advertised operation with this ID: build_creative.target_capability_id(s) for build, preview_creative.target_capability_id for preview, or validate_input targets[] kind capability for validate. Capability IDs are agent-local routes and do not appear on portable manifests or products.", pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None format: Annotated[ Format5, @@ -5762,6 +6139,7 @@ class SupportedFormat(AdCPBaseModel): description='Creative operations this capability supports. `build` means the agent can produce a conforming manifest via build_creative; `validate` means it can evaluate inputs against the declaration; `preview` means it can render a preview. New 3.2 producers MUST emit this field so buyers and registries can distinguish producers, validators, and renderers without probing tasks. Consumers interpret omission from a legacy 3.x entry as `["build"]`.', min_length=1, ), + NoExplicitNull, ] = [Operation4.build] @@ -5774,60 +6152,70 @@ class Creative(AdCPBaseModel): Field( description='When true, this creative agent can process briefs with compliance requirements (required_disclosures, prohibited_claims) and will validate that disclosures can be satisfied by the target format.' ), + NoExplicitNull, ] = None has_creative_library: Annotated[ StrictBool | None, Field( description='When true, this agent hosts a creative library and supports list_creatives and creative_id references in build_creative. Creative agents with a library should also implement the accounts protocol (sync_accounts / list_accounts) so buyers can establish access.' ), + NoExplicitNull, ] = False supports_revisions: Annotated[ StrictBool | None, Field( description='When true, this agent accepts buyer-assigned revision_id on sync_creatives, enforces immutable revision content, echoes accepted revision identity, returns it from list_creatives, and attributes delivered executions to it. Revision support does not imply revision history, rollback, or staged activation.' ), + NoExplicitNull, ] = False supports_generation: Annotated[ StrictBool | None, Field( description='When true, this agent can generate creatives from natural language briefs via build_creative. The buyer provides a message with creative direction, and the agent produces a manifest with generated assets. When false, build_creative only supports transformation or library retrieval.' ), + NoExplicitNull, ] = False supports_transformation: Annotated[ StrictBool | None, Field( description='When true, this agent can transform or resize existing canonical manifests via build_creative. The buyer supplies a creative_manifest and an advertised target_capability_id.' ), + NoExplicitNull, ] = False representation_resolution: Annotated[ RepresentationResolution | None, Field( description='Explicit opt-in for deterministic seller-bound selection from `build_creative.creative_representation_set`. Only the destination sales agent may advertise and exercise this capability because resolution requires its current product, placement/publisher narrowings, and seller-wide execution ceilings. A standalone creative agent may help a buyer select locally but MUST NOT advertise this capability or claim seller deliverability. Absence means the caller selects a representation before sending a seller-bound manifest; the agent MUST NOT guess silently.' ), + NoExplicitNull, ] = None supports_transformers: Annotated[ StrictBool | None, Field( description='When true, this agent exposes account-scoped creative transformers via list_transformers (the creative analog of media-buy products) and accepts transformer_id + config on build_creative. Buyers SHOULD call list_transformers to discover available transformers, their typed config params (and account-scoped enumerable option values via expand_params), and pricing. When false or absent, the agent does not offer the transformer surface.' ), + NoExplicitNull, ] = False supports_refinement: Annotated[ StrictBool | None, Field( description="When true, this agent retains produced build_variant leaves for an agent-defined retention window and can re-build from one via build_creative's refine_from_build_variant_id — applying a natural-language instruction in message plus an optional config delta, returning new lineage-linked variants. A build-time agent capability independent of generation/transformation. When false or absent, refine_from_build_variant_id is rejected with UNSUPPORTED_FEATURE; buyers refine instead via the transform path (creative_manifest + message)." ), + NoExplicitNull, ] = False supports_spend_controls: Annotated[ StrictBool | None, Field( description='When true, build_creative honors a per-call `max_spend` ceiling (producing partial paid results and returning budget_status:"capped" + a BUDGET_CAP_REACHED advisory rather than overspending) AND supports mode:"estimate" dry-runs (a projected cost band, producing/billing nothing). When false or absent, max_spend / mode:estimate are rejected with UNSUPPORTED_FEATURE. Out-of-band billers (bills_through_adcp:false) have no AdCP cost truth to cap against, so this is meaningful only alongside bills_through_adcp:true.' ), + NoExplicitNull, ] = False supports_evaluator: Annotated[ StrictBool | None, Field( description="Experimental (x-status: experimental) — agents setting this true MUST also list `creative.evaluator` in `experimental_features`; the surface MAY change between 3.x releases with notice (see docs/reference/experimental-status). When true, build_creative accepts an advisory `evaluator` input (exemplars / account-arranged evaluator_id / agent_url, plus an optional `feature_requirement[]` gate, a `rank_by` ordering, and an allowlisted `feature_agent` pointer). Feature discovery uses this response's governance.creative_features catalog: rank_by, feature_requirement, and eval.features[] all share the same creative-feature vocabulary as get_creative_features. evaluator_id is not discovered from this catalog; it is a pre-provisioned account preset whose emitted feature_ids still come from it. The evaluator populates a per-leaf `eval` block of creative-feature values (creative-feature-result[], the same shape get_creative_features returns) on BuildCreativeVariantSuccess leaves, which is what the recommended/rank it sets on the best_of_n axis are computed over. The agent runs a gate-then-rank pipeline over its best_of_n exploration: it evaluates each leaf, DROPS leaves failing `feature_requirement[]` from its recommended survivors, then orders survivors by `rank_by`. The gate is internal pruning of which leaves the agent recommends/returns from its own exploration — it never blocks an already-produced billable leaf: what is produced and billed is governed by max_variants/max_creatives/max_spend, not the evaluator. When the evaluator names an external agent, it MUST appear in `creative_policy.accepted_verifiers[]` (off-list → EVALUATOR_AGENT_NOT_ACCEPTED), and the producing agent authenticates the outbound evaluator call on the transport. Evaluator credentials and caller-supplied trust material MUST NOT be passed in the build_creative payload; credential- or trust-material payload keys should be rejected with CREDENTIAL_IN_ARGS. When false or absent, the `evaluator` input is ignored and no `eval` block is emitted." ), + NoExplicitNull, ] = False refinable_retention_seconds: Annotated[ SchemaInt | None, @@ -5835,36 +6223,42 @@ class Creative(AdCPBaseModel): description='When supports_refinement is true, the GUARANTEED-MINIMUM window (a floor, not a ceiling) during which a produced build_variant_id remains refinable via refine_from_build_variant_id: a ref within this window from production SHOULD resolve; the agent MAY retain longer. Omit when the retention window is agent-defined and not advertised — buyers then treat refinability as best-effort and handle REFERENCE_NOT_FOUND.', ge=0, ), + NoExplicitNull, ] = None multiplicity: Annotated[ Multiplicity | None, Field( description="Pre-call discriminators for build_creative fan-out, so a buyer knows BEFORE sending max_creatives / max_variants whether this agent supports them and the ceilings. Over-limit requests are CLAMPED to these ceilings (the agent produces up to the limit and signals the shortfall via items_returned < items_total on BuildCreativeVariantSuccess), not rejected — consistent with item_limit's 'use the lesser' rule. Absent means no fan-out: build_creative produces a single creative and max_creatives/max_variants>1 are not supported." ), + NoExplicitNull, ] = None supported_formats: Annotated[ list[SupportedFormat] | None, Field( description='Canonical-format capability catalog for this creative agent. This is the 3.2 source of truth for discovering which format contracts the agent can build, validate, or preview; it replaces the deprecated `list_creative_formats` task. Each entry uses the authority-free `CreativeOperationFormatDeclaration` projection of a product format declaration: canonical shape and creative-route macro processing are preserved, while seller production commitments are excluded. New 3.2 producers MUST publish a stable agent-local `capability_id` and explicit `operations` for task routing. Every emitted `capability_id` MUST be unique within this catalog so a route selects exactly one entry. During the 3.x compatibility window, consumers MUST also accept legacy entries that omit either field; absent `operations` means `["build"]`, while an absent `capability_id` means the entry is discoverable by canonical contract but cannot be selected through a capability-ID route.\n\n**Publisher-specific support.** To claim exact support for a publisher declaration, `format` carries the declaration\'s `{publisher_domain, format_option_id}` pair plus its canonical `format_kind` and narrowed `params`. Generic creative agents MAY instead advertise a canonical parameter envelope without publisher identity. A generic capability matches a target declaration only when the capability can satisfy every target constraint; matching canonical names alone is insufficient. Registries MAY reverse-index these entries by `format.format_kind`, `format.publisher_domain`, and `format.format_option_id`.\n\nThis catalog describes creative operations, not sales-agent inventory deliverability. Sales agents publish the purchasable closed set on each `Product.format_options[]`; publisher acceptance lives in `adagents.json.formats[]`.' ), + NoExplicitNull, ] = None preview: Annotated[ Preview | None, Field( description='Per-route preview_creative capability metadata. New 3.2 producers whose supported_formats[] explicitly advertises a routable preview operation MUST emit this block. rendering_origin describes how each route is implemented but is informational and never grants presentation authority: only a matching publisher-origin placement preview_provider delegation can do that. routes[].capability_id MUST equal the set of capability IDs on supported_formats[] entries whose operations contains preview.' ), + NoExplicitNull, ] = None localization: Annotated[ Localization | None, Field( description='Materialized creative-localization support for sync_creatives/list_creatives, including source-only monolingual topology. Presence opts the agent into exact locale-variant round-trip, strict RFC 4647 Lookup, optional buyer-declared language-family fallback rules, explicit final default/unmatched behavior, creative-wide review, transactional replacement, seller product-format locale-policy enforcement, and delivery attribution. This is a coarse structural capability, not a promise that every locale/format/account combination is accepted; sellers publish accepted ranges on product format declarations and validate each write before mutation. Omit this object when localization is unsupported.' ), + NoExplicitNull, ] = None bills_through_adcp: Annotated[ StrictBool | None, Field( description='When true, this creative agent bills through the AdCP rate-card surface: list_creatives returns pricing_options when include_pricing=true with an authenticated account, build_creative populates pricing_option_id and vendor_cost on the response, and report_usage accepts records against the rate card. When false or absent, the agent bills out of band (flat license, SaaS contract, bundled enterprise agreement) and buyers should skip pricing fields and tolerate report_usage returning accepted: 0 with errors carrying BILLING_OUT_OF_BAND. A pre-call discriminator so buyer agents can route across many creative agents without first establishing an account to probe pricing.' ), + NoExplicitNull, ] = False canonical_catalog_version: Annotated[ str | None, @@ -5872,6 +6266,7 @@ class Creative(AdCPBaseModel): description="Optional. The AdCP canonical-formats catalog version this agent's runtime is built against (e.g., `3.1`, `3.2.0`). Lets buyer SDKs detect canonical-catalog skew between their generated types and the seller's actual support. SDKs MAY declare the version they were generated against (typically the AdCP version they ship for); when seller and SDK versions disagree, SDKs SHOULD soft-warn rather than fail (the open-enum semantics on `canonical-format-kind.json` make unknown canonicals safe to retain, so skew is not a hard error — it just means the older side might not understand newer canonical values). Omitted by sellers who haven't yet generated against a versioned catalog; absence is interpreted as the AdCP version advertised by the broader capabilities response.", pattern='^\\d+\\.\\d+(\\.\\d+)?$', ), + NoExplicitNull, ] = None @@ -5885,6 +6280,7 @@ class PolicyProfile(AdCPBaseModel): description='Standalone policy modes accepted in this exact scope and allocation context. Combination-only components belong only in supported_combinations and need not appear here. `automatic` means the seller accepts and preserves an explicit `{automatic:true}` authored block; omission only invokes inheritance/default behavior.', min_length=1, ), + NoExplicitNull, ] = None cost_per_strengths: Annotated[ list[CostPerStrength] | None, @@ -5892,6 +6288,7 @@ class PolicyProfile(AdCPBaseModel): description='Supported standalone cost_per strengths in this profile. Required exactly when modes includes cost_per.', min_length=1, ), + NoExplicitNull, ] = None roas_strengths: Annotated[ list[RoasStrength] | None, @@ -5899,6 +6296,7 @@ class PolicyProfile(AdCPBaseModel): description='Supported standalone roas strengths in this profile. Required exactly when modes includes roas.', min_length=1, ), + NoExplicitNull, ] = None supported_combinations: Annotated[ list[MaxBidWithCostPer | MaxBidWithRoas] | None, @@ -5906,6 +6304,7 @@ class PolicyProfile(AdCPBaseModel): description='Multi-field policies supported in this exact scope and allocation context. Each entry independently qualifies the strengths supported in that combination; standalone strength support does not imply combination support. Absence means no multi-field combination is claimed.', min_length=1, ), + NoExplicitNull, ] = None @@ -5914,15 +6313,18 @@ class FrequencyCapIntervalConstraints(AdCPBaseModel): extra='forbid', ) unit: Unit - minimum_interval: Annotated[SchemaInt | None, Field(ge=1)] = None - maximum_interval: Annotated[SchemaInt | None, Field(ge=1)] = None - allowed_intervals: Annotated[list[AllowedInterval] | None, Field(min_length=1)] = None + minimum_interval: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + maximum_interval: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + allowed_intervals: Annotated[ + list[AllowedInterval] | None, Field(min_length=1), NoExplicitNull + ] = None ext: Annotated[ dict[str, Any] | None, Field( description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -5933,10 +6335,12 @@ class ScopeCapability(AdCPBaseModel): fixed: Annotated[ PolicyProfile | None, Field(description='Policies supported when budget allocation is fixed or omitted.'), + NoExplicitNull, ] = None seller_optimized: Annotated[ PolicyProfile | None, Field(description='Policies supported when budget_allocation.mode is seller_optimized.'), + NoExplicitNull, ] = None @@ -5949,12 +6353,14 @@ class BiddingPolicyCapability(AdCPBaseModel): Field( description='Policies accepted at media-buy scope and inherited by packages that omit a package override.' ), + NoExplicitNull, ] = None package: Annotated[ ScopeCapability | None, Field( description='Policies accepted as package-authored policies, including explicit automatic overrides.' ), + NoExplicitNull, ] = None @@ -5967,6 +6373,7 @@ class FrequencyCapConstraints(AdCPBaseModel): Field( description='Logical cap fields that can change after creation. An empty array means create-only. For example, [max_impressions] permits changing the count while keeping per and window fixed. Omission means update support is undeclared and the legacy broad meaning applies.' ), + NoExplicitNull, ] = None supported_control_modes: Annotated[ list[SupportedControlMode] | None, @@ -5974,10 +6381,12 @@ class FrequencyCapConstraints(AdCPBaseModel): description='FrequencyCap shapes accepted by the product. max_impressions_and_suppress means both controls may appear together and are enforced with AND semantics.', min_length=1, ), + NoExplicitNull, ] = None supported_per_units: Annotated[ list[ReachUnit] | None, Field(description='Entity granularities accepted by max_impressions caps.', min_length=1), + NoExplicitNull, ] = None max_impressions_constraints: Annotated[ MaxImpressionsConstraints | None, @@ -5985,6 +6394,7 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Exact supported max_impressions presets or range.', title='Frequency Cap Impression Constraints', ), + NoExplicitNull, ] = None window_constraints: Annotated[ list[FrequencyCapIntervalConstraints] | None, @@ -5992,6 +6402,7 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Exact supported max_impressions.window intervals, one entry per duration unit.', min_length=1, ), + NoExplicitNull, ] = None suppression_constraints: Annotated[ list[FrequencyCapIntervalConstraints] | None, @@ -5999,6 +6410,7 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Exact supported suppress cooldown intervals, one entry per duration unit.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ dict[str, Any] | None, @@ -6006,6 +6418,7 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None @@ -6016,60 +6429,70 @@ class Features1(AdCPBaseModel): deprecated=True, description='Deprecated 3.x compatibility capability for creatives provided inline in create_media_buy and update_media_buy package payloads. buy_products, accept_proposal, and control_media_buy never accept inline creatives. New integrations use the dedicated creative lifecycle. Removed in 4.0.', ), + NoExplicitNull, ] = None property_list_filtering: Annotated[ StrictBool | None, Field( description='Honors property_list parameter in get_products to filter results to buyer-approved properties' ), + NoExplicitNull, ] = None catalog_management: Annotated[ StrictBool | None, Field( description='Supports sync_catalogs task for catalog feed management with platform review and approval' ), + NoExplicitNull, ] = None catalog_item_availability_updates: Annotated[ StrictBool | None, Field( description='Supports buyer-pushed item_availability_updates and item_availability_queries on sync_catalogs for immediate suppression/restoration and current-state readback in buyer-managed catalogs. Seller declarations may be true only with catalog_management: true; buyer required_features filters may request this feature alone. Requests containing availability operations are synchronous and accept at most 1,000 combined update and query entries. A successful suppress covers selection, dynamic rendering, and cached or pre-generated creatives materialized from the item. Internal lineage MUST retain resolved_account_id, catalog_id, catalog_generation, and item_id. Static creatives supplied or promoted by the buyer without catalog lineage remain outside this automatic guarantee. Suppression persists across feed refreshes and ordinary upserts until restore, expires_at, or catalog deletion. A seller that does not declare true MUST reject availability operations with UNSUPPORTED_FEATURE before lookup or mutation and MUST NOT interpret them as discovery. This does not control seller-owned wholesale inventory or let restore bypass seller controls.' ), + NoExplicitNull, ] = None committed_metrics_supported: Annotated[ StrictBool | None, Field( description="Seller has per-package snapshot infrastructure for the reporting contract. When true, the seller MUST populate `package.committed_metrics` on committed `create_media_buy` responses where `confirmed_at` is non-null, MUST omit `package.committed_metrics` while `confirmed_at` is null for a provisional buy, and MUST honor append-only mid-flight metric additions via `update_media_buy`. The unified `committed_metrics` array (per the metric-accountability design) covers both standard and vendor-defined metric entries, so a single flag is load-bearing. Buyers filtering on this flag are detecting 'this seller can stamp the reporting contract,' which closes the audit gap from PR #3510 where absence of `committed_metrics` was indistinguishable between 'didn't snapshot' and 'snapshot infrastructure not implemented.'" ), + NoExplicitNull, ] = None seller_optimized_budget: Annotated[ StrictBool | None, Field( description="Supports the core seller-optimized shared-budget contract for budget_allocation.mode `seller_optimized`: one hard shared total_budget, seller allocation of that total across the buy's packages against budget_allocation.optimization_goals, media-buy-level pacing, and echo of the allocation configuration on buy read surfaces. Sellers declaring true MUST accept eligible explicit-package and proposal executions that use only these core controls and MUST enforce the aggregate budget. Core media-buy pacing: sellers declaring true MUST accept omitted media-buy pacing (which defaults to even when total_budget is present) and pacing `even` on seller-optimized buys; they MAY reject `asap` or `front_loaded` with `UNSUPPORTED_FEATURE` (error.field `pacing`) before any provider mutation, and MUST NOT silently coerce them to `even`. Package-level controls inside a seller-optimized buy are separate capabilities: package budget caps (seller_optimized_package_budgets), package minimum-spend targets (seller_optimized_min_spend_targets), and package pacing (seller_optimized_package_pacing). A seller declaring this feature but not one of those sub-capabilities MUST reject any request that would leave that package control on a seller-optimized buy with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, and MUST NOT silently drop, soften, or coerce it. Over-subscription validation (`INVALID_REQUEST`) applies only to package controls the seller has declared; see seller_optimized_min_spend_targets. Product combinations may still be rejected when their currencies, optimization capabilities, pricing terms, or delivery constraints are incompatible. Sellers that do not declare this feature MUST reject any request carrying `budget_allocation.mode: 'seller_optimized'` with `UNSUPPORTED_FEATURE` before any provider mutation, and MUST NOT coerce the request to fixed allocation." ), + NoExplicitNull, ] = None seller_optimized_package_budgets: Annotated[ StrictBool | None, Field( description='Honors package `budget` as an optional hard lifetime package spend cap inside a seller-optimized buy: packages[].budget and new_packages[].budget on create_media_buy and update_media_buy, purchases[].budget on buy_products, package budget controls on control_media_buy, and max_spend_percentage on seller-optimized proposal allocations. The cap is a ceiling, not a reserved or current allocation, and package caps may sum above total_budget. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. A seller that declares seller_optimized_budget without this feature MUST reject a request that would leave a package budget on a seller-optimized buy, including an allocation-mode switch that retains fixed-mode package budgets (the buyer clears them with null in the same atomic update), with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, and MUST NOT issue seller-optimized proposals carrying max_spend_percentage. Does not govern fixed allocation, where package budgets remain required.' ), + NoExplicitNull, ] = None seller_optimized_min_spend_targets: Annotated[ StrictBool | None, Field( description='Honors package `min_spend_target` as a soft lifetime minimum-spend target inside a seller-optimized buy: packages[].min_spend_target and new_packages[].min_spend_target on create_media_buy and update_media_buy, purchases[].min_spend_target on buy_products, package min_spend_target controls on control_media_buy, and min_spend_target_percentage on seller-optimized proposal allocations. The seller SHOULD attempt to deliver at least the target before allocating incremental spend elsewhere; it is not a billing or delivery guarantee. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. Sellers declaring this feature MUST reject package minimum-spend targets summing above total_budget with `INVALID_REQUEST` before mutation, and, when they also declare seller_optimized_package_budgets, MUST likewise reject a min_spend_target above its own package budget. A seller that declares seller_optimized_budget without this feature MUST reject a request carrying a numeric min_spend_target with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, so an over-subscribed target sent to such a seller yields `UNSUPPORTED_FEATURE`, and MUST NOT issue seller-optimized proposals carrying min_spend_target_percentage.' ), + NoExplicitNull, ] = None seller_optimized_package_pacing: Annotated[ StrictBool | None, Field( description='Honors package `pacing` as subordinate per-package pacing inside a seller-optimized buy, in addition to the media-buy-level pacing covered by seller_optimized_budget: packages[].pacing and new_packages[].pacing on create_media_buy and update_media_buy, purchases[].pacing on buy_products, package pacing controls on control_media_buy, and allocation pacing on seller-optimized proposals. Package pacing MUST NOT cause delivery to exceed aggregate media-buy pacing. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. Package pacing equal to the effective media-buy pacing adds no subordinate constraint and does not require this feature; buyers SHOULD omit package pacing on seller-optimized buys unless this feature is advertised. A seller that declares seller_optimized_budget without this feature MUST reject a request that would leave package pacing differing from media-buy pacing on a seller-optimized buy, including an allocation-mode switch that retains such fixed-mode package pacing (the buyer can align it in the same update), with `UNSUPPORTED_FEATURE` before any provider mutation, and MUST NOT issue seller-optimized proposals carrying allocation pacing. Does not govern package pacing in fixed allocation.' ), + NoExplicitNull, ] = None bidding_policy: Annotated[ BiddingPolicyCapability | None, Field( description='Structured support for canonical bidding by authored scope, allocation context, mode, strength, and strength-qualified multi-field combination. Presence does not imply support for both scopes, both allocation modes, or every policy shape. Sellers MUST preserve every advertised semantic exactly and reject unadvertised policies rather than translating them.' ), + NoExplicitNull, ] = None @@ -6092,12 +6515,14 @@ class MediaBuy(AdCPBaseModel): Field( description='Whether this seller accepts product discovery without caller credentials. This applies to list_products and to get_products in brief or wholesale mode; it does not apply to proposal refinement or finalization, purchasing, account-scoped reads, or mutations. true means an anonymous discovery request can produce a successful response, but the response may be a public subset and may differ from results for an authenticated principal or selected account. false means these discovery calls require an authenticated principal. Absence means unspecified legacy behavior, so callers probe and handle AUTH_MISSING. A valid authenticated request is never rejected merely because credentials were supplied. true is inconsistent with account.required_for_products=true because an anonymous caller cannot select protected account context.' ), + NoExplicitNull, ] = None acceptance_policy_discovery: Annotated[ AcceptancePolicyDiscovery | None, Field( description='Registry-backed seller acceptance-policy discovery. Presence means the seller publishes a versioned catalog; it does not claim that the seller evaluates acceptance_context during discovery. Discovery is advisory, exact task responses remain authoritative, and absent capability means support is unknown rather than unrestricted acceptance.' ), + NoExplicitNull, ] = None supported_pricing_models: Annotated[ list[SupportedPricingModel] | None, @@ -6105,6 +6530,7 @@ class MediaBuy(AdCPBaseModel): description='Pricing models this seller supports across its product portfolio. Buyers can use this for pre-flight filtering before querying individual products. Individual products may support a subset of these models.', min_length=1, ), + NoExplicitNull, ] = None buying_modes: Annotated[ list[BuyingMode] | None, @@ -6112,18 +6538,21 @@ class MediaBuy(AdCPBaseModel): description="Buying modes this seller supports on get_products. 'brief' (semantic discovery driven by the brief) is universally supported and implicit. 'wholesale' (raw wholesale product feed enumeration — caller omits brief and the seller returns the full priced product feed, paginated) is opt-in and SHOULD be declared explicitly so buyers can probe before issuing wholesale calls. 'refine' lets buyers iterate on prior products/proposals and is also the vehicle for finalizing draft proposals when the seller returns them. Sellers MAY declare ['brief', 'wholesale'] to signal wholesale support; absent declaration is treated as ['brief'] for wholesale-feed probing purposes and sellers MAY return INVALID_REQUEST for wholesale calls they do not support. Symmetric with signals.discovery_modes.", min_length=1, ), + NoExplicitNull, ] = [BuyingMode.brief] measurement_terms_acceptance: Annotated[ StrictBool | None, Field( description="Whether this seller can accept the default measurement_terms it advertises on a product. A value of true means the seller can return a product carrying measurement_terms for a measurement-specific brief and accept those terms unchanged on a package for that product. This opts the seller into conformance scenarios that discover and replay the product's own terms. False or absent means acceptance is outside the seller's advertised scope, but the seller must still reject unsupported terms with TERMS_REJECTED rather than being graded on an acceptance path it did not claim." ), + NoExplicitNull, ] = False availability_horizon: Annotated[ StrictBool | None, Field( description='Whether this seller supports flexible-window availability discovery: parsing offer_filters.availability_horizon and answering with time-dimensioned forecast points that carry availability_status. Sellers declaring true MUST apply the full window contract — half-open non-overlapping windows that partition the requested horizon (or signal gaps via incomplete[]), with availability_status computed from all booking eligibility constraints, not only competing holds. false or absent means flexible-window support is unknown: buyers SHOULD use exact start_date/end_date filtering, and sellers MAY ignore the field or reject it. Conformance storyboards gate flexible-window checks on this declaration.' ), + NoExplicitNull, ] = False lifecycle_tools: Annotated[ list[LifecycleTool] | None, @@ -6131,12 +6560,14 @@ class MediaBuy(AdCPBaseModel): description='Compact product and MediaBuy lifecycle operation names this seller supports. Added in AdCP 3.2 as task-specific contracts that form the 4.0 lifecycle foundation. Sellers may advertise any supported subset while retaining the deprecated get_products/create_media_buy/update_media_buy facades throughout 3.x. Each stateful split task has its own idempotency identity; callers MUST retry with the same tool name.', min_length=1, ), + NoExplicitNull, ] = None proposal_refinement: Annotated[ ProposalRefinement | None, Field( description='Pre-flight support for typed refine_proposals revision dimensions. These declarations mean the seller can parse and mechanically validate a dimension; they never promise that the seller will commercially concede it. Absence means typed-dimension support is unknown and buyers must handle per-result partial or unable outcomes. A dimension omitted from an explicit supported_dimensions list MUST be rejected at task level with UNSUPPORTED_FEATURE before any proposal is created.' ), + NoExplicitNull, ] = None reporting_delivery_methods: Annotated[ list[CapabilityReportingDeliveryMethod] | None, @@ -6144,12 +6575,14 @@ class MediaBuy(AdCPBaseModel): description="How this seller delivers reporting data to buyers. Polling via get_media_buy_delivery is always available as a baseline regardless of this field. This array declares additional push-based delivery methods the seller supports. 'webhook': seller pushes to buyer-provided URL (configured per buy via reporting_webhook). 'offline': seller pushes batch files to a cloud storage bucket (seller-provisioned per account via reporting_bucket on the account object). When absent, only polling is available.", min_length=1, ), + NoExplicitNull, ] = None performance_feedback: Annotated[ PerformanceFeedback | None, Field( description='Structured seller performance-feedback support beyond the legacy scalar contract. Presence means the seller accepts compact baseline/metric/provenance fields from a buyer orchestrator and returns a feedback_id.' ), + NoExplicitNull, ] = None offline_delivery_protocols: Annotated[ list[CloudStorageProtocol] | None, @@ -6157,6 +6590,7 @@ class MediaBuy(AdCPBaseModel): description="Cloud storage protocols this seller supports for offline file delivery. Only meaningful when reporting_delivery_methods includes 'offline'. Buyers express a protocol preference in sync_accounts; the seller provisions the account's reporting_bucket using a supported protocol.", min_length=1, ), + NoExplicitNull, ] = None reporting_delivery: Annotated[ ReportingDelivery | None, @@ -6164,24 +6598,28 @@ class MediaBuy(AdCPBaseModel): description='AdCP 3.2 Reliable Reporting capability. The affirmative machine answer to ‘Do you support Reliable Reporting?’ requires this block with supported: true and reliable_reporting_version: 1.0 plus media_buy.reporting_delivery in experimental_features. Core exposes seller get_reporting_status; during the published migration window, consumer_status_task separately advertises opt-in buyer-to-seller sync_reporting_status and becomes required Core only in the next eligible minor. managed_delivery and reconciled_billing identify optional tiers. This generalizes, but does not remove, the legacy reporting_delivery_methods/offline_delivery_protocols surface; those legacy fields and declarations from other protocols do not imply Reliable Reporting support.', title='Reporting Delivery Capabilities', ), + NoExplicitNull, ] = None supports_proposals: Annotated[ StrictBool | None, Field( description='Conformance declaration that this seller supports proposals through either the compact request/refine/finalize lifecycle or the legacy get_products facade. accept_proposal, or the create_media_buy compatibility facade, consumes a finalized committed proposal_id before expires_at.' ), + NoExplicitNull, ] = False outcome_target: Annotated[ StrictBool | None, Field( description="Whether this seller supports reverse-forecast planning: parsing criteria.outcome_target (a compact goal — a forecastable-metric delivery metric or an event-type conversion event — plus a desired volume, a cost_per target, or both) and planning against it, answering with total_budget_guidance and forecasts whose points carry the goal's key in metrics; see outcome-target.json for cost_per answers and rejections. This flag does not distinguish sellers that plan cost_per; buyers rely on the negotiated adcp_version together with features.bidding_policy. false or absent means support is unknown: buyers SHOULD express outcome goals in brief prose instead, and sellers reject a structured outcome_target on proposal requests with UNSUPPORTED_FEATURE rather than silently ignoring it (list_products ignores it for every seller)." ), + NoExplicitNull, ] = False governance_aware: Annotated[ StrictBool | None, Field( description='Compatibility claim used by existing media-buy conformance runners. A value of true corresponds only to online governance consultation for create_media_buy, the historically graded surface. Agents use adcp.governance_enforcement for explicit task-scoped claims, including update_media_buy and cross-role signed-context enforcement.' ), + NoExplicitNull, ] = False propagation_surfaces: Annotated[ list[PropagationSurface] | None, @@ -6189,12 +6627,14 @@ class MediaBuy(AdCPBaseModel): description='Where this seller surfaces dependency-resource impairments (creative suspended/rejected post-approval, audience suspended, catalog item withdrawn, event source insufficient, property depublished) to buyers. Non-exclusive: a seller mirroring impairments on both the buy snapshot AND firing webhooks declares `["snapshot", "webhook"]` (the common case for premium guaranteed sellers). Each value names one surface where buyers can observe an impairment:\n\n- **`snapshot`** — seller propagates resource transitions into `media_buy.health` and `media_buy.impairments[]` on the next `get_media_buys` read. The `impairment.coherence` compliance assertion grades this surface; storyboards that exercise it (`media_buy_seller/dependency_impairment`, `media_buy_seller/dependency_impairment_cardinality`) require `"snapshot"` to be declared, else they grade `not_applicable`.\n- **`webhook`** — seller fires `notification-type: impairment` webhooks (configured via `push_notification_config`). Sellers declaring `"webhook"` MUST satisfy the persistent-channel webhook contract for the impairment event type. A seller declaring `["webhook"]` without `"snapshot"` is webhook-only — buyers reconcile state from the push channel alone, and snapshot-coherence storyboards grade `not_applicable`.\n- **`out_of_band`** — seller propagates via channels outside the AdCP protocol surface entirely (email to trafficker, separate dashboard, partner-specific notification feed). Long-tail and enterprise-bundled platforms commonly use this when impairment workflows are managed in human channels. Sellers declaring only `["out_of_band"]` are not graded by snapshot or webhook compliance — their bar is the offline agreement, not a protocol assertion. If a seller has impairment data in their API under a non-AdCP field name (a mapping gap, not truly out-of-band), they SHOULD document the mapping rather than declare `out_of_band` — the spec\'s gap, not the seller\'s posture, is what `out_of_band` legitimately covers.\n\nDefault: `["snapshot"]` when absent (preserves the existing snapshot-coherence contract for sellers that don\'t declare). Empty array `[]` is invalid (`minItems: 1`) — omit the field to inherit the default rather than declaring no surfaces. Pick the surfaces that honestly describe where buyers will see impairments on this agent. Mixing is normative — `["snapshot", "webhook"]` is the documented common case; `["snapshot", "webhook", "out_of_band"]` is valid for sellers that ship all three surfaces (rare but legal). See lifecycle.mdx § Compliance for the per-surface contract.', min_length=1, ), + NoExplicitNull, ] = [PropagationSurface.snapshot] creative_approval_mode: Annotated[ CreativeApprovalMode | None, Field( description="Tenant-wide applicability signal for media-buy creative approval behavior. This is not a notification or new approval workflow. `auto_approve` means human review does not block serving eligibility after creatives are assigned and automated validation passes. `require_human` means one or more products/accounts may require manual review before creatives become eligible to serve; buyers and compliance runners MUST treat this as a worst-case ceiling across this seller's portfolio unless a future product-level override says otherwise. Compliance runners use this mainly to decide whether auto-approval-dependent storyboards apply. When absent, approval behavior is legacy-unspecified; runners SHOULD NOT treat omission as an affirmative auto-approval claim. `ai_assisted` is intentionally not part of the enum until a behavioral contract is defined." ), + NoExplicitNull, ] = None supported_indicator_types: Annotated[ list[SupportedIndicatorType] | None, @@ -6202,34 +6642,41 @@ class MediaBuy(AdCPBaseModel): description="Indicator types this seller can expose on get_media_buys media-buy/package/creative-assignment snapshots. Each type's meaning is defined by the negotiated AdCP release; indicator types do not carry independent sub-versions. This is availability, not complete upstream coverage. Poll-only sellers may declare this field without relationship_notifications. If relationship_notifications includes indicators.changed, this field is required so receivers know which durable indicator types can be repaired.", min_length=1, ), + NoExplicitNull, ] = None relationship_notifications: Annotated[ RelationshipNotifications | None, Field( description='Optional durable account-level invalidations for indicator, creative-assignment, and assignment-approval changes. A seller may expose indicators only through polling and omit this block. A seller without an indicator catalog may declare creative.assignment_changed alone. get_media_buys is the complete authoritative repair read. creative.assignment_changed is independent of the optional bounded list_creatives reverse projection, so inline-only sellers can advertise approval and assignment invalidations. Presence means the seller accepts the declared subscriptions through sync_accounts notification_configs. Timestamp-only reevaluation does not fire. Poll-based upstream integrations fire when they detect a change; this declaration does not promise upstream detection latency.' ), + NoExplicitNull, ] = None - features: Features1 | None = None + features: Annotated[Features1 | None, NoExplicitNull] = None execution: Annotated[ - Execution | None, Field(description='Technical execution capabilities for media buying') + Execution | None, + Field(description='Technical execution capabilities for media buying'), + NoExplicitNull, ] = None audience_evidence: Annotated[ AudienceEvidence | None, Field( description='Support for structured product audience evidence and buyer-authored evidence policy. Presence means the seller can publish Product.audience_evidence, preserve immutable snapshots through package readback, and evaluate the declared policy modes. It does not declare audience-targeting capability.' ), + NoExplicitNull, ] = None rights_attestations: Annotated[ RightsAttestations | None, Field( description='Seller evaluation policy for portable rights-grant attestations carried on creative rights constraints. Presence requires adcp.attestations and the rights-grant claim URI in accepted_claim_types. The seller remains verifier-of-record and never treats verification_url or a buyer-authored evaluation as authorization.' ), + NoExplicitNull, ] = None audience_targeting: Annotated[ AudienceTargeting | None, Field( description='Audience targeting capabilities. Presence of this object indicates the seller supports audience targeting, including sync_audiences and audience_include/audience_exclude in targeting overlays.' ), + NoExplicitNull, ] = None supported_optimization_metrics: Annotated[ list[SupportedOptimizationMetric] | None, @@ -6237,24 +6684,28 @@ class MediaBuy(AdCPBaseModel): description='Optimization metrics this seller can support on at least one of their products. Seller-level rollup of product-level metric_optimization.supported_metrics declarations (core/product.json). Buyers SHOULD filter their requested optimization goals against this list before submitting briefs. Sellers MUST keep this in sync with their product catalog — if no products support a metric, it must not appear here. Omitting this field means the seller declares no specific guarantees about which metrics they support; buyers should fall back to per-product inspection of metric_optimization.supported_metrics.', min_length=1, ), + NoExplicitNull, ] = None vendor_metric_optimization: Annotated[ VendorMetricOptimization | None, Field( description='Seller-level rollup of vendor-metric optimization capabilities supported by at least one product. Product-level vendor_metric_optimization.supported_metrics[] remains authoritative for the specific (vendor, metric_id) pairs and target kinds a buyer may bind on a package; this seller-level object exists so buyers and compliance runners can discover whether vendor_metric goals are in scope before walking the catalog. Sellers MUST keep this in sync with product-level vendor_metric_optimization declarations.' ), + NoExplicitNull, ] = None conversion_tracking: Annotated[ ConversionTracking | None, Field( description='Seller-level conversion tracking capabilities. Presence of this object indicates the seller supports sync_event_sources and log_event for conversion event tracking.' ), + NoExplicitNull, ] = None frequency_capping: Annotated[ FrequencyCapping | None, Field( description='Seller-wide package frequency-capping infrastructure. Presence means the seller honors targeting_overlay.frequency_cap on packages, with an independent counter per package, and MUST reject caps it cannot enforce rather than silently dropping them. Product.overlay_support is the binding per-product declaration. A cap whose single counter spans every package in a MediaBuy is advertised separately through aggregate_frequency_capping.' ), + NoExplicitNull, ] = None aggregate_frequency_capping: Annotated[ AggregateFrequencyCapping | None, @@ -6262,24 +6713,28 @@ class MediaBuy(AdCPBaseModel): description='Seller-wide support for a maximum-impression cap whose single counter is shared across every package in a MediaBuy. Presence is required before a buyer sends a root frequency_cap. Products additionally declare media_buy_support.frequency_cap and any narrower constraints.', title='Media Buy Frequency Cap Capability', ), + NoExplicitNull, ] = None budget_capping: Annotated[ BudgetCapping | None, Field( description='Hard daily budget-cap capabilities. Presence declares only the scopes listed in supported_scopes; sellers MUST reject a daily_budget_cap at an undeclared scope with UNSUPPORTED_FEATURE before mutation and MUST NOT silently drop or soften it. A cap is always a hard ceiling. Sellers that offer a best-effort pacing target must expose that under a separately named feature rather than interpreting daily_budget_cap as soft. Daily caps are orthogonal to pacing.' ), + NoExplicitNull, ] = None content_standards: Annotated[ ContentStandards | None, Field( description='Content standards implementation details. Presence of this object indicates the seller supports content_standards configuration including sampling rates and category filtering. Gives buyers pre-buy visibility into local evaluation and artifact delivery capabilities. This is a seller-side media-buy capability; governance agents providing content standards services declare `specialisms: ["content-standards"]` instead.' ), + NoExplicitNull, ] = None portfolio: Annotated[ Portfolio | None, Field( description="Information about the seller's media inventory portfolio. Media-buy sellers SHOULD publish primary_channels and primary_countries as their complete brief-routing scope. Buyers use publisher_domains to verify authorization via adagents.json. Omitted routing arrays mean unknown scope and MUST NOT be interpreted as global coverage." ), + NoExplicitNull, ] = None @@ -6294,6 +6749,7 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -6303,12 +6759,14 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): ge=1, le=99, ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, Field( description='Transport-managed conversation identifier. On A2A, this maps to the native Message/Task `contextId` used to associate messages with a conversation; it is not carried inside the AdCP DataPart. On MCP, a request-body `context_id`, where admitted by the selected request schema, is a compatibility-only field: servers MUST ignore it, callers MUST NOT rely on it for continuity, and it MUST NOT select session state, identity, account, authorization, task continuation, or idempotency scope. MCP continuity, if provided, comes from the transport session. Distinct from `context` (per-request opaque echo, see below) and from `task_id` (AdCP operation tracking).' ), + NoExplicitNull, ] = None context: Annotated[ dict[str, Any] | None, @@ -6316,12 +6774,14 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): description='Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (transport-managed A2A conversation correlation or MCP compatibility metadata) — `context` is caller-owned echo and never selects transport state. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root — they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.', title='Context Object', ), + NoExplicitNull, ] = None task_id: Annotated[ str | None, Field( description='Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.' ), + NoExplicitNull, ] = None status: Annotated[ Status, @@ -6335,18 +6795,21 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): Field( description='Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response.' ), + NoExplicitNull, ] = None timestamp: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress.' ), + NoExplicitNull, ] = None replayed: Annotated[ StrictBool | None, Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False adcp_error: Annotated[ AdcpError | None, @@ -6354,6 +6817,7 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): description="Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array — the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` — the envelope MUST NOT carry `adcp_error` for non-failures.", title='Error', ), + NoExplicitNull, ] = None push_notification_config: Annotated[ PushNotificationConfig | None, @@ -6361,6 +6825,7 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): description='AdCP application-layer webhook configuration for async task updates over MCP, A2A, or REST. Echoed from the request to confirm webhook settings. It is distinct from transport-native progress or A2A TaskPushNotificationConfig delivery and can outlive the originating transport session.', title='Push Notification Config', ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -6370,12 +6835,14 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None payload: Annotated[ dict[str, Any] | None, Field( description='Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct — it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.' ), + NoExplicitNull, ] = None adcp: Annotated[Adcp, Field(description='Core AdCP protocol information')] supported_protocols: Annotated[ @@ -6390,117 +6857,136 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): Field( description='Account management capabilities. Describes how accounts are established, what billing models are supported, and whether an account is required before browsing products.' ), + NoExplicitNull, ] = None media_buy: Annotated[ MediaBuy | None, Field( description='Media-buy protocol capabilities. Expected when media_buy is in supported_protocols. Sellers declaring media_buy should also include account with supported_billing.' ), + NoExplicitNull, ] = None signals: Annotated[ Signals | None, Field( description='Signals protocol capabilities. Only present if signals is in supported_protocols.' ), + NoExplicitNull, ] = None governance: Annotated[ Governance | None, Field( description='Governance protocol capabilities. Only present if governance is in supported_protocols. Governance agents provide property and creative data like compliance scores, brand safety ratings, sustainability metrics, and creative quality assessments.' ), + NoExplicitNull, ] = None sponsored_intelligence: Annotated[ SponsoredIntelligence | None, Field( description='Sponsored Intelligence protocol capabilities. Only present if sponsored_intelligence is in supported_protocols. SI agents handle conversational brand experiences.' ), + NoExplicitNull, ] = None brand: Annotated[ Brand | None, Field( description='Brand protocol capabilities. Only present if brand is in supported_protocols. Brand agents provide identity data (logos, colors, tone, assets) and optionally rights clearance for licensable content (talent, music, stock media).' ), + NoExplicitNull, ] = None creative: Annotated[ Creative | None, Field( description='Creative protocol capabilities. Only present if creative is in supported_protocols.' ), + NoExplicitNull, ] = None oauth: Annotated[ Oauth | None, Field( description='Introduced in AdCP 3.2. OAuth 2.0 protected-resource support for inbound transport authentication. This is a capability claim, not a requirement that every caller use OAuth: `supported: true` means the agent publishes RFC 9728 protected-resource metadata for its advertised endpoint and RFC 8414 metadata for every referenced authorization server, and opts into the universal `oauth_setup` conformance storyboard. Agents that use only static Bearer, Basic, mTLS, or RFC 9421 authentication omit this block or declare `supported: false`. Operator credential acquisition remains separately described by `account.authorization_endpoint` when applicable.' ), + NoExplicitNull, ] = None request_signing: Annotated[ RequestSigning | None, Field( description='RFC 9421 HTTP Signatures support for incoming requests. Signing remains optional through 3.2, but every accepted 3.2 signature on a request with a body MUST cover content-digest. Request signing becomes required for spend-committing operations in 4.0. The full profile is defined in docs/building/by-layer/L1/security.mdx (Signed Requests (Transport Layer)).' ), + NoExplicitNull, ] = None webhook_signing: Annotated[ WebhookSigning | None, Field( description='RFC 9421 webhook-signature and delivery-retry support for outbound webhook callbacks (top-level peer of request_signing). Declares which AdCP webhook-signing profile version and algorithms this agent produces on delivery, whether it supports the legacy HMAC-SHA256 fallback for receivers that have not yet adopted RFC 9421, and the maximum retry horizon receivers use to retain immutable delivery evidence. See docs/building/by-layer/L3/webhooks.mdx.' ), + NoExplicitNull, ] = None identity: Annotated[ Identity | None, Field( description='Operator identity posture — trust-root pointer (`brand_json_url`) plus key-scoping and compromise-response controls the agent operates. `brand_json_url` is **load-bearing** for signature verification: when the agent declares any signing posture (`request_signing.supported_for`/`required_for` non-empty, `webhook_signing.supported === true`, or any `key_origins` subfield), `brand_json_url` MUST be present (storyboard-enforced in 3.x; schema-required in 4.0). Verifiers use it to bootstrap from the agent URL to the operator\'s brand.json (and from there to signing keys); see [security.mdx §Discovering an agent\'s signing keys](https://adcontextprotocol.org/docs/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url). The remaining fields (`per_principal_key_isolation`, `key_origins`, `compromise_notification`) are advisory and receivers use them to reason about blast radius and revocation latency at onboarding. Empty-object semantics: `identity: {}` means "posture block present but no posture claimed" — schema-valid but advisory-neutral and receivers MUST treat it as equivalent to omitting the block, **except** that an agent declaring a signing posture elsewhere in the response with an empty `identity` MUST be rejected by storyboard runners as missing `brand_json_url`.' ), + NoExplicitNull, ] = None measurement_gateway: Annotated[ MeasurementGateway | None, Field( description='Buyer-controlled task gateway between an orchestrator and measurement providers. In the first experimental tier, providers read buyer-approved cross-seller delivery through get_media_buy_delivery and return compact assertions through provide_performance_feedback, without receiving seller credentials. Orchestrators implementing this block MUST include measurement in supported_protocols and measurement.gateway in experimental_features; this gateway role does not claim the media_buy seller protocol.' ), + NoExplicitNull, ] = None measurement: Annotated[ Measurement | None, Field( description="Experimental measurement capability block. Presence indicates this agent computes one or more quantitative metrics about ad delivery, exposure, or effect, and is willing to be discovered as a measurement vendor. Agents implementing this block MUST list `measurement.core` in experimental_features. Returns metric definitions and whether the provider produces compact performance feedback, not pricing/coverage (negotiated via `measurement_terms` on `create_media_buy`) or raw/live datasets. Per-buy vendor values remain on delivery reports; optimizer-ready projections use provide_performance_feedback. AgenticAdvertising.org crawls each measurement agent's `metrics[]` on a TTL to populate the federated cross-vendor index." ), + NoExplicitNull, ] = None compliance_testing: Annotated[ ComplianceTesting | None, Field( description="Compliance testing capabilities. The presence of this block declares that the agent supports deterministic testing via comply_test_controller for lifecycle state machine validation. Omit the block entirely if the agent does not support compliance testing. Sellers SHOULD list every canonical controller scenario they implement so buyers and runners can distinguish full deterministic coverage from partial coverage without probing each scenario one by one; the runtime source of truth remains comply_test_controller with scenario: 'list_scenarios'." ), + NoExplicitNull, ] = None specialisms: Annotated[ list[Specialism] | None, Field( description="Optional — specialized compliance claims this agent supports. Values MUST be kebab-case enum IDs (e.g., 'creative-generative', 'sales-non-guaranteed'). An agent that implements a specialism's tools but omits its ID from this array will receive 'No applicable tracks found' from the compliance runner — tracks for that specialism are not evaluated even if every tool works. Omitting the field means the agent declares no specialism claims (it still passes the universal + domain-baseline storyboards implied by supported_protocols). Each specialism maps to a storyboard bundle at /compliance/{version}/specialisms/{id}/ that the AAO compliance runner executes to verify the claim. Each specialism rolls up to one of the protocols in supported_protocols — the runner rejects a specialism claim whose parent protocol is missing. Only list specialisms your agent actually implements — the AAO Verified badge enumerates which specialisms were demonstrably passed." ), + NoExplicitNull, ] = None extensions_supported: Annotated[ list[ExtensionsSupportedItem] | None, Field( description='Extension namespaces this agent supports. Buyers can expect meaningful data in ext.{namespace} fields on responses from this agent. Extension schemas are published in the AdCP extension registry.' ), + NoExplicitNull, ] = None experimental_features: Annotated[ list[ExperimentalFeature] | None, Field( description='Experimental AdCP surfaces this agent implements. A surface is experimental when its schema carries x-status: experimental and the working group has not yet frozen it. Sellers that implement any experimental surface MUST list its feature id here. Buyers inspect this array before relying on experimental surfaces — a seller that does not list a surface is asserting it does not implement it. Experimental surfaces MAY break between any two 3.x releases with at least 6 weeks notice; the full contract is in docs/reference/experimental-status.' ), + NoExplicitNull, ] = None wholesale_feed_versioning: Annotated[ WholesaleFeedVersioning | None, Field( description="Conditional-fetch token capabilities for get_products and get_signals. Independent of wholesale feed webhooks: an agent MAY support cheap version probes via if_wholesale_feed_version without pushing change payloads (and vice versa). When supported is true, the agent returns wholesale_feed_version on every get_products / get_signals response and honors if_wholesale_feed_version on subsequent requests. When absent or supported is false, callers MAY still send if_wholesale_feed_version — pre-3.1 agents that ignore it just return the full payload (correct, just inefficient). Pre-flight declaration here lets buyers fast-path which agents to bother caching versions for. See get_products / get_signals 'Wholesale feed versioning' sections." ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp of when capabilities were last updated. Buyers can use this for cache invalidation.' ), + NoExplicitNull, ] = None errors: Annotated[ - list[Error] | None, Field(description='Task-specific errors and warnings') + list[Error] | None, Field(description='Task-specific errors and warnings'), NoExplicitNull ] = None ext: Annotated[ dict[str, Any] | None, @@ -6508,10 +6994,12 @@ class GetAdcpCapabilitiesResponse(AdCPBaseModel): description='Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.', title='Extension Object', ), + NoExplicitNull, ] = None wholesale_feed_webhooks: Annotated[ WholesaleFeedWebhooks | None, Field( description='Per-agent wholesale product-feed and wholesale signals-feed webhook capabilities. Consumers register durable sync_accounts notification subscribers and receive actual product.*, signal.*, or wholesale_feed.bulk_change payloads without polling. Product mirrors bootstrap and repair through list_products(if_feed_version); signal mirrors use get_signals(if_wholesale_feed_version). Deprecated wholesale get_products remains the 3.x product compatibility path. Webhook emission MUST apply the same caller/account authorization and cache-scope predicate as the corresponding read.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/collection/collection_list.py b/src/adcp/types/domains/collection/collection_list.py index cc082bc3b..38da09cb0 100644 --- a/src/adcp/types/domains/collection/collection_list.py +++ b/src/adcp/types/domains/collection/collection_list.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: collection/collection_list.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..core import account_ref, brand_ref @@ -19,34 +19,39 @@ class CollectionList(AdCPBaseModel): ) list_id: Annotated[str, Field(description='Unique identifier for this collection list')] name: Annotated[str, Field(description='Human-readable name for the list')] - description: Annotated[str | None, Field(description="Description of the list's purpose")] = ( - None - ) + description: Annotated[ + str | None, Field(description="Description of the list's purpose"), NoExplicitNull + ] = None account: Annotated[ account_ref.AccountReference | None, Field( description='Account that owns this list. Returned as account_id form (seller-assigned identifier).' ), + NoExplicitNull, ] = None base_collections: Annotated[ list[base_collection_source.BaseCollectionSource] | None, Field( description="Array of collection sources to evaluate. Each entry is a discriminated union: distribution_ids (platform-independent identifiers), publisher_collections (publisher_domain + collection_ids), or publisher_genres (publisher_domain + genres). If omitted, queries the agent's entire collection database." ), + NoExplicitNull, ] = None filters: Annotated[ collection_list_filters.CollectionListFilters | None, Field(description='Dynamic filters applied when resolving the list'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference used to automatically apply appropriate rules. Resolved to full brand identity at execution time.' ), + NoExplicitNull, ] = None webhook_url: Annotated[ AnyUrl | None, Field(description='URL to receive notifications when the resolved list changes'), + NoExplicitNull, ] = None cache_duration_hours: Annotated[ SchemaInt | None, @@ -54,16 +59,18 @@ class CollectionList(AdCPBaseModel): description='Recommended cache duration for resolved list. Consumers should re-fetch after this period. Defaults to 168 (one week) because collection metadata changes less frequently than property metadata.', ge=1, ), + NoExplicitNull, ] = 168 - created_at: Annotated[AwareDatetime | None, Field(description='When the list was created')] = ( - None - ) + created_at: Annotated[ + AwareDatetime | None, Field(description='When the list was created'), NoExplicitNull + ] = None updated_at: Annotated[ - AwareDatetime | None, Field(description='When the list was last modified') + AwareDatetime | None, Field(description='When the list was last modified'), NoExplicitNull ] = None collection_count: Annotated[ SchemaInt | None, Field( description='Number of collections in the resolved list (at time of last resolution)' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/collection/collection_list_changed_webhook.py b/src/adcp/types/domains/collection/collection_list_changed_webhook.py index 0d3fb9154..7199c70a3 100644 --- a/src/adcp/types/domains/collection/collection_list_changed_webhook.py +++ b/src/adcp/types/domains/collection/collection_list_changed_webhook.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: collection/collection_list_changed_webhook.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -17,13 +17,19 @@ class ChangeSummary(AdCPBaseModel): extra='forbid', ) collections_added: Annotated[ - SchemaInt | None, Field(description='Number of collections added since last resolution') + SchemaInt | None, + Field(description='Number of collections added since last resolution'), + NoExplicitNull, ] = None collections_removed: Annotated[ - SchemaInt | None, Field(description='Number of collections removed since last resolution') + SchemaInt | None, + Field(description='Number of collections removed since last resolution'), + NoExplicitNull, ] = None total_collections: Annotated[ - SchemaInt | None, Field(description='Total collections in the resolved list') + SchemaInt | None, + Field(description='Total collections in the resolved list'), + NoExplicitNull, ] = None @@ -42,14 +48,19 @@ class CollectionListChangedWebhook(AdCPBaseModel): ] event: Annotated[Literal['collection_list_changed'], Field(description='The event type')] = 'collection_list_changed' list_id: Annotated[str, Field(description='ID of the collection list that changed')] - list_name: Annotated[str | None, Field(description='Name of the collection list')] = None + list_name: Annotated[ + str | None, Field(description='Name of the collection list'), NoExplicitNull + ] = None change_summary: Annotated[ - ChangeSummary | None, Field(description='Summary of changes to the resolved list') + ChangeSummary | None, + Field(description='Summary of changes to the resolved list'), + NoExplicitNull, ] = None resolved_at: Annotated[AwareDatetime, Field(description='When the list was re-resolved')] cache_valid_until: Annotated[ AwareDatetime | None, Field(description='When the consumer should refresh from the governance agent'), + NoExplicitNull, ] = None signature: Annotated[ str, @@ -57,4 +68,4 @@ class CollectionListChangedWebhook(AdCPBaseModel): description='HMAC-SHA256 webhook signature over {unix_timestamp}.{raw_http_body_bytes} using the secret exchanged out-of-band when the seller registered with the governance agent. Recipients MUST verify against the X-ADCP-Signature and X-ADCP-Timestamp headers using timing-safe comparison and MUST reject requests where |now - timestamp| > 300 seconds. The body copy of this field is a convenience only — the headers are authoritative. See docs/building/by-layer/L1/security#webhook-security.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/collection_list_filters.py b/src/adcp/types/domains/collection/collection_list_filters.py index a56006edd..e643fafe4 100644 --- a/src/adcp/types/domains/collection/collection_list_filters.py +++ b/src/adcp/types/domains/collection/collection_list_filters.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: collection/collection_list_filters.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import content_rating @@ -36,6 +36,7 @@ class CollectionListFilters(AdCPBaseModel): description="Exclude collections with any of these content ratings (OR logic). This is a metadata filter on the collection's declared content_rating field — it does not evaluate episode content.", min_length=1, ), + NoExplicitNull, ] = None content_ratings_include: Annotated[ list[content_rating.ContentRating] | None, @@ -43,6 +44,7 @@ class CollectionListFilters(AdCPBaseModel): description='Include only collections with any of these content ratings (OR logic). Collections without a declared content_rating are excluded.', min_length=1, ), + NoExplicitNull, ] = None genres_exclude: Annotated[ list[str] | None, @@ -50,6 +52,7 @@ class CollectionListFilters(AdCPBaseModel): description='Exclude collections tagged with any of these genres (OR logic). Values are interpreted against genre_taxonomy when present.', min_length=1, ), + NoExplicitNull, ] = None genres_include: Annotated[ list[str] | None, @@ -57,16 +60,19 @@ class CollectionListFilters(AdCPBaseModel): description='Include only collections with any of these genres (OR logic). Collections without genre metadata are excluded. Values are interpreted against genre_taxonomy when present.', min_length=1, ), + NoExplicitNull, ] = None genre_taxonomy: Annotated[ genre_taxonomy_1.GenreTaxonomy | None, Field( description='Taxonomy for genre filter values. When present, genres_include and genres_exclude values are interpreted as taxonomy IDs.' ), + NoExplicitNull, ] = None kinds: Annotated[ list[collection_kind.CollectionKind] | None, Field(description='Filter to these collection kinds', min_length=1), + NoExplicitNull, ] = None exclude_distribution_ids: Annotated[ list[ExcludeDistributionId] | None, @@ -74,8 +80,10 @@ class CollectionListFilters(AdCPBaseModel): description='Always exclude collections with these distribution identifiers', min_length=1, ), + NoExplicitNull, ] = None production_quality: Annotated[ list[production_quality_1.ProductionQuality] | None, Field(description='Filter by production quality tier', min_length=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/collection/create_collection_list_request.py b/src/adcp/types/domains/collection/create_collection_list_request.py index c0807e20d..91a6b516d 100644 --- a/src/adcp/types/domains/collection/create_collection_list_request.py +++ b/src/adcp/types/domains/collection/create_collection_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/create_collection_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref, brand_ref @@ -26,27 +27,31 @@ class CreateCollectionListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that will own the list. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. When omitted, this task applies its task-local single-account shortcut: if exactly one account is accessible to the authenticated caller, the seller may assign the list to that account; otherwise it MUST return an account-required or ambiguous-account error. Omission MUST NOT mean an undocumented credential-local default account.' ), + NoExplicitNull, ] = None name: Annotated[str, Field(description='Human-readable name for the list')] - description: Annotated[str | None, Field(description="Description of the list's purpose")] = ( - None - ) + description: Annotated[ + str | None, Field(description="Description of the list's purpose"), NoExplicitNull + ] = None base_collections: Annotated[ list[base_collection_source.BaseCollectionSource] | None, Field( description="Array of collection sources to evaluate. Each entry is a discriminated union: distribution_ids (platform-independent identifiers), publisher_collections (publisher_domain + collection_ids), or publisher_genres (publisher_domain + genres). If omitted, queries the agent's entire collection database.", min_length=1, ), + NoExplicitNull, ] = None filters: Annotated[ collection_list_filters.CollectionListFilters | None, Field(description='Dynamic filters to apply when resolving the list'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference. When provided, the agent automatically applies appropriate rules based on brand characteristics (industry, target_audience, etc.). Resolved at execution time.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -57,5 +62,5 @@ class CreateCollectionListRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/create_collection_list_response.py b/src/adcp/types/domains/collection/create_collection_list_response.py index 2c6cd87aa..fe0deed94 100644 --- a/src/adcp/types/domains/collection/create_collection_list_response.py +++ b/src/adcp/types/domains/collection/create_collection_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/create_collection_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -35,6 +36,7 @@ class CreateCollectionListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEn Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/delete_collection_list_request.py b/src/adcp/types/domains/collection/delete_collection_list_request.py index bba2ff419..843b45e15 100644 --- a/src/adcp/types/domains/collection/delete_collection_list_request.py +++ b/src/adcp/types/domains/collection/delete_collection_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/delete_collection_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -26,9 +27,10 @@ class DeleteCollectionListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/collection/delete_collection_list_response.py b/src/adcp/types/domains/collection/delete_collection_list_response.py index ebb56a177..a700dade7 100644 --- a/src/adcp/types/domains/collection/delete_collection_list_response.py +++ b/src/adcp/types/domains/collection/delete_collection_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/delete_collection_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -27,6 +28,7 @@ class DeleteCollectionListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEn Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/get_collection_list_request.py b/src/adcp/types/domains/collection/get_collection_list_request.py index bebff05ef..668048a89 100644 --- a/src/adcp/types/domains/collection/get_collection_list_request.py +++ b/src/adcp/types/domains/collection/get_collection_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/get_collection_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -24,10 +24,12 @@ class Pagination(AdCPBaseModel): max_results: Annotated[ SchemaInt | None, Field(description='Maximum number of collections to return per page', ge=1, le=10000), + NoExplicitNull, ] = 1000 cursor: Annotated[ str | None, Field(description='Opaque cursor from a previous response to fetch the next page'), + NoExplicitNull, ] = None @@ -41,18 +43,21 @@ class GetCollectionListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts and the list_id is not globally unique within that scope; optional otherwise.' ), + NoExplicitNull, ] = None resolve: Annotated[ StrictBool | None, Field( description='Whether to apply filters and return resolved collections (default: true)' ), + NoExplicitNull, ] = True pagination: Annotated[ Pagination | None, Field( description='Pagination parameters. Uses higher limits than standard pagination because collection lists can contain thousands of entries.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/get_collection_list_response.py b/src/adcp/types/domains/collection/get_collection_list_response.py index 8dc7c23c4..3957e7b8f 100644 --- a/src/adcp/types/domains/collection/get_collection_list_response.py +++ b/src/adcp/types/domains/collection/get_collection_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/get_collection_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import content_rating as content_rating_1 @@ -46,33 +46,42 @@ class Collection(AdCPBaseModel): Field( description='Registry-assigned stable identifier for this collection. Present when the collection has been registered in the collection registry.' ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, Field( description='Domain of the adagents.json that declares this collection — the canonical namespace for collection_id. SHOULD be returned whenever known: together with collection_id it is the domain-qualified identity that makes a resolved row matchable when no platform-independent distribution identifier exists (e.g. a channel known only by a platform_channel_id).' ), + NoExplicitNull, ] = None collection_id: Annotated[ str | None, Field( description="Publisher-assigned collection ID within publisher_domain's adagents.json. SHOULD be returned whenever known; identity is the tuple (publisher_domain, collection_id)." ), + NoExplicitNull, ] = None name: Annotated[str, Field(description='Human-readable collection name')] distribution_ids: Annotated[ list[DistributionId] | None, Field(description='Platform-independent identifiers for cross-publisher matching'), + NoExplicitNull, ] = None content_rating: Annotated[ content_rating_1.ContentRating | None, Field(description='Baseline content rating for this collection'), + NoExplicitNull, + ] = None + genre: Annotated[ + list[str] | None, Field(description='Genre tags for this collection'), NoExplicitNull ] = None - genre: Annotated[list[str] | None, Field(description='Genre tags for this collection')] = None genre_taxonomy: Annotated[ - genre_taxonomy_1.GenreTaxonomy | None, Field(description='Taxonomy system for genre values') + genre_taxonomy_1.GenreTaxonomy | None, + Field(description='Taxonomy system for genre values'), + NoExplicitNull, ] = None - kind: collection_kind.CollectionKind | None = None + kind: Annotated[collection_kind.CollectionKind | None, NoExplicitNull] = None class GetCollectionListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): @@ -88,22 +97,25 @@ class GetCollectionListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvel Field( description='Resolved collections that passed filters (if resolve=true). Each entry contains identification and key metadata for seller matching.' ), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None resolved_at: Annotated[ - AwareDatetime | None, Field(description='When the list was resolved') + AwareDatetime | None, Field(description='When the list was resolved'), NoExplicitNull ] = None cache_valid_until: Annotated[ AwareDatetime | None, Field( description='Cache expiration timestamp. Re-fetch the list after this time to get updated collections.' ), + NoExplicitNull, ] = None coverage_gaps: Annotated[ dict[str, list[CoverageGap]] | None, Field( description="Collections included in the list despite missing metadata for a filtered dimension. Maps dimension name (e.g., 'genre', 'content_rating') to arrays of distribution identifiers for collections not covered. Only present when filters are applied and some collections lack the filtered metadata." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/list_collection_lists_request.py b/src/adcp/types/domains/collection/list_collection_lists_request.py index 24f14a365..3f660a62b 100644 --- a/src/adcp/types/domains/collection/list_collection_lists_request.py +++ b/src/adcp/types/domains/collection/list_collection_lists_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/list_collection_lists_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -26,10 +27,13 @@ class ListCollectionListsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Filter to lists owned by this account. When omitted, returns lists across all accounts accessible to the authenticated agent.' ), + NoExplicitNull, ] = None name_contains: Annotated[ - str | None, Field(description='Filter to lists whose name contains this string') + str | None, + Field(description='Filter to lists whose name contains this string'), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/list_collection_lists_response.py b/src/adcp/types/domains/collection/list_collection_lists_response.py index 908fa19bc..0f45dd4bb 100644 --- a/src/adcp/types/domains/collection/list_collection_lists_response.py +++ b/src/adcp/types/domains/collection/list_collection_lists_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/list_collection_lists_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -26,6 +27,6 @@ class ListCollectionListsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv list[collection_list.CollectionList], Field(description='Array of collection lists (metadata only, not resolved collections)'), ] - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/collection/update_collection_list_request.py b/src/adcp/types/domains/collection/update_collection_list_request.py index 3d80c62c0..a220c12e1 100644 --- a/src/adcp/types/domains/collection/update_collection_list_request.py +++ b/src/adcp/types/domains/collection/update_collection_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/update_collection_list_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import account_ref, brand_ref @@ -27,33 +28,38 @@ class UpdateCollectionListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), + NoExplicitNull, ] = None - name: Annotated[str | None, Field(description='New name for the list')] = None - description: Annotated[str | None, Field(description='New description')] = None + name: Annotated[str | None, Field(description='New name for the list'), NoExplicitNull] = None + description: Annotated[str | None, Field(description='New description'), NoExplicitNull] = None base_collections: Annotated[ list[base_collection_source.BaseCollectionSource] | None, Field( description='Complete replacement for the base collections list (not a patch). Each entry is a discriminated union: distribution_ids (platform-independent identifiers), publisher_collections (publisher_domain + collection_ids), or publisher_genres (publisher_domain + genres).' ), + NoExplicitNull, ] = None filters: Annotated[ collection_list_filters.CollectionListFilters | None, Field(description='Complete replacement for the filters (not a patch)'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Update brand reference. Resolved to full brand identity at execution time.' ), + NoExplicitNull, ] = None webhook_url: Annotated[ AnyUrl | None, Field( description='Update the webhook URL for list change notifications (set to empty string to remove). Governance agents MUST validate this URL against SSRF per docs/building/by-layer/L1/security#webhook-url-validation-ssrf.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/collection/update_collection_list_response.py b/src/adcp/types/domains/collection/update_collection_list_response.py index 5f3545445..2495a5fdd 100644 --- a/src/adcp/types/domains/collection/update_collection_list_response.py +++ b/src/adcp/types/domains/collection/update_collection_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: collection/update_collection_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -29,6 +30,7 @@ class UpdateCollectionListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEn Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/compliance/comply_test_controller_request.py b/src/adcp/types/domains/compliance/comply_test_controller_request.py index e5dc81d9f..67fad2e4e 100644 --- a/src/adcp/types/domains/compliance/comply_test_controller_request.py +++ b/src/adcp/types/domains/compliance/comply_test_controller_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: compliance/comply_test_controller_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat @@ -112,6 +112,7 @@ class ReachWindow(AdCPBaseModel): Field( description="Duration of the measurement window. REQUIRED when kind is `period` or `rolling`. Matches the `period` field in delivery-metrics.json's reach_window block." ), + NoExplicitNull, ] = None @@ -133,6 +134,7 @@ class NotYetMeasurableVendorMetric(AdCPBaseModel): Field( description='Qualifier for the exact committed vendor-metric row whose measurement is deferred.' ), + NoExplicitNull, ] = None @@ -149,27 +151,33 @@ class Params(AdCPBaseModel): Field( description='Creative to transition (force_creative_status) or seed (seed_creative).' ), + NoExplicitNull, ] = None account_id: Annotated[ str | None, Field( description='Account to transition, simulate, or seed. Used by force_account_status, simulate_budget_spend, and seed_account.' ), + NoExplicitNull, ] = None media_buy_id: Annotated[ str | None, Field( description='Media buy to transition or purge (force_media_buy_status, force_media_buy_purge), simulate (simulate_delivery, simulate_budget_spend), or seed (seed_media_buy).' ), + NoExplicitNull, ] = None session_id: Annotated[ - str | None, Field(description='Session to transition. Used by force_session_status.') + str | None, + Field(description='Session to transition. Used by force_session_status.'), + NoExplicitNull, ] = None product_id: Annotated[ str | None, Field( description='Product to seed or prepare for deterministic compact lifecycle testing. Used by seed_product, seed_pricing_option, compact_product_lifecycle_probe prepare, and compact_direct_buy_lifecycle_probe prepare.' ), + NoExplicitNull, ] = None proposal_id: Annotated[ str | None, @@ -177,34 +185,43 @@ class Params(AdCPBaseModel): description='Committed proposal whose hold the compact_product_lifecycle_probe expires.', min_length=1, ), + NoExplicitNull, ] = None pricing_option_id: Annotated[ str | None, Field( description='Pricing option to seed, scoped to a product. Used by seed_pricing_option.' ), + NoExplicitNull, + ] = None + plan_id: Annotated[ + str | None, Field(description='Plan to seed. Used by seed_plan.'), NoExplicitNull ] = None - plan_id: Annotated[str | None, Field(description='Plan to seed. Used by seed_plan.')] = None rights_id: Annotated[ - str | None, Field(description='Rights grant to seed. Used by seed_rights_grant.') + str | None, + Field(description='Rights grant to seed. Used by seed_rights_grant.'), + NoExplicitNull, ] = None fixture: Annotated[ dict[str, Any] | None, Field( description='Arbitrary fixture payload carried by seed_* scenarios. Shape matches the domain object the seed scenario creates (account, product, creative, plan, media buy, pricing option, rights grant). Seller MAY reject malformed fixtures with INVALID_PARAMS. Kept permissive so storyboard authors can declare the minimum shape each test needs without the spec locking down every field.' ), + NoExplicitNull, ] = None operation: Annotated[ Operation | None, Field( description="Scenario-specific probe operation. catalog_item_availability_probe uses seed_inaccessible_item, query_eligibility, advance_time, and recreate_catalog. compact_product_lifecycle_probe uses prepare to make one seeded product's compact proposal, acceptance, operational-control, and MediaBuy readback path deterministic and expire_proposal to advance strictly beyond a committed proposal's stored expires_at and process the hold lapse. compact_direct_buy_lifecycle_probe uses prepare to make one seeded product's list, direct-purchase, operational-control, and readback path deterministic. reporting_core_lifecycle_probe uses prepare, advance_time, publish_zero_row, publish_nonempty, restate_snapshot, restate_after_received, omit_obligation, advance_past_status_deadline, and advance_past_escalation to exercise obligation availability, health deadlines, explicit reporting, provisional restatement, stale-received grace, buyer-side missing-obligation detection, counted consumer-status silence, and consumer-mismatch escalation. advance_past_status_deadline moves the clock strictly past expected_at plus the advertised automated_recovery_window_seconds without recording any consumer status, so obligation_counts.consumer_status_pending is gradable; advance_past_escalation moves it strictly past the open CONSUMER_STATUS_MISMATCH issue opened_at plus the advertised consumer_mismatch_escalation_seconds and returns both that boundary and any still-future stale_received_grace_deadline, so the escalation precedence rule is gradable. restate_snapshot publishes a new snapshot revision that immediately supersedes the current snapshot for the same logical slice and is invalid when the current revision is official. restate_after_received does the same restatement but only against the revision named in received_reporting_revision_id, so the stale-received grace projection can be graded live; it returns the grace deadline and advance_to positions the virtual clock inside or past it. Reliable Reporting tier probes use prepare plus publish_official_adjustment, probe_scheduler_dst, suppress_readiness, advance_within_retention, revoke_access, or publish_adjustment to seed deterministic Core-integrity, Managed Delivery, and Reconciled Billing lifecycle evidence." ), + NoExplicitNull, ] = None target_health: Annotated[ TargetHealth | None, Field( description='Clock-derived health boundary selected by reporting_core_lifecycle_probe advance_time. The controller chooses the corresponding deterministic target time; callers do not supply timestamps.' ), + NoExplicitNull, ] = None received_reporting_revision_id: Annotated[ str | None, @@ -214,12 +231,14 @@ class Params(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None advance_to: Annotated[ AdvanceTo | None, Field( description='Where reporting_core_lifecycle_probe restate_after_received leaves the virtual clock relative to the returned stale_received_grace_deadline. within_grace grades the delayed projection; past_grace grades the escalation to action_required. Repeating the operation is convergent: it reuses the committed restatement and only moves the clock.' ), + NoExplicitNull, ] = AdvanceTo.within_grace catalog_id: Annotated[ str | None, @@ -228,6 +247,7 @@ class Params(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None catalog_generation: Annotated[ str | None, @@ -236,6 +256,7 @@ class Params(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None item_id: Annotated[ str | None, @@ -244,55 +265,66 @@ class Params(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None target_time: Annotated[ AwareDatetime | None, Field( description='Deterministic sandbox clock target used by catalog_item_availability_probe advance_time.' ), + NoExplicitNull, ] = None status: Annotated[ str | None, Field( description="Target status for the resource. Type depends on scenario: creative-status for force_creative_status, account-status for force_account_status, media-buy-status for force_media_buy_status. For force_session_status, must be 'complete' or 'terminated'." ), + NoExplicitNull, ] = None rejection_reason: Annotated[ str | None, Field( description='Reason for rejection. Used by force_creative_status and force_media_buy_status when status = rejected.' ), + NoExplicitNull, ] = None purge_kind: Annotated[ PurgeKind | None, Field( description='Purge mode for force_creative_purge. soft retains a list_creatives tombstone; hard removes the creative entirely.' ), + NoExplicitNull, ] = None reason_code: Annotated[ creative_event_reason_code.CreativeEventReasonCode | None, Field( description='Creative lifecycle reason code for force_creative_status and force_creative_purge.' ), + NoExplicitNull, ] = None reason_detail: Annotated[ str | None, Field( description='Human-readable detail for force_creative_status and force_creative_purge.' ), + NoExplicitNull, ] = None termination_reason: Annotated[ str | None, Field( description='Reason for termination (e.g., session_timeout, host_terminated, policy_violation). Used by force_session_status when status = terminated.' ), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt | None, Field(description='Impressions to simulate. Used by simulate_delivery.', ge=0), + NoExplicitNull, ] = None clicks: Annotated[ - SchemaInt | None, Field(description='Clicks to simulate. Used by simulate_delivery.', ge=0) + SchemaInt | None, + Field(description='Clicks to simulate. Used by simulate_delivery.', ge=0), + NoExplicitNull, ] = None plays: Annotated[ SchemaInt | None, @@ -300,22 +332,26 @@ class Params(AdCPBaseModel): description='Raw DOOH or broadcast plays to add to delivery. Used by simulate_delivery for single-package buys; sellers MUST surface the cumulative value at totals.plays and by_package[0].plays in the next get_media_buy_delivery response. Multi-package simulations require a future package-scoped form and MUST reject this media-buy-scoped field.', ge=0, ), + NoExplicitNull, ] = None dooh_metrics: Annotated[ delivery_metrics.DoohMetrics | None, Field( description='DOOH delivery detail to inject. Used by simulate_delivery for single-package buys; the latest injected block replaces the previous simulated DOOH detail and MUST surface at totals.dooh_metrics and by_package[0].dooh_metrics. Multi-package simulations require a future package-scoped form and MUST reject this media-buy-scoped field.' ), + NoExplicitNull, ] = None conversions: Annotated[ SchemaInt | None, Field(description='Conversions to simulate. Used by simulate_delivery.', ge=0), + NoExplicitNull, ] = None delivery_date: Annotated[ date | None, Field( description="Calendar date, in the product's reporting_capabilities.timezone, attributable to this simulated delivery batch. Used by simulate_delivery to seed deterministic date-range tests. When get_media_buy_delivery supplies start_date or end_date, dated batches are included when delivery_date is greater than or equal to start_date and strictly less than end_date. Omit to preserve cumulative, unfiltered simulation behavior." ), + NoExplicitNull, ] = None conversion_value: Annotated[ StrictFloat | None, @@ -323,6 +359,7 @@ class Params(AdCPBaseModel): description='Total attributed conversion value to simulate in the delivery reporting currency. Used by simulate_delivery.', ge=0.0, ), + NoExplicitNull, ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -330,12 +367,14 @@ class Params(AdCPBaseModel): description='Settled attributed value eligible for revenue-share commission to simulate in the delivery reporting currency. Used by simulate_delivery.', ge=0.0, ), + NoExplicitNull, ] = None reported_spend: Annotated[ ReportedSpend | None, Field( description='Spend as reported in delivery data. Does not affect budget. Used by simulate_delivery.' ), + NoExplicitNull, ] = None reach: Annotated[ StrictFloat | None, @@ -343,6 +382,7 @@ class Params(AdCPBaseModel): description="Unique reach count to inject into the simulated delivery row. Used by simulate_delivery. The unit of measurement matches the reach_unit declared on the media buy's optimization goal. When supplied, sellers MUST surface this value at `totals.reach` in the next get_media_buy_delivery response.", ge=0.0, ), + NoExplicitNull, ] = None frequency: Annotated[ StrictFloat | None, @@ -350,48 +390,56 @@ class Params(AdCPBaseModel): description='Average frequency per reach unit to inject. Used by simulate_delivery. When supplied, sellers MUST surface this value at `totals.frequency`, including frequency-only delivery. The measurement window for this frequency value is declared in `reach_window` when also present.', ge=0.0, ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description='Unit for an injected reach or frequency value. Used by simulate_delivery. When supplied with reach or frequency, sellers MUST surface it on the corresponding delivery row.' ), + NoExplicitNull, ] = None reach_window: Annotated[ ReachWindow | None, Field( description='Measurement window semantics to attach to the simulated reach and frequency values. Used by simulate_delivery. When present, sellers MUST populate `reach_window` on the delivery row so buyers can determine the window semantics (cumulative vs. period vs. rolling). Omitting this param produces a row with no `reach_window` — valid per schema but sum-unsafe. Mirrors the `reach_window` shape in delivery-metrics.json.' ), + NoExplicitNull, ] = None viewability: Annotated[ delivery_metrics.Viewability | None, Field( description='Viewability metrics to inject into simulated delivery. Uses the canonical delivery viewability shape, including viewed-seconds distributions. This media-buy-scoped form is valid only for a single-package buy, where the reference seller surfaces the same values at package and media-buy totals grain.' ), + NoExplicitNull, ] = None vendor_metric_values: Annotated[ list[vendor_metric_value.VendorMetricValue] | None, Field( description="Vendor-defined metric values to inject into the next delivery report. Used by simulate_delivery. The reference seller reconciles these rows against each package's seller-stamped committed_metrics contract." ), + NoExplicitNull, ] = None vendor_metric_values_by_package: Annotated[ dict[str, list[vendor_metric_value.VendorMetricValue]] | None, Field( description='Package-scoped vendor-defined values to inject, keyed by package_id. Used by simulate_delivery for multi-package buys. Sellers MUST use this form whenever more than one package could carry the same vendor/metric_id key; the legacy media-buy-scoped vendor_metric_values form is unambiguous only for a single-package buy.' ), + NoExplicitNull, ] = None not_yet_measurable_vendor_metrics: Annotated[ list[NotYetMeasurableVendorMetric] | None, Field( description='Committed vendor metrics that are not yet measurable for the simulated measurement window. Used by simulate_delivery to prove that a future metric is omitted from missing_metrics until its measurement window matures.' ), + NoExplicitNull, ] = None not_yet_measurable_vendor_metrics_by_package: Annotated[ dict[str, list[NotYetMeasurableVendorMetricsByPackageItem]] | None, Field( description="Package-scoped committed vendor metrics that are not yet measurable for the simulated window, keyed by package_id. Used by simulate_delivery for multi-package buys so a deferral on one package never suppresses another package's overdue gap." ), + NoExplicitNull, ] = None spend_percentage: Annotated[ StrictFloat | None, @@ -400,12 +448,14 @@ class Params(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None arm: Annotated[ Arm | None, Field( description="Response arm for the next forced operation call. Used by force_create_media_buy_arm, force_get_products_arm, force_get_signals_arm, and force_get_creative_features_arm. 'submitted' is supported for all four operations; get_creative_features also supports 'completed' with a deterministic result; create_media_buy also supports 'input-required'; get_products also supports 'rejected'. Async completion after a submitted task is covered by force_task_completion; 'working' is an out-of-band progress signal, not an initial response arm." ), + NoExplicitNull, ] = None task_id: Annotated[ str | None, @@ -413,6 +463,7 @@ class Params(AdCPBaseModel): description="Deterministic task handle the seller MUST emit verbatim on the next forced operation response when arm is 'submitted'. The seller MUST accept this exact value on subsequent tasks/get or get_task_status calls within the same authenticated sandbox account + principal pair and MUST return REFERENCE_NOT_FOUND for the same task_id under any other account or principal. Sandbox task_ids are caller-opaque strings - the seller's production task-id format rules do not apply.", max_length=128, ), + NoExplicitNull, ] = None evaluation_id: Annotated[ str | None, @@ -421,6 +472,7 @@ class Params(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None message: Annotated[ str | None, @@ -428,6 +480,7 @@ class Params(AdCPBaseModel): description='Optional human-readable explanation surfaced on the next forced operation response. Used by force_create_media_buy_arm, force_get_products_arm, force_get_signals_arm, and force_get_creative_features_arm for the submitted arm. Plain text only.', max_length=2000, ), + NoExplicitNull, ] = None reason: Annotated[ str | None, @@ -436,6 +489,7 @@ class Params(AdCPBaseModel): max_length=2000, min_length=1, ), + NoExplicitNull, ] = None suggestions: Annotated[ list[Suggestion] | None, @@ -444,6 +498,7 @@ class Params(AdCPBaseModel): max_length=20, min_length=1, ), + NoExplicitNull, ] = None format_id: Annotated[ str | None, @@ -451,12 +506,14 @@ class Params(AdCPBaseModel): deprecated=True, description='**DEPRECATED 3.x compatibility fixture.** Legacy named-format ID to seed for seed_creative_format scenarios. The seller MUST expose it through the deprecated list_creative_formats projection for the duration of the compliance session. Canonical 3.2 scenarios seed creative.supported_formats capabilities instead.', ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference | None, Field( description="Measurement vendor whose catalog is being seeded. Used by seed_measurement_catalog. The seller MUST treat `metrics[]` as this vendor's `get_adcp_capabilities.measurement.metrics[]` snapshot for the duration of the compliance session." ), + NoExplicitNull, ] = None metrics: Annotated[ list[Metric] | None, @@ -464,18 +521,21 @@ class Params(AdCPBaseModel): description='Measurement catalog entries to seed for the vendor. Used by seed_measurement_catalog. Shape mirrors `get_adcp_capabilities.measurement.metrics[]`; each metric_id is unique within the seeded vendor catalog.', min_length=1, ), + NoExplicitNull, ] = None tool: Annotated[ str | None, Field( description="Tool name whose upstream dependency to affect. Used by force_upstream_unavailable. The seller marks the named upstream as unreachable for subsequent calls to this tool within the compliance session. Sellers MAY accept a wildcard ('*') to affect all tools." ), + NoExplicitNull, ] = None upstream_name: Annotated[ str | None, Field( description="Human-readable identifier for the upstream dependency to force unavailable (e.g., 'inventory-service', 'creative-agent'). Used by force_upstream_unavailable. When omitted, the seller marks its default upstream for the specified tool as unavailable. Sellers MUST include the same name in STALE_RESPONSE error.details.upstream.name on the affected response." ), + NoExplicitNull, ] = None cache_age_seconds: Annotated[ SchemaInt | None, @@ -483,12 +543,14 @@ class Params(AdCPBaseModel): description='Deterministic age of the stale cache entry used by force_upstream_unavailable. When supplied, the seller MUST emit this exact value in the matching STALE_RESPONSE error.details.cache_age_seconds on the next affected response. This lets the conformance runner bind the advisory code and its required detail fields to the same errors[] entry without assuming array order.', ge=0, ), + NoExplicitNull, ] = None result: Annotated[ task_completion_data.ComplianceTaskCompletionData | None, Field( description="Deterministic result payload. force_get_creative_features_arm uses it when arm is completed and returns it on the next matching operation call. force_task_completion records it against an existing task and supports the bounded get_products, get_signals, create_media_buy, and get_creative_features completion union; polling and production SDKs resolve all task results through the originating task's manifest response mapping instead of embedding a global result union. The seller MUST preserve caller-supplied fields. Sellers MUST emit INVALID_PARAMS when the payload does not match the required response branch and MAY reject payloads exceeding 256 KB." ), + NoExplicitNull, ] = None @@ -507,9 +569,10 @@ class ComplyTestControllerRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Scenario-specific parameters. Required for all scenarios except list_scenarios.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None account: Annotated[ Account | None, Field( diff --git a/src/adcp/types/domains/compliance/comply_test_controller_response.py b/src/adcp/types/domains/compliance/comply_test_controller_response.py index b4a45b863..24481ec89 100644 --- a/src/adcp/types/domains/compliance/comply_test_controller_response.py +++ b/src/adcp/types/domains/compliance/comply_test_controller_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: compliance/comply_test_controller_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool @@ -47,6 +47,7 @@ class Forced(AdCPBaseModel): description="Echo of the registered task_id. Present only when arm is 'submitted' (the arm that emits a task envelope).", max_length=128, ), + NoExplicitNull, ] = None evaluation_id: Annotated[ str | None, @@ -55,6 +56,7 @@ class Forced(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None reason: Annotated[ str | None, @@ -63,6 +65,7 @@ class Forced(AdCPBaseModel): max_length=2000, min_length=1, ), + NoExplicitNull, ] = None suggestions: Annotated[ list[Suggestion] | None, @@ -71,6 +74,7 @@ class Forced(AdCPBaseModel): max_length=20, min_length=1, ), + NoExplicitNull, ] = None @@ -139,9 +143,12 @@ class RecordedCalls1(AdCPBaseModel): Field( description='Host portion of the URL, useful for grouping calls by upstream platform.' ), + NoExplicitNull, ] = None path: Annotated[ - str | None, Field(description='Path portion of the URL (without query string).') + str | None, + Field(description='Path portion of the URL (without query string).'), + NoExplicitNull, ] = None content_type: Annotated[ str, @@ -160,6 +167,7 @@ class RecordedCalls1(AdCPBaseModel): Field( description="Optional adopter-supplied semantic tag for the call's role. Values: `platform_primary` for the primary upstream platform the adapter is integrating with (e.g., a TikTok audience-upload call from a sales-social adapter); `measurement` for ancillary calls to measurement vendors (DV, IAS, Nielsen, MOAT); `attribution` for server-side conversion APIs (TTD Trans-API, Meta CAPI, AppsFlyer/Branch postbacks) that flow alongside primary platform calls in a buy-step; `creative_serving` for ad-server / CDN / tag-build calls (GAM tag generation, VAST/CDN fetches, creative trafficking); `identity` for ID-graph / hashing-service calls (LiveRamp, ID5, UID2); `other` for everything else (config fetches, internal telemetry, consent signal exchange). Lets storyboards scope `upstream_traffic` assertions via `purpose_filter` so a buyer-agent adapter that legitimately calls measurement vendors during a single buy step doesn't muddy the platform-primary assertion. Calls without a `purpose` field are treated as `purpose: other` for `purpose_filter` matching — adopters who haven't classified are matched only by storyboards filtering on `other` (or by storyboards with no `purpose_filter`). Self-reported, not adversarially trustworthy — same trust model as the rest of recorded_calls; misclassification by a façade is bounded by the runner's reporting of unclassified-call counts in `actual` when filters match zero." ), + NoExplicitNull, ] = None payload: Annotated[ Any, @@ -174,6 +182,7 @@ class RecordedCalls1(AdCPBaseModel): description='SHA-256 digest of the canonicalized outbound request body, lowercase hex (64 chars). Required when `attestation_mode` is `digest`; MUST be absent when `attestation_mode` is `raw`. Canonicalization order is normative: (1) controllers MUST apply the recursive secret-key redaction (same pattern as the raw-mode payload field) BEFORE computing the digest; (2) for `application/json` and `*+json` content types, controllers MUST then serialize the redacted body to RFC 8785 (JCS) canonical form — sorted keys, no extraneous whitespace — and digest the resulting bytes. Storyboard-supplied identifiers (hashed PII, request correlation values) MUST NOT be redacted before digest computation; they are the load-bearing match target for `identifier_match_proofs` and digesting them away makes echo verification impossible. Both `payload_digest_sha256` AND `identifier_match_proofs` MUST be computed against the same post-redaction canonical bytes — diverging the two surfaces breaks coherence between digest replay and identifier echo. JCS edge cases: when a digest-mode `query_upstream_traffic` response cannot be produced because the parsed JSON-like value tree contains a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`), controllers MUST NOT coerce that value to `null`, a string, or any other placeholder for digest computation; they MUST return the typed ControllerError code `JCS_NON_FINITE_NUMBER`. Runners MUST grade the affected upstream_traffic digest validation `not_applicable` because RFC 8785 forbids non-finite numbers. Payloads carrying numeric identifiers MUST serialize them as JSON strings before digest computation (adtech bid payloads regularly carry IDs outside ±2^53 where I-JSON / RFC 7493 number round-tripping diverges across implementations). For non-JSON content types the digest is computed over the post-redaction raw body bytes.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None payload_length: Annotated[ SchemaInt, @@ -188,6 +197,7 @@ class RecordedCalls1(AdCPBaseModel): description='Per-identifier echo proofs for digest-mode calls. Required when `attestation_mode` is `digest` AND the request supplied `params.identifier_value_digests`; MUST be absent or empty otherwise. Each entry corresponds to one digest from the request. Capped at 64 to match the request-side `params.identifier_value_digests` cap. Lets storyboards verify `identifier_paths` echo in digest mode without ever transmitting plaintext identifiers to the controller. SHA-256 is a privacy mechanism here, not a trust mechanism — controllers self-report `found` and a determined façade can return any boolean; consumers MUST NOT treat digest-mode passing as cryptographically more trustworthy than raw mode. Tokenization is normative: for `application/json` and `*+json` content types, controllers MUST scan exactly the JSON string-typed leaf values of the post-redaction canonicalized body — no substring matching, no word splitting, no case folding, no Unicode normalization. A token matches when its SHA-256 hash equals one of the requested digests byte-for-byte. For non-JSON content types (form-urlencoded, multipart, plain text), `identifier_match_proofs` MUST be empty and runner-side `identifier_paths` assertions targeting those calls grade `not_applicable` — token boundaries are not portably defined across non-JSON shapes.', max_length=64, ), + NoExplicitNull, ] = None timestamp: Annotated[ AwareDatetime, @@ -202,6 +212,7 @@ class RecordedCalls1(AdCPBaseModel): ge=100, le=599, ), + NoExplicitNull, ] = None @@ -227,9 +238,12 @@ class RecordedCalls2(AdCPBaseModel): Field( description='Host portion of the URL, useful for grouping calls by upstream platform.' ), + NoExplicitNull, ] = None path: Annotated[ - str | None, Field(description='Path portion of the URL (without query string).') + str | None, + Field(description='Path portion of the URL (without query string).'), + NoExplicitNull, ] = None content_type: Annotated[ str, @@ -248,6 +262,7 @@ class RecordedCalls2(AdCPBaseModel): Field( description="Optional adopter-supplied semantic tag for the call's role. Values: `platform_primary` for the primary upstream platform the adapter is integrating with (e.g., a TikTok audience-upload call from a sales-social adapter); `measurement` for ancillary calls to measurement vendors (DV, IAS, Nielsen, MOAT); `attribution` for server-side conversion APIs (TTD Trans-API, Meta CAPI, AppsFlyer/Branch postbacks) that flow alongside primary platform calls in a buy-step; `creative_serving` for ad-server / CDN / tag-build calls (GAM tag generation, VAST/CDN fetches, creative trafficking); `identity` for ID-graph / hashing-service calls (LiveRamp, ID5, UID2); `other` for everything else (config fetches, internal telemetry, consent signal exchange). Lets storyboards scope `upstream_traffic` assertions via `purpose_filter` so a buyer-agent adapter that legitimately calls measurement vendors during a single buy step doesn't muddy the platform-primary assertion. Calls without a `purpose` field are treated as `purpose: other` for `purpose_filter` matching — adopters who haven't classified are matched only by storyboards filtering on `other` (or by storyboards with no `purpose_filter`). Self-reported, not adversarially trustworthy — same trust model as the rest of recorded_calls; misclassification by a façade is bounded by the runner's reporting of unclassified-call counts in `actual` when filters match zero." ), + NoExplicitNull, ] = None payload: Annotated[ Any | None, @@ -276,6 +291,7 @@ class RecordedCalls2(AdCPBaseModel): description='Per-identifier echo proofs for digest-mode calls. Required when `attestation_mode` is `digest` AND the request supplied `params.identifier_value_digests`; MUST be absent or empty otherwise. Each entry corresponds to one digest from the request. Capped at 64 to match the request-side `params.identifier_value_digests` cap. Lets storyboards verify `identifier_paths` echo in digest mode without ever transmitting plaintext identifiers to the controller. SHA-256 is a privacy mechanism here, not a trust mechanism — controllers self-report `found` and a determined façade can return any boolean; consumers MUST NOT treat digest-mode passing as cryptographically more trustworthy than raw mode. Tokenization is normative: for `application/json` and `*+json` content types, controllers MUST scan exactly the JSON string-typed leaf values of the post-redaction canonicalized body — no substring matching, no word splitting, no case folding, no Unicode normalization. A token matches when its SHA-256 hash equals one of the requested digests byte-for-byte. For non-JSON content types (form-urlencoded, multipart, plain text), `identifier_match_proofs` MUST be empty and runner-side `identifier_paths` assertions targeting those calls grade `not_applicable` — token boundaries are not portably defined across non-JSON shapes.', max_length=64, ), + NoExplicitNull, ] = None timestamp: Annotated[ AwareDatetime, @@ -290,6 +306,7 @@ class RecordedCalls2(AdCPBaseModel): ge=100, le=599, ), + NoExplicitNull, ] = None @@ -338,8 +355,8 @@ class ComplyTestControllerResponse1(ComplyTestControllerResponse): description='Scenarios this seller has implemented. Runners and sellers MUST accept unknown scenario strings (open-for-extension) — new scenarios may be added in additive releases. Adopters who advertise `catalog_item_availability_probe` support deterministic cross-principal reference, eligibility-gate, expiry-clock, and catalog-generation tests for the catalog availability storyboard. Adopters who advertise `compact_product_lifecycle_probe` support deterministic synchronous list/request/finalize/decline/accept/control/readback behavior for a prepared product and strict post-deadline expiry of a committed proposal. Adopters who advertise `compact_direct_buy_lifecycle_probe` support deterministic synchronous list/buy/control/readback behavior for a prepared product. Adopters who advertise `reporting_core_lifecycle_probe` support deterministic obligation-before-report, clock-health, zero-row reporting, provisional-restatement, and post-received restatement-grace tests without wall-clock waits. `reliable_reporting_core_integrity_probe`, `reliable_reporting_managed_delivery_probe`, and `reliable_reporting_reconciled_billing_probe` seed the source-calendar/checkpoint, managed-resource, and receipt/adjustment workflows used by the Reliable Reporting tier storyboards. Adopters who advertise `force_creative_purge` opt in to deterministic creative purge coverage for account-level lifecycle webhooks. Adopters who advertise `force_media_buy_purge` opt in to deterministic deletion-independent idempotency replay coverage. Adopters who advertise `seed_measurement_catalog` opt in to deterministic measurement-catalog fixtures used by vendor_metric precondition storyboards. Adopters who advertise `query_upstream_traffic` opt in to the upstream-traffic conformance contract; storyboards that declare `check: upstream_traffic` grade not_applicable against adopters who do not advertise it. Adopters who advertise `query_provenance_audit_observations` opt in to sandbox-only audit-observation assertions for accepted creatives. Adopters who advertise `force_upstream_unavailable` opt in to stale-cache conformance testing via the `stale_response_advisory` storyboard.' ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse2(ComplyTestControllerResponse): @@ -350,10 +367,12 @@ class ComplyTestControllerResponse2(ComplyTestControllerResponse): previous_state: Annotated[str, Field(description='State before this transition')] current_state: Annotated[str, Field(description='State after this transition')] message: Annotated[ - str | None, Field(description='Human-readable description of the transition') + str | None, + Field(description='Human-readable description of the transition'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse3(ComplyTestControllerResponse): @@ -368,10 +387,11 @@ class ComplyTestControllerResponse3(ComplyTestControllerResponse): cumulative: Annotated[ dict[str, Any] | None, Field(description='Running totals across all simulation calls (simulate_delivery only)'), + NoExplicitNull, ] = None - message: str | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + message: Annotated[str | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse4(ComplyTestControllerResponse): @@ -385,9 +405,11 @@ class ComplyTestControllerResponse4(ComplyTestControllerResponse): description='Echo of the registered directive. The next matching operation call from this sandbox account will return the named arm.' ), ] - message: Annotated[str | None, Field(description='Human-readable acknowledgement.')] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + message: Annotated[ + str | None, Field(description='Human-readable acknowledgement.'), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse5(ComplyTestControllerResponse): @@ -395,9 +417,11 @@ class ComplyTestControllerResponse5(ComplyTestControllerResponse): extra='allow', ) success: Literal[True] - message: Annotated[str | None, Field(description='Human-readable acknowledgement.')] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + message: Annotated[ + str | None, Field(description='Human-readable acknowledgement.'), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse6(ComplyTestControllerResponse): @@ -412,8 +436,8 @@ class ComplyTestControllerResponse6(ComplyTestControllerResponse): list[audit_observation.CreativeAuditObservation], Field(description='Audit observations recorded for the creative in the sandbox session.'), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse7(ComplyTestControllerResponse): @@ -439,6 +463,7 @@ class ComplyTestControllerResponse7(ComplyTestControllerResponse): Field( description='True when `total_count > recorded_calls.length`. Runners MAY raise the `limit` and re-query, but storyboards SHOULD declare assertions that fit within the default 100-call window — a truncated response is a signal that the storyboard step is causing more upstream activity than expected.' ), + NoExplicitNull, ] = None since_timestamp: Annotated[ AwareDatetime, @@ -446,8 +471,8 @@ class ComplyTestControllerResponse7(ComplyTestControllerResponse): description="Echo of the `since_timestamp` the runner requested (or the session-start timestamp the adopter substituted when the runner omitted it). Informational — the runner SHOULD use its own clock-bracket of when it issued the AdCP step request, not this echo, when attributing recorded_calls to a specific step. The controller is part of the adopter's claimed conformance; the echo is not adversarially trustworthy." ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ComplyTestControllerResponse8(ComplyTestControllerResponse): @@ -462,10 +487,10 @@ class ComplyTestControllerResponse8(ComplyTestControllerResponse): ), ] error_detail: Annotated[ - str | None, Field(description='Human-readable explanation of the failure') + str | None, Field(description='Human-readable explanation of the failure'), NoExplicitNull ] = None current_state: Annotated[ str | None, Field(description='Current state of the entity, or null if not found') ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/compliance/get_creative_features_completion.py b/src/adcp/types/domains/compliance/get_creative_features_completion.py index a7fd506c2..2206975c7 100644 --- a/src/adcp/types/domains/compliance/get_creative_features_completion.py +++ b/src/adcp/types/domains/compliance/get_creative_features_completion.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: compliance/get_creative_features_completion.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..core import creative_consumption @@ -20,7 +20,7 @@ class GetCreativeFeaturesComplianceCompletion(AdCPBaseModel): status: Literal['completed'] = 'completed' evaluation_id: Annotated[str, Field(min_length=1)] results: list[creative_feature_result.CreativeFeatureResult] - pricing_option_id: str | None = None - vendor_cost: Annotated[StrictFloat | None, Field(ge=0.0)] = None - currency: Annotated[str | None, Field(pattern='^[A-Z]{3}$')] = None - consumption: creative_consumption.CreativeConsumption | None = None + pricing_option_id: Annotated[str | None, NoExplicitNull] = None + vendor_cost: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + currency: Annotated[str | None, Field(pattern='^[A-Z]{3}$'), NoExplicitNull] = None + consumption: Annotated[creative_consumption.CreativeConsumption | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/artifact.py b/src/adcp/types/domains/content_standards/artifact.py index 82e314ab3..a74834062 100644 --- a/src/adcp/types/domains/content_standards/artifact.py +++ b/src/adcp/types/domains/content_standards/artifact.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: content_standards/artifact.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel from ..core import format_id as format_id_1 @@ -55,17 +55,19 @@ class Metadata(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - canonical: Annotated[AnyUrl | None, Field(description='Canonical URL')] = None - author: Annotated[str | None, Field(description='Artifact author name')] = None - keywords: Annotated[str | None, Field(description='Artifact keywords')] = None + canonical: Annotated[AnyUrl | None, Field(description='Canonical URL'), NoExplicitNull] = None + author: Annotated[str | None, Field(description='Artifact author name'), NoExplicitNull] = None + keywords: Annotated[str | None, Field(description='Artifact keywords'), NoExplicitNull] = None open_graph: Annotated[ - dict[str, Any] | None, Field(description='Open Graph protocol metadata') + dict[str, Any] | None, Field(description='Open Graph protocol metadata'), NoExplicitNull + ] = None + twitter_card: Annotated[ + dict[str, Any] | None, Field(description='Twitter Card metadata'), NoExplicitNull ] = None - twitter_card: Annotated[dict[str, Any] | None, Field(description='Twitter Card metadata')] = ( - None - ) json_ld: Annotated[ - list[dict[str, Any]] | None, Field(description='JSON-LD structured data (schema.org)') + list[dict[str, Any]] | None, + Field(description='JSON-LD structured data (schema.org)'), + NoExplicitNull, ] = None @@ -73,11 +75,19 @@ class Identifiers(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - apple_podcast_id: Annotated[str | None, Field(description='Apple Podcasts ID')] = None - spotify_collection_id: Annotated[str | None, Field(description='Spotify collection ID')] = None - podcast_guid: Annotated[str | None, Field(description='Podcast GUID (from RSS feed)')] = None - youtube_video_id: Annotated[str | None, Field(description='YouTube video ID')] = None - rss_url: Annotated[AnyUrl | None, Field(description='RSS feed URL')] = None + apple_podcast_id: Annotated[ + str | None, Field(description='Apple Podcasts ID'), NoExplicitNull + ] = None + spotify_collection_id: Annotated[ + str | None, Field(description='Spotify collection ID'), NoExplicitNull + ] = None + podcast_guid: Annotated[ + str | None, Field(description='Podcast GUID (from RSS feed)'), NoExplicitNull + ] = None + youtube_video_id: Annotated[ + str | None, Field(description='YouTube video ID'), NoExplicitNull + ] = None + rss_url: Annotated[AnyUrl | None, Field(description='RSS feed URL'), NoExplicitNull] = None class AssetAccess1(AdCPBaseModel): @@ -104,6 +114,7 @@ class AssetAccess2(AdCPBaseModel): deprecated=True, description="Deprecated in AdCP 3.2 and eligible for removal in 4.0 or later only after the deprecation-policy notice and release-cycle gates are satisfied. Presence denotes the legacy inline-credential form, not workload-identity authorization. New producers MUST omit this field. Consumers MUST NOT activate received credentials automatically; they may process them only for an explicitly allowlisted legacy peer and otherwise MUST reject or quarantine them. Consumers MUST redact the complete value from logs, traces, errors, metrics, analytics, model context, and durable storage. Use the consumer's pre-authorized workload identity or `signed_url` instead.", ), + NoExplicitNull, ] = None @@ -134,6 +145,7 @@ class Assets1(AdCPBaseModel): Field( description="Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." ), + NoExplicitNull, ] = None content: Annotated[ str, @@ -147,47 +159,58 @@ class Assets1(AdCPBaseModel): Field( description='MIME type indicating how to parse the content field. Default: text/plain.' ), + NoExplicitNull, ] = ContentFormat.text_plain language: Annotated[ str | None, Field( description="BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." ), + NoExplicitNull, ] = None heading_level: Annotated[ SchemaInt | None, Field(description='Heading level (1-6), only for role=heading', ge=1, le=6), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field(description='Provenance for this text block, overrides artifact-level provenance'), + NoExplicitNull, ] = None class Assets2(AdCPBaseModel): type: Literal['image'] = 'image' url: Annotated[AnyUrl, Field(description='Image URL')] - access: Annotated[AssetAccess | None, Field(description='Authentication for secured URLs')] = ( - None - ) - alt_text: Annotated[str | None, Field(description='Alt text or image description')] = None - caption: Annotated[str | None, Field(description='Image caption')] = None - width: Annotated[SchemaInt | None, Field(description='Image width in pixels')] = None - height: Annotated[SchemaInt | None, Field(description='Image height in pixels')] = None + access: Annotated[ + AssetAccess | None, Field(description='Authentication for secured URLs'), NoExplicitNull + ] = None + alt_text: Annotated[ + str | None, Field(description='Alt text or image description'), NoExplicitNull + ] = None + caption: Annotated[str | None, Field(description='Image caption'), NoExplicitNull] = None + width: Annotated[ + SchemaInt | None, Field(description='Image width in pixels'), NoExplicitNull + ] = None + height: Annotated[ + SchemaInt | None, Field(description='Image height in pixels'), NoExplicitNull + ] = None provenance: Annotated[ provenance_1.Provenance | None, Field(description='Provenance for this image, overrides artifact-level provenance'), + NoExplicitNull, ] = None class Assets3(AdCPBaseModel): type: Literal['video'] = 'video' url: Annotated[AnyUrl, Field(description='Video URL')] - access: Annotated[AssetAccess | None, Field(description='Authentication for secured URLs')] = ( - None - ) + access: Annotated[ + AssetAccess | None, Field(description='Authentication for secured URLs'), NoExplicitNull + ] = None duration_ms: Annotated[ - SchemaInt | None, Field(description='Video duration in milliseconds') + SchemaInt | None, Field(description='Video duration in milliseconds'), NoExplicitNull ] = None transcript: Annotated[ str | None, @@ -195,31 +218,38 @@ class Assets3(AdCPBaseModel): description='Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.', max_length=200000, ), + NoExplicitNull, ] = None transcript_format: Annotated[ TranscriptFormat | None, Field( description='MIME type indicating how to parse the transcript field. Default: text/plain.' ), + NoExplicitNull, ] = TranscriptFormat.text_plain transcript_source: Annotated[ - TranscriptSource | None, Field(description='How the transcript was generated') + TranscriptSource | None, + Field(description='How the transcript was generated'), + NoExplicitNull, + ] = None + thumbnail_url: Annotated[ + AnyUrl | None, Field(description='Video thumbnail URL'), NoExplicitNull ] = None - thumbnail_url: Annotated[AnyUrl | None, Field(description='Video thumbnail URL')] = None provenance: Annotated[ provenance_1.Provenance | None, Field(description='Provenance for this video, overrides artifact-level provenance'), + NoExplicitNull, ] = None class Assets4(AdCPBaseModel): type: Literal['audio'] = 'audio' url: Annotated[AnyUrl, Field(description='Audio URL')] - access: Annotated[AssetAccess | None, Field(description='Authentication for secured URLs')] = ( - None - ) + access: Annotated[ + AssetAccess | None, Field(description='Authentication for secured URLs'), NoExplicitNull + ] = None duration_ms: Annotated[ - SchemaInt | None, Field(description='Audio duration in milliseconds') + SchemaInt | None, Field(description='Audio duration in milliseconds'), NoExplicitNull ] = None transcript: Annotated[ str | None, @@ -227,19 +257,24 @@ class Assets4(AdCPBaseModel): description='Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.', max_length=200000, ), + NoExplicitNull, ] = None transcript_format: Annotated[ TranscriptFormat | None, Field( description='MIME type indicating how to parse the transcript field. Default: text/plain.' ), + NoExplicitNull, ] = TranscriptFormat.text_plain transcript_source: Annotated[ - TranscriptSource1 | None, Field(description='How the transcript was generated') + TranscriptSource1 | None, + Field(description='How the transcript was generated'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field(description='Provenance for this audio, overrides artifact-level provenance'), + NoExplicitNull, ] = None @@ -270,6 +305,7 @@ class Artifact(AdCPBaseModel): Field( description="Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." ), + NoExplicitNull, ] = None format_id: Annotated[ format_id_1.FormatReferenceStructuredObject | None, @@ -277,23 +313,29 @@ class Artifact(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format reference. Use `format_kind` for canonical artifact media classification.', ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field(description='Optional canonical media/creative kind associated with this artifact.'), + NoExplicitNull, ] = None url: Annotated[ AnyUrl | None, Field( description='Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes).' ), + NoExplicitNull, ] = None published_time: Annotated[ - AwareDatetime | None, Field(description='When the artifact was published (ISO 8601 format)') + AwareDatetime | None, + Field(description='When the artifact was published (ISO 8601 format)'), + NoExplicitNull, ] = None last_update_time: Annotated[ AwareDatetime | None, Field(description='When the artifact was last modified (ISO 8601 format)'), + NoExplicitNull, ] = None assets: Annotated[ list[Assets], @@ -303,14 +345,19 @@ class Artifact(AdCPBaseModel): ), ] metadata: Annotated[ - Metadata | None, Field(description='Rich metadata extracted from the artifact') + Metadata | None, + Field(description='Rich metadata extracted from the artifact'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact — individual assets can override with their own provenance.' ), + NoExplicitNull, ] = None identifiers: Annotated[ - Identifiers | None, Field(description='Platform-specific identifiers for this artifact') + Identifiers | None, + Field(description='Platform-specific identifiers for this artifact'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/content_standards/artifact_webhook_payload.py b/src/adcp/types/domains/content_standards/artifact_webhook_payload.py index 39c89399d..668cc3e35 100644 --- a/src/adcp/types/domains/content_standards/artifact_webhook_payload.py +++ b/src/adcp/types/domains/content_standards/artifact_webhook_payload.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: content_standards/artifact_webhook_payload.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -15,13 +15,17 @@ class Pagination(AdCPBaseModel): total_artifacts: Annotated[ - SchemaInt | None, Field(description='Total artifacts in the delivery period') + SchemaInt | None, + Field(description='Total artifacts in the delivery period'), + NoExplicitNull, ] = None batch_number: Annotated[ - SchemaInt | None, Field(description='Current batch number (1-indexed)') + SchemaInt | None, Field(description='Current batch number (1-indexed)'), NoExplicitNull ] = None total_batches: Annotated[ - SchemaInt | None, Field(description='Total batches for this delivery period') + SchemaInt | None, + Field(description='Total batches for this delivery period'), + NoExplicitNull, ] = None @@ -33,9 +37,12 @@ class Artifact(AdCPBaseModel): impression_id: Annotated[ str | None, Field(description='Optional impression identifier for correlation with delivery reports'), + NoExplicitNull, ] = None package_id: Annotated[ - str | None, Field(description='Package within the media buy this artifact relates to') + str | None, + Field(description='Package within the media buy this artifact relates to'), + NoExplicitNull, ] = None @@ -68,6 +75,8 @@ class ArtifactWebhookPayload(AdCPBaseModel): list[Artifact], Field(description='Content artifacts from delivered impressions') ] pagination: Annotated[ - Pagination | None, Field(description='Pagination info when batching large artifact sets') + Pagination | None, + Field(description='Pagination info when batching large artifact sets'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/calibrate_content_request.py b/src/adcp/types/domains/content_standards/calibrate_content_request.py index f22e59cc6..e13d9a84c 100644 --- a/src/adcp/types/domains/content_standards/calibrate_content_request.py +++ b/src/adcp/types/domains/content_standards/calibrate_content_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/calibrate_content_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..core import context as context_1 @@ -28,5 +29,5 @@ class CalibrateContentRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/content_standards.py b/src/adcp/types/domains/content_standards/content_standards.py index 18137b446..66390fa00 100644 --- a/src/adcp/types/domains/content_standards/content_standards.py +++ b/src/adcp/types/domains/content_standards/content_standards.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: content_standards/content_standards.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import Field from ..core import ext as ext_1 @@ -20,10 +20,12 @@ class CalibrationExemplars(AdCPBaseModel): pass_: Annotated[ list[artifact.Artifact] | None, Field(alias='pass', description='Artifacts that pass the content standards'), + NoExplicitNull, ] = None fail: Annotated[ list[artifact.Artifact] | None, Field(description='Artifacts that fail the content standards'), + NoExplicitNull, ] = None @@ -32,7 +34,9 @@ class ContentStandards(AdCPBaseModel): str, Field(description='Unique identifier for this standards configuration') ] name: Annotated[ - str | None, Field(description='Human-readable name for this standards configuration') + str | None, + Field(description='Human-readable name for this standards configuration'), + NoExplicitNull, ] = None countries_all: Annotated[ list[str] | None, @@ -40,6 +44,7 @@ class ContentStandards(AdCPBaseModel): description='ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic).', min_length=1, ), + NoExplicitNull, ] = None channels_any: Annotated[ list[channels.MediaChannel] | None, @@ -47,6 +52,7 @@ class ContentStandards(AdCPBaseModel): description='Advertising channels. Standards apply to ANY of the listed channels (OR logic).', min_length=1, ), + NoExplicitNull, ] = None languages_any: Annotated[ list[str] | None, @@ -54,6 +60,7 @@ class ContentStandards(AdCPBaseModel): description="BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards.", min_length=1, ), + NoExplicitNull, ] = None policies: Annotated[ list[policy_entry.PolicyEntry] | None, @@ -61,12 +68,14 @@ class ContentStandards(AdCPBaseModel): description='Bespoke policies for this content-standards configuration, using the same shape as registry entries. Each policy is addressable by policy_id; governance findings reference the policy_id that triggered them.', min_length=1, ), + NoExplicitNull, ] = None calibration_exemplars: Annotated[ CalibrationExemplars | None, Field( description='Training/test set to calibrate policy interpretation. Provides concrete examples of pass/fail decisions.' ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -74,5 +83,6 @@ class ContentStandards(AdCPBaseModel): description='Pricing options for this content standards service. The buyer passes the selected pricing_option_id in report_usage for billing verification.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/create_content_standards_request.py b/src/adcp/types/domains/content_standards/create_content_standards_request.py index e4b269173..4f6cc84e3 100644 --- a/src/adcp/types/domains/content_standards/create_content_standards_request.py +++ b/src/adcp/types/domains/content_standards/create_content_standards_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/create_content_standards_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, model_validator from ..core import context as context_1 @@ -29,6 +29,7 @@ class Scope(AdCPBaseModel): description='ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic).', min_length=1, ), + NoExplicitNull, ] = None channels_any: Annotated[ list[channels.MediaChannel] | None, @@ -36,6 +37,7 @@ class Scope(AdCPBaseModel): description='Advertising channels. Standards apply to ANY of the listed channels (OR logic).', min_length=1, ), + NoExplicitNull, ] = None languages_any: Annotated[ list[str], @@ -45,7 +47,7 @@ class Scope(AdCPBaseModel): ), ] description: Annotated[ - str | None, Field(description='Human-readable description of this scope') + str | None, Field(description='Human-readable description of this scope'), NoExplicitNull ] = None @@ -58,7 +60,7 @@ class Pass(AdCPBaseModel): ), ] language: Annotated[ - str | None, Field(description='BCP 47 language tag for content at this URL') + str | None, Field(description='BCP 47 language tag for content at this URL'), NoExplicitNull ] = None @@ -71,7 +73,7 @@ class Fail(AdCPBaseModel): ), ] language: Annotated[ - str | None, Field(description='BCP 47 language tag for content at this URL') + str | None, Field(description='BCP 47 language tag for content at this URL'), NoExplicitNull ] = None @@ -79,9 +81,12 @@ class CalibrationExemplars(AdCPBaseModel): pass_: Annotated[ list[Pass | artifact.Artifact] | None, Field(alias='pass', description='Content that passes the standards'), + NoExplicitNull, ] = None fail: Annotated[ - list[Fail | artifact.Artifact] | None, Field(description='Content that fails the standards') + list[Fail | artifact.Artifact] | None, + Field(description='Content that fails the standards'), + NoExplicitNull, ] = None @@ -95,6 +100,7 @@ class CreateContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Registry policy IDs to use as the evaluation basis for this content standard. When provided, the agent resolves policies from the registry and uses their policy text and exemplars as the evaluation criteria. The 'policy' field becomes optional when registry_policy_ids is provided." ), + NoExplicitNull, ] = None policies: Annotated[ list[policy_entry.PolicyEntry] | None, @@ -102,12 +108,14 @@ class CreateContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): description='Bespoke policies for this content-standards configuration, using the same shape as registry entries. Each policy is addressable by policy_id and carries its own enforcement (must|should); governance findings reference the policy_id that triggered them. Inline bespoke policies can omit version/name/category (defaulted by the server). Combines with registry_policy_ids — registry policies and bespoke policies are both evaluated. Bespoke policy_ids MUST be flat (no colons/slashes) to avoid collision with namespaced registry ids.', min_length=1, ), + NoExplicitNull, ] = None calibration_exemplars: Annotated[ CalibrationExemplars | None, Field( description='Training/test set to calibrate policy interpretation. Use URL references for pages to be fetched and analyzed, or full artifacts for pre-extracted content.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -118,8 +126,8 @@ class CreateContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> CreateContentStandardsRequest: diff --git a/src/adcp/types/domains/content_standards/create_content_standards_response.py b/src/adcp/types/domains/content_standards/create_content_standards_response.py index bab6dbf3c..e407ccce2 100644 --- a/src/adcp/types/domains/content_standards/create_content_standards_response.py +++ b/src/adcp/types/domains/content_standards/create_content_standards_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/create_content_standards_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,6 +10,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..core import context as context_1 @@ -34,8 +35,8 @@ class CreateContentStandardsResponse1(CreateContentStandardsResponse): standards_id: Annotated[ str, Field(description='Unique identifier for the created standards configuration') ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CreateContentStandardsResponse2(CreateContentStandardsResponse): @@ -45,6 +46,7 @@ class CreateContentStandardsResponse2(CreateContentStandardsResponse): Field( description='If the error is a scope conflict, the ID of the existing standards that conflict' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/get_content_standards_request.py b/src/adcp/types/domains/content_standards/get_content_standards_request.py index f9a21cf14..d6df04f8c 100644 --- a/src/adcp/types/domains/content_standards/get_content_standards_request.py +++ b/src/adcp/types/domains/content_standards/get_content_standards_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/get_content_standards_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..core import context as context_1 @@ -19,5 +20,5 @@ class GetContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): standards_id: Annotated[ str, Field(description='Identifier for the standards configuration to retrieve') ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/get_media_buy_artifacts_request.py b/src/adcp/types/domains/content_standards/get_media_buy_artifacts_request.py index e2128bcea..032313abd 100644 --- a/src/adcp/types/domains/content_standards/get_media_buy_artifacts_request.py +++ b/src/adcp/types/domains/content_standards/get_media_buy_artifacts_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/get_media_buy_artifacts_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..core import account_ref @@ -18,10 +18,12 @@ class TimeRange(AdCPBaseModel): - start: Annotated[AwareDatetime | None, Field(description='Start of time range (inclusive)')] = ( - None - ) - end: Annotated[AwareDatetime | None, Field(description='End of time range (exclusive)')] = None + start: Annotated[ + AwareDatetime | None, Field(description='Start of time range (inclusive)'), NoExplicitNull + ] = None + end: Annotated[ + AwareDatetime | None, Field(description='End of time range (exclusive)'), NoExplicitNull + ] = None class Pagination(AdCPBaseModel): @@ -31,10 +33,12 @@ class Pagination(AdCPBaseModel): max_results: Annotated[ SchemaInt | None, Field(description='Maximum number of artifacts to return per page', ge=1, le=10000), + NoExplicitNull, ] = 1000 cursor: Annotated[ str | None, Field(description='Opaque cursor from a previous response to fetch the next page'), + NoExplicitNull, ] = None @@ -44,26 +48,30 @@ class GetMediaBuyArtifactsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Filter artifacts to a specific account. When omitted, returns artifacts across all accessible accounts.' ), + NoExplicitNull, ] = None media_buy_id: Annotated[str, Field(description='Media buy to get artifacts from')] package_ids: Annotated[ list[str] | None, Field(description='Filter to specific packages within the media buy', min_length=1), + NoExplicitNull, ] = None failures_only: Annotated[ StrictBool | None, Field( description="When true, only return artifacts where the seller's local model returned local_verdict: 'fail'. Useful for auditing false positives. Not useful when the seller does not run a local evaluation model (all verdicts are 'unevaluated')." ), + NoExplicitNull, ] = False - time_range: Annotated[TimeRange | None, Field(description='Filter to specific time period')] = ( - None - ) + time_range: Annotated[ + TimeRange | None, Field(description='Filter to specific time period'), NoExplicitNull + ] = None pagination: Annotated[ Pagination | None, Field( description='Pagination parameters. Uses higher limits than standard pagination because artifact result sets can be very large.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/list_content_standards_request.py b/src/adcp/types/domains/content_standards/list_content_standards_request.py index 0669ef86a..632eff837 100644 --- a/src/adcp/types/domains/content_standards/list_content_standards_request.py +++ b/src/adcp/types/domains/content_standards/list_content_standards_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/list_content_standards_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -22,15 +23,20 @@ class ListContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): extra='allow', ) channels: Annotated[ - list[channels_1.MediaChannel] | None, Field(description='Filter by channel', min_length=1) + list[channels_1.MediaChannel] | None, + Field(description='Filter by channel', min_length=1), + NoExplicitNull, ] = None languages: Annotated[ - list[str] | None, Field(description='Filter by BCP 47 language tags', min_length=1) + list[str] | None, + Field(description='Filter by BCP 47 language tags', min_length=1), + NoExplicitNull, ] = None countries: Annotated[ list[str] | None, Field(description='Filter by ISO 3166-1 alpha-2 country codes', min_length=1), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/list_content_standards_response.py b/src/adcp/types/domains/content_standards/list_content_standards_response.py index 15d935636..2705a74d9 100644 --- a/src/adcp/types/domains/content_standards/list_content_standards_response.py +++ b/src/adcp/types/domains/content_standards/list_content_standards_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/list_content_standards_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,6 +10,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..core import context as context_1 @@ -34,8 +35,8 @@ def _response_arm_models(cls) -> tuple[type[ListContentStandardsResponse], ...]: class ListContentStandardsResponse2(ListContentStandardsResponse): errors: list[error.Error] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ListContentStandardsResponse1(ListContentStandardsResponse): @@ -43,6 +44,6 @@ class ListContentStandardsResponse1(ListContentStandardsResponse): list[content_standards.ContentStandards], Field(description='Array of content standards configurations matching the filter criteria'), ] - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/update_content_standards_request.py b/src/adcp/types/domains/content_standards/update_content_standards_request.py index 9c142430f..7a5d215f1 100644 --- a/src/adcp/types/domains/content_standards/update_content_standards_request.py +++ b/src/adcp/types/domains/content_standards/update_content_standards_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/update_content_standards_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import context as context_1 @@ -26,6 +26,7 @@ class Scope(AdCPBaseModel): description='ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic).', min_length=1, ), + NoExplicitNull, ] = None channels_any: Annotated[ list[channels.MediaChannel] | None, @@ -33,6 +34,7 @@ class Scope(AdCPBaseModel): description='Advertising channels. Standards apply to ANY of the listed channels (OR logic).', min_length=1, ), + NoExplicitNull, ] = None languages_any: Annotated[ list[str] | None, @@ -40,9 +42,10 @@ class Scope(AdCPBaseModel): description="BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards.", min_length=1, ), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of this scope') + str | None, Field(description='Human-readable description of this scope'), NoExplicitNull ] = None @@ -55,7 +58,7 @@ class Pass(AdCPBaseModel): ), ] language: Annotated[ - str | None, Field(description='BCP 47 language tag for content at this URL') + str | None, Field(description='BCP 47 language tag for content at this URL'), NoExplicitNull ] = None @@ -68,7 +71,7 @@ class Fail(AdCPBaseModel): ), ] language: Annotated[ - str | None, Field(description='BCP 47 language tag for content at this URL') + str | None, Field(description='BCP 47 language tag for content at this URL'), NoExplicitNull ] = None @@ -76,9 +79,12 @@ class CalibrationExemplars(AdCPBaseModel): pass_: Annotated[ list[Pass | artifact.Artifact] | None, Field(alias='pass', description='Content that passes the standards'), + NoExplicitNull, ] = None fail: Annotated[ - list[Fail | artifact.Artifact] | None, Field(description='Content that fails the standards') + list[Fail | artifact.Artifact] | None, + Field(description='Content that fails the standards'), + NoExplicitNull, ] = None @@ -90,12 +96,14 @@ class UpdateContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): scope: Annotated[ Scope | None, Field(description='Updated scope for where this standards configuration applies'), + NoExplicitNull, ] = None registry_policy_ids: Annotated[ list[str] | None, Field( description='Registry policy IDs to use as the evaluation basis. When provided, the agent resolves policies from the registry and uses their policy text and exemplars as the evaluation criteria.' ), + NoExplicitNull, ] = None policies: Annotated[ list[policy_entry.PolicyEntry] | None, @@ -103,15 +111,17 @@ class UpdateContentStandardsRequest(AdcpRequest, AdcpVersionEnvelope): description='Updated bespoke policies for this content-standards configuration, using the same shape as registry entries. Replaces the existing policies array; use stable policy_ids to track policies across versions. Combines with registry_policy_ids. Bespoke policy_ids MUST be flat (no colons/slashes).', min_length=1, ), + NoExplicitNull, ] = None calibration_exemplars: Annotated[ CalibrationExemplars | None, Field( description='Updated training/test set to calibrate policy interpretation. Use URL references for pages to be fetched and analyzed, or full artifacts for pre-extracted content.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/content_standards/update_content_standards_response.py b/src/adcp/types/domains/content_standards/update_content_standards_response.py index 1b68f3c58..1765569e7 100644 --- a/src/adcp/types/domains/content_standards/update_content_standards_response.py +++ b/src/adcp/types/domains/content_standards/update_content_standards_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/update_content_standards_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,6 +10,7 @@ from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -38,8 +39,8 @@ class UpdateContentStandardsResponse1(UpdateContentStandardsResponse): Literal[True], Field(description='Indicates the update was applied successfully') ] standards_id: Annotated[str, Field(description='ID of the updated standards configuration')] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class UpdateContentStandardsResponse2(UpdateContentStandardsResponse): @@ -55,6 +56,7 @@ class UpdateContentStandardsResponse2(UpdateContentStandardsResponse): Field( description='If scope change conflicts with another configuration, the ID of the conflicting standards' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/content_standards/validate_content_delivery_request.py b/src/adcp/types/domains/content_standards/validate_content_delivery_request.py index 0c1019bdb..3ab3a7476 100644 --- a/src/adcp/types/domains/content_standards/validate_content_delivery_request.py +++ b/src/adcp/types/domains/content_standards/validate_content_delivery_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: content_standards/validate_content_delivery_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, Field, StrictBool from ..core import context as context_1 @@ -18,8 +18,10 @@ class BrandContext(AdCPBaseModel): - brand_id: Annotated[str | None, Field(description='Brand identifier')] = None - sku_id: Annotated[str | None, Field(description='Product/SKU identifier if applicable')] = None + brand_id: Annotated[str | None, Field(description='Brand identifier'), NoExplicitNull] = None + sku_id: Annotated[ + str | None, Field(description='Product/SKU identifier if applicable'), NoExplicitNull + ] = None class Record(AdCPBaseModel): @@ -27,22 +29,28 @@ class Record(AdCPBaseModel): media_buy_id: Annotated[ str | None, Field(description='Media buy this record belongs to (when batching across multiple buys)'), + NoExplicitNull, + ] = None + timestamp: Annotated[ + AwareDatetime | None, Field(description='When the delivery occurred'), NoExplicitNull ] = None - timestamp: Annotated[AwareDatetime | None, Field(description='When the delivery occurred')] = ( - None - ) artifact: Annotated[artifact_1.Artifact, Field(description='Artifact where ad was delivered')] country: Annotated[ - str | None, Field(description='ISO 3166-1 alpha-2 country code where delivery occurred') + str | None, + Field(description='ISO 3166-1 alpha-2 country code where delivery occurred'), + NoExplicitNull, ] = None channel: Annotated[ - str | None, Field(description='Channel type (e.g., display, video, audio, social)') + str | None, + Field(description='Channel type (e.g., display, video, audio, social)'), + NoExplicitNull, ] = None brand_context: Annotated[ BrandContext | None, Field( description='Brand information for policy evaluation. Schema TBD - placeholder for brand identifiers.' ), + NoExplicitNull, ] = None @@ -57,9 +65,10 @@ class ValidateContentDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): feature_ids: Annotated[ list[str] | None, Field(description='Specific features to evaluate (defaults to all)', min_length=1), + NoExplicitNull, ] = None include_passed: Annotated[ - StrictBool | None, Field(description='Include passed records in results') + StrictBool | None, Field(description='Include passed records in results'), NoExplicitNull ] = True - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/account.py b/src/adcp/types/domains/core/account.py index 0142c4c4c..3a5bd7caa 100644 --- a/src/adcp/types/domains/core/account.py +++ b/src/adcp/types/domains/core/account.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/account.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..enums import account_scope as account_scope_1 @@ -34,10 +34,11 @@ class Setup(AdCPBaseModel): Field( description='URL where the human can complete the required action (credit application, legal agreement, add funds).' ), + NoExplicitNull, ] = None message: Annotated[str, Field(description="Human-readable description of what's needed.")] expires_at: Annotated[ - AwareDatetime | None, Field(description='When this setup link expires.') + AwareDatetime | None, Field(description='When this setup link expires.'), NoExplicitNull ] = None @@ -85,6 +86,7 @@ class ReportingBucket(AdCPBaseModel): max_length=512, pattern='^[a-zA-Z0-9/_.-]+$', ), + NoExplicitNull, ] = None region: Annotated[ str | None, @@ -94,15 +96,19 @@ class ReportingBucket(AdCPBaseModel): max_length=64, pattern='^[a-z0-9-]+$', ), + NoExplicitNull, ] = None format: Annotated[ Format | None, Field( description='File format for delivered files. Parquet, Avro, and ORC use internal compression (the top-level compression field is ignored for these formats).' ), + NoExplicitNull, ] = Format.jsonl compression: Annotated[ - Compression | None, Field(description='Compression applied to delivered files') + Compression | None, + Field(description='Compression applied to delivered files'), + NoExplicitNull, ] = Compression.gzip file_retention_days: Annotated[ SchemaInt, @@ -117,6 +123,7 @@ class ReportingBucket(AdCPBaseModel): Field( description='URL to documentation for configuring buyer read access to this bucket (IAM role, service account, etc.). Operator-facing documentation — buyer agents MUST NOT auto-fetch this URL; surface it to a human operator. If an implementation fetches it (for preview), apply webhook URL SSRF validation and do not pass the fetched content into an LLM context without indirect-prompt-injection guarding. See docs/media-buy/media-buys/optimization-reporting#security-considerations-for-offline-delivery.' ), + NoExplicitNull, ] = None @@ -129,13 +136,16 @@ class Account(AdCPBaseModel): str, Field(description="Human-readable account name (e.g., 'Acme', 'Acme c/o Pinnacle')") ] advertiser: Annotated[ - str | None, Field(description='The advertiser whose rates apply to this account') + str | None, + Field(description='The advertiser whose rates apply to this account'), + NoExplicitNull, ] = None billing_proxy: Annotated[ str | None, Field( description='Optional intermediary who receives invoices on behalf of the advertiser (e.g., agency)' ), + NoExplicitNull, ] = None status: Annotated[ account_status.AccountStatus, @@ -146,6 +156,7 @@ class Account(AdCPBaseModel): brand: Annotated[ brand_ref.BrandReference | None, Field(description='Brand reference identifying the advertiser'), + NoExplicitNull, ] = None operator: Annotated[ str | None, @@ -153,12 +164,14 @@ class Account(AdCPBaseModel): description="Domain of the entity operating this account. When the brand operates directly, this is the brand's domain.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None operator_unit: Annotated[ operator_unit_1.OperatorUnit | None, Field( description='Operator-owned business unit, agency seat, or platform account associated with this advertiser account. The id round-trips from the natural key; name is mutable display metadata. This is distinct from account_id, which belongs to the seller/storefront namespace.' ), + NoExplicitNull, ] = None revision: Annotated[ SchemaInt | None, @@ -166,12 +179,14 @@ class Account(AdCPBaseModel): description='Monotonically increasing optimistic-concurrency token for this account. Incremented on every persisted settings change, identity-change request, and identity-change disposition; reads, dry runs, validation failures, and exact idempotency replays do not increment it. Pass the latest observed value in a sync_accounts settings-update entry to prevent lost updates.', ge=1, ), + NoExplicitNull, ] = None identity_change: Annotated[ account_identity_change.AccountIdentityChange | None, Field( description='Pending or rejected operator-identity transition. While present, the top-level operator and operator_unit remain the current canonical identity. Re-read list_accounts until the request is applied (canonical fields change and this object disappears) or rejected.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -179,6 +194,7 @@ class Account(AdCPBaseModel): description="Immutable transaction currency when the seller's advertiser object is currency-bound. Media buys on this account MUST use this currency. Omit when the account selects currency independently per media buy.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, @@ -186,42 +202,50 @@ class Account(AdCPBaseModel): description='Immutable operational timezone for this account, expressed as UTC or an IANA timezone identifier. AdCP 3.2 sellers return it on every account. It is the default calendar-day boundary for account-scoped behavior unless a feature explicitly declares another timezone basis. For buyer-selected account_fixed provisioning it participates in the natural account key.', min_length=1, ), + NoExplicitNull, ] = None billing: Annotated[ billing_party.BillingParty | None, Field( description="Who is invoiced on this account. See billing_entity for the invoiced party's business details." ), + NoExplicitNull, ] = None billing_entity: Annotated[ business_entity.BusinessEntity | None, Field( description='Current canonical business entity for the party responsible for payment. Contains the legal name, tax IDs, and address needed for formal B2B invoicing. Corresponds to whoever billing points to (operator, agent, or advertiser). When this account appears in a response, bank details MUST be omitted and the request-only destination_billing_entity MUST NOT be exposed.' ), + NoExplicitNull, ] = None destination_billing_entity: Annotated[ Any | None, Field(description='Request-only staging field. It MUST NOT appear in account read models.'), + NoExplicitNull, ] = None rate_card: Annotated[ - str | None, Field(description='Identifier for the rate card applied to this account') + str | None, + Field(description='Identifier for the rate card applied to this account'), + NoExplicitNull, ] = None payment_terms: Annotated[ payment_terms_1.PaymentTerms | None, Field( description='Payment terms agreed for this account. Binding for all invoices when the account is active.' ), + NoExplicitNull, ] = None credit_limit: Annotated[ - CreditLimit | None, Field(description='Maximum outstanding balance allowed') + CreditLimit | None, Field(description='Maximum outstanding balance allowed'), NoExplicitNull ] = None setup: Annotated[ Setup | None, Field( description="Present when status is 'pending_approval'. Contains next steps for completing account activation." ), + NoExplicitNull, ] = None - account_scope: account_scope_1.AccountScope | None = None + account_scope: Annotated[account_scope_1.AccountScope | None, NoExplicitNull] = None governance_agents: Annotated[ list[GovernanceAgent] | None, Field( @@ -229,18 +253,21 @@ class Account(AdCPBaseModel): max_length=1, min_length=1, ), + NoExplicitNull, ] = None reporting_bucket: Annotated[ ReportingBucket | None, Field( description="Cloud storage bucket where the seller delivers offline reporting files for this account. Seller provisions a dedicated bucket or a per-account prefix within a shared bucket, and grants the buyer read access out-of-band. Access MUST be scoped at the IAM layer so each account can only read its own prefix — bucket-wide grants are non-compliant even with per-account prefixes. Seller MUST revoke access when the account's status transitions to inactive, suspended, or closed. See security considerations for offline delivery in docs/media-buy/media-buys/optimization-reporting. Only present when the seller supports offline delivery (reporting_delivery_methods includes 'offline' in capabilities)." ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='When true, this is a sandbox account — no real platform calls, no real spend. For account-id namespaces, sandbox accounts are pre-existing test accounts on the platform discovered via list_accounts or supplied out-of-band. For buyer-declared accounts, sandbox is part of the natural key: the same brand/operator pair can have both a production and sandbox account.' ), + NoExplicitNull, ] = None notification_configs: Annotated[ list[notification_config.NotificationConfig] | None, @@ -248,6 +275,7 @@ class Account(AdCPBaseModel): description='Account-level webhook subscriptions for creative lifecycle/assignment changes, indicators.changed, account status, durable account-change wake-ups, wholesale feed changes, and reporting.delivery_ready. Buyers manage entries via sync_accounts and verify persisted state on list_accounts. account.change_recorded wakes receivers to drain list_account_changes; reporting.delivery_ready is repaired through get_reporting_status; indicator and assignment payloads are invalidations repaired completely through get_media_buys; list_creatives may provide a bounded reverse projection. Distinct from per-resource push_notification_config. Entries are keyed by account-scoped subscriber_id; credentials are write-only.', max_length=16, ), + NoExplicitNull, ] = None reporting_delivery_configs: Annotated[ list[reporting_delivery_config_state.ReportingDeliveryConfigurationState] | None, @@ -255,6 +283,7 @@ class Account(AdCPBaseModel): description="Resolved durable reporting delivery configurations owned by the authenticated caller for this account. list_accounts MUST expose only the calling principal's set. State and seller-issued destination_ref are returned; credentials and bearer profiles MUST NOT appear. Any setup URL is a secret-free authenticated entry point, not a bearer credential.", max_length=16, ), + NoExplicitNull, ] = None webhook_activity: Annotated[ list[webhook_activity_record.WebhookActivityRecord] | None, @@ -262,5 +291,6 @@ class Account(AdCPBaseModel): description='Recent webhook delivery attempts scoped to this account when the caller requested webhook activity on list_accounts and the seller surfaces the log. Includes account-anchored notifications such as account.status_changed and MAY include other account-level fires relevant to this account. Three-state presence follows the shared webhook_activity[] contract: omitted means unsupported or not requested, [] means supported but no retained fires, non-empty lists recent attempts most-recent-first.', max_length=200, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/account_authorization.py b/src/adcp/types/domains/core/account_authorization.py index f4acc12c1..2b35f018d 100644 --- a/src/adcp/types/domains/core/account_authorization.py +++ b/src/adcp/types/domains/core/account_authorization.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/account_authorization.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool @@ -40,16 +40,19 @@ class AccountAuthorization(AdCPBaseModel): Field( description='Optional per-task allowlist of request fields the caller may set. Keys are task names (which MUST also appear in allowed_tasks). Values are arrays of top-level request-field paths permitted for that task. When a task appears in field_scopes, requests to that task with any field outside the allowlist MUST be rejected with FIELD_NOT_PERMITTED. Compact product tools use their own task names and top-level fields such as criteria and refinements. Implicit framing fields are always permitted and do NOT need to appear in the allowlist — they identify the resource or shape the call rather than mutating business state. Tasks absent from field_scopes have no field-level restriction beyond what the task schema already enforces.' ), + NoExplicitNull, ] = None scope_name: Annotated[ Literal['attestation_verifier'] | ScopeName | None, Field( description='Optional named scope identifier. When present, callers and the vendor agent can reason about the grant by name rather than by enumerating allowed_tasks and field_scopes. Modeled as a discriminated union so code generators produce a literal type for the standard scope(s) and a distinct type for agent-defined values — this prevents a typo of `attestation_verifier` from being silently accepted as a custom scope. Agent-defined scope names MUST use a `custom:` prefix to avoid collision with future standard scopes. The prefix is protocol-neutral: a signals agent, a governance agent, or a creative agent defines custom scopes the same way a media-buy sales agent does.' ), + NoExplicitNull, ] = None read_only: Annotated[ StrictBool | None, Field( description='Convenience flag. When true, the caller is permitted only non-mutating tasks. Sellers MUST reject any mutation from a read-only caller with READ_ONLY_SCOPE. For the AdCP 3.2 product split, read-only permits list_products but rejects request_proposals, refine_proposals, and decline_proposals; a legacy get_products call is permitted only when the seller can guarantee the selected arm is a synchronous side-effect-free read. Sellers MAY omit this field; omission is equivalent to `false`. Callers MUST NOT infer read-only from `allowed_tasks` alone — the seller MUST set this explicitly when it applies.' ), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/account_change.py b/src/adcp/types/domains/core/account_change.py index 2150fedd6..4d3570ff1 100644 --- a/src/adcp/types/domains/core/account_change.py +++ b/src/adcp/types/domains/core/account_change.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/account_change.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictBool @@ -49,6 +49,7 @@ class Resource(AdCPBaseModel): description='Additional canonical parent identifiers needed to repair a nested resource, for example media_buy_id and package_id for a creative assignment. Keys and values MUST NOT contain sensitive payload data.', max_length=12, ), + NoExplicitNull, ] = None @@ -71,6 +72,7 @@ class Origin(AdCPBaseModel): description='Opaque, non-secret reference to the connected source when safe for this caller.', max_length=255, ), + NoExplicitNull, ] = None @@ -115,8 +117,9 @@ class Repair(AdCPBaseModel): Field( description='False when deletion or compelled erasure makes the resource unavailable on the repair read.' ), + NoExplicitNull, ] = True - unavailable_reason: UnavailableReason | None = None + unavailable_reason: Annotated[UnavailableReason | None, NoExplicitNull] = None class Type(StrEnum): @@ -133,7 +136,9 @@ class Actor(AdCPBaseModel): ) type: Type id: Annotated[ - str | None, Field(description='Opaque, redaction-safe actor reference.', max_length=255) + str | None, + Field(description='Opaque, redaction-safe actor reference.', max_length=255), + NoExplicitNull, ] = None @@ -161,6 +166,7 @@ class AccountChange(AdCPBaseModel): Field( description='Upstream business time when the change occurred, only when the seller can establish it reliably.' ), + NoExplicitNull, ] = None batch_id: Annotated[ str | None, @@ -170,6 +176,7 @@ class AccountChange(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None resource: Annotated[ Resource, @@ -197,6 +204,7 @@ class AccountChange(AdCPBaseModel): Field( description='Post-change revision exposed by the repair read, when that resource family defines one.' ), + NoExplicitNull, ] = None changed_paths: Annotated[ list[ChangedPath] | None, @@ -204,6 +212,7 @@ class AccountChange(AdCPBaseModel): description='Bounded set of RFC 6901 JSON Pointers naming material fields that changed. Values are intentionally omitted.', max_length=64, ), + NoExplicitNull, ] = None repair: Annotated[ Repair, @@ -216,6 +225,7 @@ class AccountChange(AdCPBaseModel): Field( description='Optional privacy-safe actor classification. Sellers MUST omit direct personal identifiers unless the authenticated caller is authorized for them.' ), + NoExplicitNull, ] = None reason: Annotated[ str | None, @@ -224,6 +234,7 @@ class AccountChange(AdCPBaseModel): max_length=100, pattern='^[a-z][a-z0-9_.-]{0,99}$', ), + NoExplicitNull, ] = None summary: Annotated[ str | None, @@ -231,10 +242,12 @@ class AccountChange(AdCPBaseModel): description='Optional brief, untrusted human-readable summary. MUST NOT contain secrets or sensitive payload data.', max_length=500, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Bounded vendor extensions. The entire encoded change record, including extensions, MUST NOT exceed 64 KiB and remains subject to the same secret/PII prohibitions.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/account_change_recorded_webhook.py b/src/adcp/types/domains/core/account_change_recorded_webhook.py index 8cea2f4b8..d4570f8c7 100644 --- a/src/adcp/types/domains/core/account_change_recorded_webhook.py +++ b/src/adcp/types/domains/core/account_change_recorded_webhook.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/account_change_recorded_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import ext as ext_1 @@ -18,7 +18,7 @@ class Resource(AdCPBaseModel): ) type: Annotated[str, Field(max_length=100, min_length=1, pattern='^[a-z][a-z0-9_.-]{0,99}$')] resource_id: Annotated[str, Field(max_length=255, min_length=1)] - parent_ids: Annotated[dict[str, str] | None, Field(max_length=12)] = None + parent_ids: Annotated[dict[str, str] | None, Field(max_length=12), NoExplicitNull] = None class AccountChangeRecordedWebhook(AdCPBaseModel): @@ -65,10 +65,12 @@ class AccountChangeRecordedWebhook(AdCPBaseModel): max_length=4096, min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Bounded vendor extensions subject to the same secret/PII prohibitions as the base payload.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/account_identity_change.py b/src/adcp/types/domains/core/account_identity_change.py index dea0f8d79..69ea474ae 100644 --- a/src/adcp/types/domains/core/account_identity_change.py +++ b/src/adcp/types/domains/core/account_identity_change.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/account_identity_change.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import operator_identity @@ -25,7 +25,9 @@ class AccountIdentityChange1(AdCPBaseModel): Field(description='Complete desired operator identity submitted by the buyer.'), ] requested_at: Annotated[ - AwareDatetime | None, Field(description='When the seller recorded the request.') + AwareDatetime | None, + Field(description='When the seller recorded the request.'), + NoExplicitNull, ] = None @@ -44,7 +46,9 @@ class AccountIdentityChange2(AdCPBaseModel): Field(description='Complete desired operator identity submitted by the buyer.'), ] requested_at: Annotated[ - AwareDatetime | None, Field(description='When the seller recorded the request.') + AwareDatetime | None, + Field(description='When the seller recorded the request.'), + NoExplicitNull, ] = None reason: Annotated[ str, Field(description='Human-readable rejection reason.', max_length=1000, min_length=1) diff --git a/src/adcp/types/domains/core/account_identity_change_preview.py b/src/adcp/types/domains/core/account_identity_change_preview.py index e614355c3..d1a32b9a1 100644 --- a/src/adcp/types/domains/core/account_identity_change_preview.py +++ b/src/adcp/types/domains/core/account_identity_change_preview.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/account_identity_change_preview.json -# timestamp: 2026-10-04T00:53:27+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, RootModel @@ -42,7 +42,7 @@ class Impact(AdCPBaseModel): ) area: Area effect: Effect - reason: Annotated[str | None, Field(max_length=1000, min_length=1)] = None + reason: Annotated[str | None, Field(max_length=1000, min_length=1), NoExplicitNull] = None class Effect1(StrEnum): @@ -52,7 +52,7 @@ class Effect1(StrEnum): class NonblockingImpact(Impact): - effect: Effect1 | None = None + effect: Annotated[Effect1 | None, NoExplicitNull] = None class NonblockingImpacts(RootModel[list[NonblockingImpact]]): diff --git a/src/adcp/types/domains/core/account_ref.py b/src/adcp/types/domains/core/account_ref.py index 03e2e74ea..fe4e1f462 100644 --- a/src/adcp/types/domains/core/account_ref.py +++ b/src/adcp/types/domains/core/account_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/account_ref.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import brand_ref @@ -44,6 +44,7 @@ class AccountReference2(AdCPBaseModel): Field( description='Optional operator-owned business unit, agency seat, or platform account. Only id participates in the natural account key; name is mutable display metadata.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -51,6 +52,7 @@ class AccountReference2(AdCPBaseModel): description="Immutable ISO 4217 transaction currency when the seller's advertiser object is currency-bound. When present, this is part of the natural account key and media buys on the account MUST use it. Omit when currency is selected independently per media buy.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, @@ -58,12 +60,14 @@ class AccountReference2(AdCPBaseModel): description='Immutable account timezone. Include it in the natural key when get_adcp_capabilities.account.timezone declares account_fixed with buyer_selected; omit it for seller_fixed or seller_assigned accounts.', min_length=1, ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='When true, references the sandbox account for this brand/operator pair. Defaults to false (production account).' ), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/account_status_changed_webhook.py b/src/adcp/types/domains/core/account_status_changed_webhook.py index 4e3fbf2e9..7e9504653 100644 --- a/src/adcp/types/domains/core/account_status_changed_webhook.py +++ b/src/adcp/types/domains/core/account_status_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/account_status_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import account_status @@ -41,7 +41,9 @@ class Setup(AdCPBaseModel): ), ] expires_at: Annotated[ - AwareDatetime | None, Field(description='When the setup action or hint expires, if known.') + AwareDatetime | None, + Field(description='When the setup action or hint expires, if known.'), + NoExplicitNull, ] = None @@ -122,11 +124,13 @@ class AccountStatusChangedWebhook(AdCPBaseModel): description='Optional short human-readable detail. Treat as untrusted text. Sellers MUST NOT include secrets, setup tokens, internal stack traces, or regulated financial details.', max_length=500, ), + NoExplicitNull, ] = None setup: Annotated[ Setup | None, Field( description='Reduced setup hint when the new status requires human action. This block intentionally omits setup.url; receivers fetch the current setup URL from list_accounts if the caller is authorized to see it.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/account_timezone_capability.py b/src/adcp/types/domains/core/account_timezone_capability.py index bf7b14481..de7841c1d 100644 --- a/src/adcp/types/domains/core/account_timezone_capability.py +++ b/src/adcp/types/domains/core/account_timezone_capability.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/account_timezone_capability.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -43,12 +43,14 @@ class AccountTimezoneCapability(AdCPBaseModel): description='Seller-wide timezone used by every account. Required only for seller_fixed. Use UTC or an IANA timezone identifier.', min_length=1, ), + NoExplicitNull, ] = None account_selection: Annotated[ AccountSelection | None, Field( description='How an account_fixed timezone is established. seller_assigned covers an existing upstream account or seller onboarding choice; buyer_selected requires timezone in buyer-declared sync_accounts provisioning.' ), + NoExplicitNull, ] = None supported_timezones: Annotated[ list[SupportedTimezone] | None, @@ -56,4 +58,5 @@ class AccountTimezoneCapability(AdCPBaseModel): description='Exact timezone values accepted during buyer-selected account provisioning. Required when account_selection is buyer_selected so buyers can validate the choice before sync_accounts.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/account_with_authorization.py b/src/adcp/types/domains/core/account_with_authorization.py index 7a56c8547..2e6e91e75 100644 --- a/src/adcp/types/domains/core/account_with_authorization.py +++ b/src/adcp/types/domains/core/account_with_authorization.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/account_with_authorization.json -# timestamp: 2026-06-05T16:54:33+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from . import account_authorization @@ -18,4 +19,5 @@ class AccountWithAuthorization(Account): Field( description="Optional. The caller's scope grant against this account. Vendor agents of any type (media-buy, signals, governance, creative, brand) that support scope introspection SHOULD populate this so callers can preempt SCOPE_INSUFFICIENT / FIELD_NOT_PERMITTED errors rather than discovering scope by trial and error. Media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope for this account — callers MUST NOT infer access from absence, and fall back to error-driven discovery via the RBAC error codes." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/ad_inventory_config.py b/src/adcp/types/domains/core/ad_inventory_config.py index 40c8df5df..e7a2d2052 100644 --- a/src/adcp/types/domains/core/ad_inventory_config.py +++ b/src/adcp/types/domains/core/ad_inventory_config.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/ad_inventory_config.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -18,7 +18,9 @@ class AdInventoryConfiguration(AdCPBaseModel): SchemaInt, Field(description='Number of planned ad breaks in the installment', ge=0) ] total_ad_seconds: Annotated[ - SchemaInt | None, Field(description='Total seconds of ad time across all breaks', ge=0) + SchemaInt | None, + Field(description='Total seconds of ad time across all breaks', ge=0), + NoExplicitNull, ] = None max_ad_duration_seconds: Annotated[ SchemaInt | None, @@ -26,16 +28,19 @@ class AdInventoryConfiguration(AdCPBaseModel): description='Maximum duration in seconds for a single ad within a break. Buyers need this to know whether their creative fits.', ge=1, ), + NoExplicitNull, ] = None unplanned_breaks: Annotated[ StrictBool | None, Field( description='Whether ad breaks are dynamic and driven by live conditions (sports timeouts, election coverage). When false, all breaks are pre-defined.' ), + NoExplicitNull, ] = None supported_formats: Annotated[ list[str] | None, Field( description="Ad format types supported in breaks (e.g., 'video', 'audio', 'display')" ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/agent_notification_config.py b/src/adcp/types/domains/core/agent_notification_config.py index af2f567ad..a800e3bec 100644 --- a/src/adcp/types/domains/core/agent_notification_config.py +++ b/src/adcp/types/domains/core/agent_notification_config.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/agent_notification_config.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import auth_scheme, notification_type @@ -24,6 +24,7 @@ class Authentication(AdCPBaseModel): description='Credentials for the legacy scheme. Bearer: token. HMAC-SHA256: shared secret. Minimum 32 characters. Exchanged out-of-band during onboarding. Write-only.', min_length=32, ), + NoExplicitNull, ] = None @@ -58,12 +59,14 @@ class AgentNotificationConfig(AdCPBaseModel): Field( description="Explicit scope acknowledgment required whenever event_types includes any account-anchored type: true states that this subscriber intentionally receives those events for every account the principal is authorized for at each delivery attempt. Subscribing an endpoint to all accounts is never implicit. Authorization is evaluated per delivery attempt, including retries: losing authority for an account suppresses queued retries carrying that account's data." ), + NoExplicitNull, ] = None include_future_event_types: Annotated[ StrictBool | None, Field( description='When true, the seller also fires caller-eligible notification types added to the enum by later AdCP versions — but only types classified invalidation-only, whose payloads carry identifiers and a repair pointer rather than domain data. Payload-bearing types always require explicit enumeration in event_types; this flag never silently opts a caller into more-sensitive payloads. There is deliberately no wildcard event type. Receivers setting this MUST tolerate unknown notification_type values.' ), + NoExplicitNull, ] = False authentication: Annotated[ Authentication | None, @@ -71,11 +74,13 @@ class AgentNotificationConfig(AdCPBaseModel): deprecated=True, description="Legacy authentication selector. Same precedence and semantics as `push-notification-config.authentication` and account-level `notification-config.authentication`: presence opts the seller into Bearer or HMAC-SHA256 signing; absence selects the default RFC 9421 webhook profile keyed off the seller's brand.json `agents[]` JWKS. Deprecated; removed in AdCP 4.0. Credentials are write-only and MUST NOT be echoed on reads.", ), + NoExplicitNull, ] = None active: Annotated[ StrictBool | None, Field( description='When false, the seller persists the configuration but suppresses fires. Use to pause a subscriber without losing the registration. Paused configs may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time. Reactivation requires full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof.' ), + NoExplicitNull, ] = True - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/agent_notification_config_state.py b/src/adcp/types/domains/core/agent_notification_config_state.py index 68353b331..6eda58e1c 100644 --- a/src/adcp/types/domains/core/agent_notification_config_state.py +++ b/src/adcp/types/domains/core/agent_notification_config_state.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/agent_notification_config_state.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import auth_scheme, notification_type @@ -32,10 +32,13 @@ class AgentNotificationConfigState(AdCPBaseModel): all_authorized_accounts: Annotated[ StrictBool | None, Field(description='Echoed scope acknowledgment for account-anchored event types.'), + NoExplicitNull, ] = None include_future_event_types: Annotated[ - StrictBool | None, Field(description='Echoed from the desired configuration when set.') + StrictBool | None, + Field(description='Echoed from the desired configuration when set.'), + NoExplicitNull, ] = None - authentication: Annotated[Authentication | None, Field(deprecated=True)] = None - active: StrictBool | None = True - ext: ext_1.ExtensionObject | None = None + authentication: Annotated[Authentication | None, Field(deprecated=True), NoExplicitNull] = None + active: Annotated[StrictBool | None, NoExplicitNull] = True + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/agent_reporting_destination.py b/src/adcp/types/domains/core/agent_reporting_destination.py index d5e09f31d..0717aee2e 100644 --- a/src/adcp/types/domains/core/agent_reporting_destination.py +++ b/src/adcp/types/domains/core/agent_reporting_destination.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/agent_reporting_destination.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import delivery_provider, delivery_recipient, reporting_verification_profile_set @@ -48,6 +48,7 @@ class AgentReportingDestination1(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$', ), + NoExplicitNull, ] = None active: Annotated[ StrictBool, @@ -112,6 +113,7 @@ class AgentReportingDestination2(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$', ), + NoExplicitNull, ] = None active: Annotated[ StrictBool, @@ -170,6 +172,7 @@ class AgentReportingDestination3(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$', ), + NoExplicitNull, ] = None active: Annotated[ StrictBool, diff --git a/src/adcp/types/domains/core/agent_reporting_destination_state.py b/src/adcp/types/domains/core/agent_reporting_destination_state.py index 1cbef19d0..e213fb699 100644 --- a/src/adcp/types/domains/core/agent_reporting_destination_state.py +++ b/src/adcp/types/domains/core/agent_reporting_destination_state.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/agent_reporting_destination_state.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -37,12 +37,14 @@ class Setup(AdCPBaseModel): Field( description='HTTPS page for completing provider-native setup. Printable ASCII without whitespace, \'?\', \'#\', \'%\', \'"\', \'<\', or \'>\'. It MUST NOT carry a credential, userinfo authority, or signed component and MUST be rendered as an untrusted link, never executed as agent instructions.' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Optional expiry of this setup action. A new sync obtains a fresh action after expiry.' ), + NoExplicitNull, ] = None @@ -73,6 +75,7 @@ class AgentReportingDestinationState(AdCPBaseModel): description='Retained superseded generation references for this destination_id, newest first, still resolvable for existing authorized account bindings and retained reporting history. Enumerable only by the owning principal. Suspension and revocation of the destination apply to these generations too.', max_length=32, ), + NoExplicitNull, ] = None state: Annotated[ reporting_destination_setup_state.ReportingDestinationSetupState, @@ -91,6 +94,7 @@ class AgentReportingDestinationState(AdCPBaseModel): Field( description='Closed, non-secret setup instruction. Human-readable messages are deliberately excluded; agents dispatch only the typed action and treat setup_url as an untrusted navigation target.' ), + NoExplicitNull, ] = None issues: Annotated[ list[error.Error] | None, @@ -98,4 +102,5 @@ class AgentReportingDestinationState(AdCPBaseModel): description='Structured validation or setup issues. Messages and details are untrusted display data and MUST NOT be executed as instructions.', max_length=16, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/agent_signing_key.py b/src/adcp/types/domains/core/agent_signing_key.py index 80caf3d82..2608be649 100644 --- a/src/adcp/types/domains/core/agent_signing_key.py +++ b/src/adcp/types/domains/core/agent_signing_key.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/agent_signing_key.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field @@ -19,28 +19,40 @@ class AgentSigningKey(AdCPBaseModel): alg: Annotated[ str | None, Field(description="Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'."), + NoExplicitNull, ] = None use: Annotated[ - str | None, Field(description="Optional JWK use value. Typically 'sig' for signing keys.") + str | None, + Field(description="Optional JWK use value. Typically 'sig' for signing keys."), + NoExplicitNull, ] = None crv: Annotated[ str | None, Field(description="Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'."), + NoExplicitNull, ] = None x: Annotated[ str | None, Field( description='Base64url-encoded public key x coordinate or public key value for OKP keys.' ), + NoExplicitNull, ] = None y: Annotated[ - str | None, Field(description='Base64url-encoded public key y coordinate for EC keys.') + str | None, + Field(description='Base64url-encoded public key y coordinate for EC keys.'), + NoExplicitNull, + ] = None + n: Annotated[ + str | None, Field(description='Base64url-encoded RSA modulus.'), NoExplicitNull + ] = None + e: Annotated[ + str | None, Field(description='Base64url-encoded RSA public exponent.'), NoExplicitNull ] = None - n: Annotated[str | None, Field(description='Base64url-encoded RSA modulus.')] = None - e: Annotated[str | None, Field(description='Base64url-encoded RSA public exponent.')] = None revoked_at: Annotated[ AwareDatetime | None, Field( description='Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/agent_webhook_challenge.py b/src/adcp/types/domains/core/agent_webhook_challenge.py index 00e893df2..49ae89e97 100644 --- a/src/adcp/types/domains/core/agent_webhook_challenge.py +++ b/src/adcp/types/domains/core/agent_webhook_challenge.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/agent_webhook_challenge.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -33,6 +33,7 @@ class DeliveryAuth(AdCPBaseModel): description='SHA-256 hex fingerprint of the exact legacy credential string supplied in notification_configs[].authentication.credentials. Required when mode is Bearer or HMAC-SHA256; absent for rfc9421.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/app_item.py b/src/adcp/types/domains/core/app_item.py index ae754a723..126a56b5b 100644 --- a/src/adcp/types/domains/core/app_item.py +++ b/src/adcp/types/domains/core/app_item.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/app_item.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -47,6 +47,7 @@ class AppItem(AdCPBaseModel): Field( description="Reverse-domain bundle identifier (e.g., 'com.acmegames.puzzlequest'). The universal store identifier: required for Android (Google Play), also used for iOS MMP attribution, SKAN matching, and app-ads.txt verification. Distinct from app_id, which is a buyer-assigned catalog key." ), + NoExplicitNull, ] = None apple_id: Annotated[ str | None, @@ -54,27 +55,33 @@ class AppItem(AdCPBaseModel): description="Numeric Apple App Store ID (e.g., '389801252'). Required for Apple Search Ads and iOS platforms that use the numeric ID rather than bundle_id.", pattern='^[0-9]+$', ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='App description. Platforms typically pull this from the store listing automatically; supply here to override or for platforms that require it in the request.' ), + NoExplicitNull, ] = None category: Annotated[ str | None, Field( description="Primary store category (e.g., 'games', 'productivity', 'finance', 'health_fitness', 'social_networking')." ), + NoExplicitNull, ] = None genre: Annotated[ str | None, Field( description="Sub-genre within the category. Particularly relevant for games (e.g., 'puzzle', 'strategy', 'rpg', 'casual', 'simulation', 'action')." ), + NoExplicitNull, ] = None icon_url: Annotated[ - AnyUrl | None, Field(description='App icon image URL. Typically 1024×1024 px.') + AnyUrl | None, + Field(description='App icon image URL. Typically 1024×1024 px.'), + NoExplicitNull, ] = None screenshots: Annotated[ list[AnyUrl] | None, @@ -82,26 +89,31 @@ class AppItem(AdCPBaseModel): description='App store screenshot URLs. Used by platforms for creative generation when native store assets are not available.', min_length=1, ), + NoExplicitNull, ] = None preview_video_url: Annotated[ AnyUrl | None, Field(description='App preview or gameplay video URL for use in video ad creatives.'), + NoExplicitNull, ] = None store_url: Annotated[ AnyUrl | None, Field( description="Direct link to the app's store listing (Apple App Store or Google Play)." ), + NoExplicitNull, ] = None deep_link_url: Annotated[ AnyUrl | None, Field( description="Deep link URI for re-engagement campaigns targeting existing users. Use Universal Links (iOS) or App Links (Android) where available (e.g., 'https://acmegames.com/app/level/5'). Falls back to URI scheme (e.g., 'acmegames://level/5') when universal links are not configured." ), + NoExplicitNull, ] = None price: Annotated[ price_1.Price | None, Field(description='App download price. Set amount to 0 for free apps.'), + NoExplicitNull, ] = None rating: Annotated[ StrictFloat | None, @@ -110,15 +122,17 @@ class AppItem(AdCPBaseModel): ge=0.0, le=5.0, ), + NoExplicitNull, ] = None rating_count: Annotated[ - SchemaInt | None, Field(description='Total number of store ratings.', ge=0) + SchemaInt | None, Field(description='Total number of store ratings.', ge=0), NoExplicitNull ] = None content_rating: Annotated[ str | None, Field( description="Age or content rating (e.g., '4+', '12+', 'Everyone', 'Teen', 'PEGI 12'). Format depends on store and region." ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -126,6 +140,7 @@ class AppItem(AdCPBaseModel): description="Tags for filtering and targeting (e.g., 'multiplayer', 'offline', 'no-ads', 'subscription').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -133,5 +148,6 @@ class AppItem(AdCPBaseModel): description="Typed creative asset pools for this app. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (promotional hero), 'images_vertical' (9:16 for Snap, Stories), 'images_square' (1:1 for display), 'video' (gameplay or demo video). Supplements icon_url and screenshots for platform-specific format requirements.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/assets/audio_asset.py b/src/adcp/types/domains/core/assets/audio_asset.py index 8cb913633..01ca0c875 100644 --- a/src/adcp/types/domains/core/assets/audio_asset.py +++ b/src/adcp/types/domains/core/assets/audio_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/audio_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from enum import IntEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from ...enums import audio_channel_layout @@ -32,43 +32,52 @@ class AudioAsset(AdCPBaseModel): ] = 'audio' url: Annotated[AnyUrl, Field(description='URL to the audio asset')] duration_ms: Annotated[ - SchemaInt | None, Field(description='Audio duration in milliseconds', ge=0) + SchemaInt | None, Field(description='Audio duration in milliseconds', ge=0), NoExplicitNull + ] = None + file_size_bytes: Annotated[ + SchemaInt | None, Field(description='File size in bytes', ge=1), NoExplicitNull ] = None - file_size_bytes: Annotated[SchemaInt | None, Field(description='File size in bytes', ge=1)] = ( - None - ) container_format: Annotated[ str | None, Field(description='Audio container/file format (mp3, m4a, aac, wav, ogg, flac, etc.)'), + NoExplicitNull, ] = None codec: Annotated[ str | None, Field( description='Audio codec used (aac, aac_lc, he_aac, pcm, mp3, vorbis, opus, flac, ac3, eac3, etc.)' ), + NoExplicitNull, ] = None sampling_rate_hz: Annotated[ - SchemaInt | None, Field(description='Sampling rate in Hz (e.g., 44100, 48000, 96000)') + SchemaInt | None, + Field(description='Sampling rate in Hz (e.g., 44100, 48000, 96000)'), + NoExplicitNull, ] = None channels: Annotated[ - audio_channel_layout.AudioChannelLayout | None, Field(description='Channel configuration') + audio_channel_layout.AudioChannelLayout | None, + Field(description='Channel configuration'), + NoExplicitNull, ] = None - bit_depth: Annotated[BitDepth | None, Field(description='Bit depth')] = None + bit_depth: Annotated[BitDepth | None, Field(description='Bit depth'), NoExplicitNull] = None bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Bitrate in kilobits per second', ge=1) + SchemaInt | None, Field(description='Bitrate in kilobits per second', ge=1), NoExplicitNull ] = None loudness_lufs: Annotated[ - StrictFloat | None, Field(description='Integrated loudness in LUFS') + StrictFloat | None, Field(description='Integrated loudness in LUFS'), NoExplicitNull + ] = None + true_peak_dbfs: Annotated[ + StrictFloat | None, Field(description='True peak level in dBFS'), NoExplicitNull ] = None - true_peak_dbfs: Annotated[StrictFloat | None, Field(description='True peak level in dBFS')] = ( - None - ) transcript_url: Annotated[ - AnyUrl | None, Field(description='URL to text transcript of the audio content') + AnyUrl | None, + Field(description='URL to text transcript of the audio content'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/card_asset.py b/src/adcp/types/domains/core/assets/card_asset.py index 5d202c99b..8a1bbe58f 100644 --- a/src/adcp/types/domains/core/assets/card_asset.py +++ b/src/adcp/types/domains/core/assets/card_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/card_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import asset_union @@ -34,34 +34,40 @@ class CardAsset(AdCPBaseModel): Field( description='Optional per-card short text label (typically 25-40 chars). Length governed by `card_headline_max_chars` on the format declaration. Meta carousel headline, Pinterest pin title, Snap Collection sticker text, TikTok caption-short.' ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Optional per-card longer text (typically 100-500 chars). Distinct from `headline`: `description` is body copy, `headline` is the label. Length governed by `card_description_max_chars` on the format declaration. Meta carousel description, Pinterest pin description, AI-surface result body text, TikTok long caption.' ), + NoExplicitNull, ] = None cta: Annotated[ str | None, Field( description="Optional per-card call-to-action label (e.g., 'SHOP_NOW', 'LEARN_MORE'). When the parent format declares `cta_values` (allowed CTA labels), the per-card `cta` MUST be one of those values. Lets a Meta or TikTok carousel show different CTAs per card." ), + NoExplicitNull, ] = None landing_page_url: Annotated[ asset_union.UrlAsset | None, Field( description='Optional per-card click-through URL. URL asset with `url_type: "clickthrough"`.' ), + NoExplicitNull, ] = None platform_extensions: Annotated[ list[asset_union.PlatformExtensionRef] | None, Field( description='Per-card platform-specific extensions (URI+digest references). Same hosting model as format-level platform_extensions. Use this for Meta carousel-card attributes, Pinterest pin overrides, etc. — NEVER inline non-canonical keys on the card object directly.' ), + NoExplicitNull, ] = None provenance: Annotated[ asset_union.Provenance | None, Field( description='Provenance metadata for this card, overrides manifest-level provenance.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/css_asset.py b/src/adcp/types/domains/core/assets/css_asset.py index 41e41b5ad..4e60db723 100644 --- a/src/adcp/types/domains/core/assets/css_asset.py +++ b/src/adcp/types/domains/core/assets/css_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/css_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from .. import provenance as provenance_1 @@ -24,11 +24,14 @@ class CssAsset(AdCPBaseModel): ] = 'css' content: Annotated[str, Field(description='CSS content')] media: Annotated[ - str | None, Field(description="CSS media query context (e.g., 'screen', 'print')") + str | None, + Field(description="CSS media query context (e.g., 'screen', 'print')"), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/daast_asset.py b/src/adcp/types/domains/core/assets/daast_asset.py index cf4a9c9b9..5823c9807 100644 --- a/src/adcp/types/domains/core/assets/daast_asset.py +++ b/src/adcp/types/domains/core/assets/daast_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/daast_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, RootModel, StrictBool from ...enums import daast_tracking_event @@ -87,6 +87,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -94,6 +95,7 @@ class MacroDeclaration(AdCPBaseModel): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -108,6 +110,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -115,12 +118,14 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None location: Location encoding: Annotated[ @@ -168,6 +173,7 @@ class MacroDeclaration12(AdCPBaseModel): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -175,6 +181,7 @@ class MacroDeclaration12(AdCPBaseModel): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -189,6 +196,7 @@ class MacroDeclaration12(AdCPBaseModel): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -196,12 +204,14 @@ class MacroDeclaration12(AdCPBaseModel): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None location: Location13 encoding: Annotated[ @@ -235,27 +245,36 @@ class DaastAsset3(AdCPBaseModel): ), ] = 'daast' daast_version: Annotated[ - daast_version_1.DaastVersion | None, Field(description='DAAST specification version') + daast_version_1.DaastVersion | None, + Field(description='DAAST specification version'), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Expected audio duration in milliseconds (if known)', ge=0), + NoExplicitNull, ] = None tracking_events: Annotated[ list[daast_tracking_event.DaastTrackingEvent] | None, Field(description='Tracking events supported by this DAAST tag'), + NoExplicitNull, ] = None companion_ads: Annotated[ - StrictBool | None, Field(description='Whether companion display ads are included') + StrictBool | None, + Field(description='Whether companion display ads are included'), + NoExplicitNull, ] = None transcript_url: Annotated[ - AnyUrl | None, Field(description='URL to text transcript of the audio content') + AnyUrl | None, + Field(description='URL to text transcript of the audio content'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None macro_declarations: Annotated[ list[MacroDeclaration] | None, @@ -263,6 +282,7 @@ class DaastAsset3(AdCPBaseModel): description='One declaration per occurrence in a field carried by this asset. URL-delivered assets declare only locator-URL occurrences; receivers do not infer declarations for tokens discovered later in a fetched document.', min_length=1, ), + NoExplicitNull, ] = None delivery_type: Annotated[ Literal['url'], @@ -287,27 +307,36 @@ class DaastAsset4(AdCPBaseModel): ), ] = 'daast' daast_version: Annotated[ - daast_version_1.DaastVersion | None, Field(description='DAAST specification version') + daast_version_1.DaastVersion | None, + Field(description='DAAST specification version'), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Expected audio duration in milliseconds (if known)', ge=0), + NoExplicitNull, ] = None tracking_events: Annotated[ list[daast_tracking_event.DaastTrackingEvent] | None, Field(description='Tracking events supported by this DAAST tag'), + NoExplicitNull, ] = None companion_ads: Annotated[ - StrictBool | None, Field(description='Whether companion display ads are included') + StrictBool | None, + Field(description='Whether companion display ads are included'), + NoExplicitNull, ] = None transcript_url: Annotated[ - AnyUrl | None, Field(description='URL to text transcript of the audio content') + AnyUrl | None, + Field(description='URL to text transcript of the audio content'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None macro_declarations: Annotated[ list[MacroDeclaration12] | None, @@ -315,6 +344,7 @@ class DaastAsset4(AdCPBaseModel): description='One declaration per occurrence in a field carried by this asset. URL-delivered assets declare only locator-URL occurrences; receivers do not infer declarations for tokens discovered later in a fetched document.', min_length=1, ), + NoExplicitNull, ] = None delivery_type: Annotated[ Literal['inline'], diff --git a/src/adcp/types/domains/core/assets/daast_tracker_asset.py b/src/adcp/types/domains/core/assets/daast_tracker_asset.py index 065ec6771..81bb26308 100644 --- a/src/adcp/types/domains/core/assets/daast_tracker_asset.py +++ b/src/adcp/types/domains/core/assets/daast_tracker_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/daast_tracker_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ...enums import daast_tracking_event @@ -75,6 +75,7 @@ class DaastTrackerAsset(AdCPBaseModel): Field( description='Exact tokens in `url` and their resolver/encoding contracts.', min_length=1 ), + NoExplicitNull, ] = None offset: Annotated[ str | None, @@ -82,16 +83,19 @@ class DaastTrackerAsset(AdCPBaseModel): description='DAAST `offset` attribute. Required when `daast_event` is `progress` (DAAST 1.1 §3.2.4.3); ignored otherwise for compatibility with existing 3.x manifests. Same format as VAST 4.2 `Tracking@offset`: `HH:MM:SS` or `HH:MM:SS.mmm` for absolute time (two-digit hours, minutes 00–59, seconds 00–59), or an integer percentage 0–100 suffixed with `%`. Negative offsets are NOT permitted.', pattern='^(\\d{2}:[0-5]\\d:[0-5]\\d(\\.\\d{3})?|(100|\\d{1,2})%)$', ), + NoExplicitNull, ] = None target: Annotated[ Target | None, Field( description='Which DAAST creative element this tracker scopes to — `linear` for `/` (DAAST 1.1 §3.2.1.7), `companion` for `//` (DAAST 1.1 §3.2.2.7). DAAST has no `` element. Defaults to `linear`. Existing 3.x assets remain structurally permissive; a tracker execution contract applies the standards-valid event/target matrix when matching a creative to a product.' ), + NoExplicitNull, ] = Target.linear provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/display_tag_asset.py b/src/adcp/types/domains/core/assets/display_tag_asset.py index d5ceb0365..4423f1a7f 100644 --- a/src/adcp/types/domains/core/assets/display_tag_asset.py +++ b/src/adcp/types/domains/core/assets/display_tag_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/display_tag_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, RootModel, StrictBool from .. import macro_bearing_url @@ -85,6 +85,7 @@ class MacroDeclaration(MacroDeclaration_1): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -92,6 +93,7 @@ class MacroDeclaration(MacroDeclaration_1): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -106,6 +108,7 @@ class MacroDeclaration(MacroDeclaration_1): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -113,12 +116,14 @@ class MacroDeclaration(MacroDeclaration_1): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None encoding: Annotated[ asset_union.MacroEncoding, @@ -156,12 +161,14 @@ class DisplayTagAsset4(AdCPBaseModel): description='Exact macro tokens present anywhere in this representation. Tokens remain byte-preserved until the resolver named by each declaration substitutes them.', min_length=1, ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overriding manifest-level provenance.' ), + NoExplicitNull, ] = None delivery_type: Literal['tag_url'] = 'tag_url' url: Annotated[ @@ -198,6 +205,7 @@ class MacroDeclaration15(MacroDeclaration_1): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -205,6 +213,7 @@ class MacroDeclaration15(MacroDeclaration_1): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -219,6 +228,7 @@ class MacroDeclaration15(MacroDeclaration_1): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -226,12 +236,14 @@ class MacroDeclaration15(MacroDeclaration_1): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None encoding: Annotated[ asset_union.MacroEncoding, @@ -269,12 +281,14 @@ class DisplayTagAsset5(AdCPBaseModel): description='Exact macro tokens present anywhere in this representation. Tokens remain byte-preserved until the resolver named by each declaration substitutes them.', min_length=1, ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overriding manifest-level provenance.' ), + NoExplicitNull, ] = None delivery_type: Literal['inline_markup'] = 'inline_markup' markup_type: Annotated[ @@ -316,6 +330,7 @@ class MacroDeclaration16(MacroDeclaration_1): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -323,6 +338,7 @@ class MacroDeclaration16(MacroDeclaration_1): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -337,6 +353,7 @@ class MacroDeclaration16(MacroDeclaration_1): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -344,12 +361,14 @@ class MacroDeclaration16(MacroDeclaration_1): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None encoding: Annotated[ asset_union.MacroEncoding, @@ -387,12 +406,14 @@ class DisplayTagAsset6(AdCPBaseModel): description='Exact macro tokens present anywhere in this representation. Tokens remain byte-preserved until the resolver named by each declaration substitutes them.', min_length=1, ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overriding manifest-level provenance.' ), + NoExplicitNull, ] = None delivery_type: Literal['paired_redirect'] = 'paired_redirect' ad_request_url: Annotated[ diff --git a/src/adcp/types/domains/core/assets/html_asset.py b/src/adcp/types/domains/core/assets/html_asset.py index 9ad378fc1..b5b92a758 100644 --- a/src/adcp/types/domains/core/assets/html_asset.py +++ b/src/adcp/types/domains/core/assets/html_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/html_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from .. import provenance as provenance_1 @@ -14,21 +14,26 @@ class Accessibility(AdCPBaseModel): alt_text: Annotated[ - str | None, Field(description='Text alternative describing the creative content') + str | None, + Field(description='Text alternative describing the creative content'), + NoExplicitNull, ] = None keyboard_navigable: Annotated[ StrictBool | None, Field(description='Whether the creative can be fully operated via keyboard'), + NoExplicitNull, ] = None motion_control: Annotated[ StrictBool | None, Field( description='Whether the creative respects prefers-reduced-motion or provides pause/stop controls' ), + NoExplicitNull, ] = None screen_reader_tested: Annotated[ StrictBool | None, Field(description='Whether the creative has been tested with screen readers'), + NoExplicitNull, ] = None @@ -43,14 +48,18 @@ class HtmlAsset(AdCPBaseModel): ), ] = 'html' content: Annotated[str, Field(description='HTML content')] - version: Annotated[str | None, Field(description="HTML version (e.g., 'HTML5')")] = None + version: Annotated[ + str | None, Field(description="HTML version (e.g., 'HTML5')"), NoExplicitNull + ] = None accessibility: Annotated[ Accessibility | None, Field(description='Self-declared accessibility properties for this opaque creative'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/image_asset.py b/src/adcp/types/domains/core/assets/image_asset.py index 9a5de6b11..15130540d 100644 --- a/src/adcp/types/domains/core/assets/image_asset.py +++ b/src/adcp/types/domains/core/assets/image_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/image_asset.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat from pydantic import AnyUrl, ConfigDict, Field, StrictFloat @@ -37,6 +37,7 @@ class ImageAsset(AdCPBaseModel): description='Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.', ge=1, ), + NoExplicitNull, ] = None pixel_ratio: Annotated[ StrictFloat | None, @@ -44,18 +45,21 @@ class ImageAsset(AdCPBaseModel): description='Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.', gt=0.0, ), + NoExplicitNull, ] = None state_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None breakpoint_id: Annotated[ str | None, Field( description='Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.' ), + NoExplicitNull, ] = None focal_point: Annotated[ list[FocalPointItem] | None, @@ -64,14 +68,20 @@ class ImageAsset(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None format: Annotated[ - str | None, Field(description='Image file format (jpg, png, gif, webp, etc.)') + str | None, + Field(description='Image file format (jpg, png, gif, webp, etc.)'), + NoExplicitNull, + ] = None + alt_text: Annotated[ + str | None, Field(description='Alternative text for accessibility'), NoExplicitNull ] = None - alt_text: Annotated[str | None, Field(description='Alternative text for accessibility')] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/javascript_asset.py b/src/adcp/types/domains/core/assets/javascript_asset.py index 81b2d19fa..d77ae9325 100644 --- a/src/adcp/types/domains/core/assets/javascript_asset.py +++ b/src/adcp/types/domains/core/assets/javascript_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/javascript_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ...enums import javascript_module_type @@ -15,21 +15,26 @@ class Accessibility(AdCPBaseModel): alt_text: Annotated[ - str | None, Field(description='Text alternative describing the creative content') + str | None, + Field(description='Text alternative describing the creative content'), + NoExplicitNull, ] = None keyboard_navigable: Annotated[ StrictBool | None, Field(description='Whether the creative can be fully operated via keyboard'), + NoExplicitNull, ] = None motion_control: Annotated[ StrictBool | None, Field( description='Whether the creative respects prefers-reduced-motion or provides pause/stop controls' ), + NoExplicitNull, ] = None screen_reader_tested: Annotated[ StrictBool | None, Field(description='Whether the creative has been tested with screen readers'), + NoExplicitNull, ] = None @@ -47,14 +52,17 @@ class JavascriptAsset(AdCPBaseModel): module_type: Annotated[ javascript_module_type.JavascriptModuleType | None, Field(description='JavaScript module type'), + NoExplicitNull, ] = None accessibility: Annotated[ Accessibility | None, Field(description='Self-declared accessibility properties for this opaque creative'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/markdown_asset.py b/src/adcp/types/domains/core/assets/markdown_asset.py index af288a283..a9cbe5c53 100644 --- a/src/adcp/types/domains/core/assets/markdown_asset.py +++ b/src/adcp/types/domains/core/assets/markdown_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/markdown_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ...enums import markdown_flavor as markdown_flavor_1 @@ -33,16 +33,19 @@ class MarkdownAsset(AdCPBaseModel): Field( description='Optional language claim for this markdown. In a materialized creative localization variant, localized-creative-asset.json requires this value to use /schemas/core/locale-tag.json and conformance requires exact equality with the enclosing variant locale. General non-localized assets retain the legacy unconstrained string for compatibility.' ), + NoExplicitNull, ] = None markdown_flavor: Annotated[ markdown_flavor_1.MarkdownFlavor | None, Field( description='Markdown flavor used. CommonMark for strict compatibility, GFM for tables/task lists/strikethrough.' ), + NoExplicitNull, ] = markdown_flavor_1.MarkdownFlavor.commonmark allow_raw_html: Annotated[ StrictBool | None, Field( description='Whether raw HTML blocks are allowed in the markdown. False recommended for security.' ), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/assets/pixel_tracker_asset.py b/src/adcp/types/domains/core/assets/pixel_tracker_asset.py index e7c9ff657..7ad5f981d 100644 --- a/src/adcp/types/domains/core/assets/pixel_tracker_asset.py +++ b/src/adcp/types/domains/core/assets/pixel_tracker_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/pixel_tracker_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ...enums import pixel_tracking_event @@ -69,6 +69,7 @@ class PixelTrackerAsset(AdCPBaseModel): Field( description="How the tracker URL is invoked at serve time:\n- `img` — fired as an image pixel (HTTP GET with ``-like semantics; no JS execution)\n- `js` — fired as a script include (renderer evaluates the URL's response as JavaScript)\n\nMatches IAB OpenRTB Native 1.2 method enum (1=img, 2=js). `js` MUST only be used by sellers whose renderer supports JavaScript trackers; sellers without JS-tracker support MUST reject `method: js` declarations at sync_creatives time with `CREATIVE_REJECTED` carrying the reason." ), + NoExplicitNull, ] = Method.img url: Annotated[ macro_bearing_url.MacroBearingUrl, @@ -82,16 +83,19 @@ class PixelTrackerAsset(AdCPBaseModel): description='One declaration per token occurrence in `url`; declaration_id values MUST be unique and locations MUST resolve. When omitted, legacy behavior applies.', min_length=1, ), + NoExplicitNull, ] = None custom_event_name: Annotated[ str | None, Field( description='REQUIRED when `event` is `custom`; otherwise MUST be absent. Adopter-defined event name. When tracker execution is undeclared, an unknown custom event is a forward-compatible probe and the seller silently no-ops instead of rejecting the creative. An effective tracker_execution_contract overrides that legacy fallback: with complete:true an unlisted custom selector is unsupported and rejects compatibility with tracker_contract_mismatch; a listed custom selector is an affirmative accept-and-initiate commitment.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/published_post_asset.py b/src/adcp/types/domains/core/assets/published_post_asset.py index a9236e5a0..20e9df09f 100644 --- a/src/adcp/types/domains/core/assets/published_post_asset.py +++ b/src/adcp/types/domains/core/assets/published_post_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/published_post_asset.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, model_validator from .. import provenance as provenance_1 @@ -21,13 +21,17 @@ class IdentityRef(AdCPBaseModel): extra='allow', ) handle: Annotated[ - str | None, Field(description='Human-readable handle or username when available.') + str | None, + Field(description='Human-readable handle or username when available.'), + NoExplicitNull, ] = None profile_url: Annotated[ - AnyUrl | None, Field(description='URL for the authoring profile, page, or channel.') + AnyUrl | None, + Field(description='URL for the authoring profile, page, or channel.'), + NoExplicitNull, ] = None platform_identity_id: Annotated[ - str | None, Field(description='Opaque platform-native identity identifier.') + str | None, Field(description='Opaque platform-native identity identifier.'), NoExplicitNull ] = None @@ -53,18 +57,22 @@ class ReferenceAuthorization(AdCPBaseModel): checked_at: Annotated[ AwareDatetime | None, Field(description='When the seller last checked the authorization state.'), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field(description='When the authorization expires, if the platform exposes an expiry.'), + NoExplicitNull, ] = None authorization_url: Annotated[ AnyUrl | None, Field(description='Optional URL the buyer or creator can use to complete authorization.'), + NoExplicitNull, ] = None authorization_instructions: Annotated[ str | None, Field(description='Human-readable instructions for completing or restoring authorization.'), + NoExplicitNull, ] = None @@ -83,40 +91,47 @@ class PublishedPostAsset(AdCPBaseModel): Field( description='Canonical URL for the published post. Preferred when the platform exposes a stable public or authenticated URL.' ), + NoExplicitNull, ] = None platform: Annotated[ str | None, Field( description="Optional platform or publisher namespace for the referenced post. Informational unless the seller's product declaration or platform extension narrows the accepted values." ), + NoExplicitNull, ] = None platform_post_id: Annotated[ str | None, Field( description='Optional platform-native post identifier when a URL alone is not stable or not available. Buyers SHOULD include `platform` when using `platform_post_id` without `post_url`, unless the product or format declaration already narrows the platform. Platform-specific identifier semantics belong in platform_extensions; this field is only an opaque reference.' ), + NoExplicitNull, ] = None identity_ref: Annotated[ IdentityRef | None, Field( description='Optional identity hint for the authoring handle/page/channel that owns the post. Sellers MUST verify authorization from platform state; buyers MUST NOT use this object as proof of authorization.' ), + NoExplicitNull, ] = None published_at: Annotated[ AwareDatetime | None, Field(description='When the referenced post was originally published, if known.'), + NoExplicitNull, ] = None reference_authorization: Annotated[ ReferenceAuthorization | None, Field( description='Server-emitted, seller-observed authorization state for the referenced post or identity. Sellers MAY return this object on read surfaces. On write requests, sellers MUST ignore buyer-supplied `reference_authorization.status` and other authorization-state claims unless a platform extension explicitly defines a signed proof shape and the seller verifies that proof.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this reference asset, overrides manifest-level provenance.' ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/assets/text_asset.py b/src/adcp/types/domains/core/assets/text_asset.py index 11f1ea791..8f544ee95 100644 --- a/src/adcp/types/domains/core/assets/text_asset.py +++ b/src/adcp/types/domains/core/assets/text_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/text_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from .. import provenance as provenance_1 @@ -28,10 +28,12 @@ class TextAsset(AdCPBaseModel): Field( description='Optional language claim for this text. In a materialized creative localization variant, localized-creative-asset.json requires this value to use /schemas/core/locale-tag.json and conformance requires exact equality with the enclosing variant locale. General non-localized assets retain the legacy unconstrained string for compatibility.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/url_asset.py b/src/adcp/types/domains/core/assets/url_asset.py index 0e312fafa..6d8bd47af 100644 --- a/src/adcp/types/domains/core/assets/url_asset.py +++ b/src/adcp/types/domains/core/assets/url_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/url_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ...enums import url_asset_type @@ -64,6 +64,7 @@ class UrlAsset(AdCPBaseModel): Field( description='Mechanism a receiver uses to invoke this URL: `clickthrough` for a user destination, `ad_request` for a third-party display creative request, `tracker_pixel` for an event HTTP request, or `tracker_script` for a script include. SHOULD be present on every URL asset.' ), + NoExplicitNull, ] = None macro_declarations: Annotated[ list[MacroDeclaration] | None, @@ -71,19 +72,22 @@ class UrlAsset(AdCPBaseModel): description='One declaration per token occurrence in `url`; declaration_id values MUST be unique and every location MUST identify an existing occurrence. Absence retains legacy opaque transport behavior.', min_length=1, ), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Description of what this URL points to') + str | None, Field(description='Description of what this URL points to'), NoExplicitNull ] = None state_id: Annotated[ str | None, Field( description='Binding used only when this URL populates a `seller_rendered_stateful_display` `state_click_urls` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it); at most one entry per state. Omit for ordinary URL slots.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/vast_asset.py b/src/adcp/types/domains/core/assets/vast_asset.py index 4f9ea6726..6f85ec7a9 100644 --- a/src/adcp/types/domains/core/assets/vast_asset.py +++ b/src/adcp/types/domains/core/assets/vast_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/vast_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, RootModel, StrictBool from ...enums import vast_tracking_event @@ -87,6 +87,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -94,6 +95,7 @@ class MacroDeclaration(AdCPBaseModel): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -108,6 +110,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -115,12 +118,14 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None location: Location encoding: Annotated[ @@ -158,6 +163,7 @@ class VastAsset3(AdCPBaseModel): Field( description='Exact VAST version declared by the supplied URL response or inline document. Required by the 3.2 canonical `video_vast` and `audio_vast` manifest paths; optional only on the deprecated named-format compatibility path. Receivers MUST NOT relabel or synthesize a newer version merely because the destination accepts it.' ), + NoExplicitNull, ] = None macro_declarations: Annotated[ list[MacroDeclaration] | None, @@ -165,31 +171,37 @@ class VastAsset3(AdCPBaseModel): description='One declaration per exact occurrence in a field carried by this asset. A URL-delivered asset can declare only occurrences in its locator `url`; tokens discovered later in a fetched VAST response require document validation evidence or an inline/snapshotted asset and MUST NOT be guessed from the locator. IAB tokens cite a registry namespace and revision rather than copying the live registry into AdCP.', min_length=1, ), + NoExplicitNull, ] = None vpaid_enabled: Annotated[ StrictBool | None, Field(description='Whether VPAID (Video Player-Ad Interface Definition) is supported'), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Expected media duration in milliseconds (if known)', ge=0), + NoExplicitNull, ] = None tracking_events: Annotated[ list[vast_tracking_event.VastTrackingEvent] | None, Field(description='Tracking events supported by this VAST tag'), + NoExplicitNull, ] = None captions_url: Annotated[ - AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)') + AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)'), NoExplicitNull ] = None audio_description_url: Annotated[ AnyUrl | None, Field(description='URL to audio description track for visually impaired users'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None delivery_type: Annotated[ Literal['url'], @@ -228,6 +240,7 @@ class MacroDeclaration20(AdCPBaseModel): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -235,6 +248,7 @@ class MacroDeclaration20(AdCPBaseModel): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -249,6 +263,7 @@ class MacroDeclaration20(AdCPBaseModel): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -256,12 +271,14 @@ class MacroDeclaration20(AdCPBaseModel): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None location: Location26 encoding: Annotated[ @@ -299,6 +316,7 @@ class VastAsset4(AdCPBaseModel): Field( description='Exact VAST version declared by the supplied URL response or inline document. Required by the 3.2 canonical `video_vast` and `audio_vast` manifest paths; optional only on the deprecated named-format compatibility path. Receivers MUST NOT relabel or synthesize a newer version merely because the destination accepts it.' ), + NoExplicitNull, ] = None macro_declarations: Annotated[ list[MacroDeclaration20] | None, @@ -306,31 +324,37 @@ class VastAsset4(AdCPBaseModel): description='One declaration per exact occurrence in a field carried by this asset. A URL-delivered asset can declare only occurrences in its locator `url`; tokens discovered later in a fetched VAST response require document validation evidence or an inline/snapshotted asset and MUST NOT be guessed from the locator. IAB tokens cite a registry namespace and revision rather than copying the live registry into AdCP.', min_length=1, ), + NoExplicitNull, ] = None vpaid_enabled: Annotated[ StrictBool | None, Field(description='Whether VPAID (Video Player-Ad Interface Definition) is supported'), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Expected media duration in milliseconds (if known)', ge=0), + NoExplicitNull, ] = None tracking_events: Annotated[ list[vast_tracking_event.VastTrackingEvent] | None, Field(description='Tracking events supported by this VAST tag'), + NoExplicitNull, ] = None captions_url: Annotated[ - AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)') + AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)'), NoExplicitNull ] = None audio_description_url: Annotated[ AnyUrl | None, Field(description='URL to audio description track for visually impaired users'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None delivery_type: Annotated[ Literal['inline'], diff --git a/src/adcp/types/domains/core/assets/vast_tracker_asset.py b/src/adcp/types/domains/core/assets/vast_tracker_asset.py index d6395ac3a..b8b1169cb 100644 --- a/src/adcp/types/domains/core/assets/vast_tracker_asset.py +++ b/src/adcp/types/domains/core/assets/vast_tracker_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/assets/vast_tracker_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ...enums import vast_tracking_event @@ -76,6 +76,7 @@ class VastTrackerAsset(AdCPBaseModel): Field( description='Exact tokens in `url` and their resolver/encoding contracts.', min_length=1 ), + NoExplicitNull, ] = None offset: Annotated[ str | None, @@ -83,16 +84,19 @@ class VastTrackerAsset(AdCPBaseModel): description='VAST `offset` attribute. Required when `vast_event` is `progress`; ignored otherwise for compatibility with existing 3.x manifests. Format matches the VAST 4.2 XSD `Tracking@offset` pattern: `HH:MM:SS` or `HH:MM:SS.mmm` for absolute time (two-digit hours, minutes 00–59, seconds 00–59), or an integer percentage 0–100 suffixed with `%`. Negative offsets are NOT permitted — the VAST 4.2 XSD pattern does not allow a leading minus.', pattern='^(\\d{2}:[0-5]\\d:[0-5]\\d(\\.\\d{3})?|(100|\\d{1,2})%)$', ), + NoExplicitNull, ] = None target: Annotated[ Target | None, Field( description='Which VAST creative element this tracker scopes to — `linear` for `/`, `non_linear` for `/`, `companion` for `//`. Defaults to `linear`. Existing 3.x assets remain structurally permissive; a tracker execution contract applies the standards-valid event/target matrix when matching a creative to a product.' ), + NoExplicitNull, ] = Target.linear provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/video_asset.py b/src/adcp/types/domains/core/assets/video_asset.py index 0612ddb2d..eba2a43cc 100644 --- a/src/adcp/types/domains/core/assets/video_asset.py +++ b/src/adcp/types/domains/core/assets/video_asset.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/assets/video_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat from ...enums import audio_channel_layout @@ -79,79 +79,107 @@ class VideoAsset(AdCPBaseModel): ), ] duration_ms: Annotated[ - SchemaInt | None, Field(description='Video duration in milliseconds', ge=1) + SchemaInt | None, Field(description='Video duration in milliseconds', ge=1), NoExplicitNull + ] = None + file_size_bytes: Annotated[ + SchemaInt | None, Field(description='File size in bytes', ge=1), NoExplicitNull ] = None - file_size_bytes: Annotated[SchemaInt | None, Field(description='File size in bytes', ge=1)] = ( - None - ) container_format: Annotated[ - str | None, Field(description='Video container format (mp4, webm, mov, etc.)') + str | None, + Field(description='Video container format (mp4, webm, mov, etc.)'), + NoExplicitNull, ] = None video_codec: Annotated[ - str | None, Field(description='Video codec used (h264, h265, vp9, av1, prores, etc.)') + str | None, + Field(description='Video codec used (h264, h265, vp9, av1, prores, etc.)'), + NoExplicitNull, ] = None video_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Video stream bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Video stream bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None frame_rate: Annotated[ str | None, Field( description="Frame rate as string to preserve precision (e.g., '23.976', '29.97', '30')" ), + NoExplicitNull, + ] = None + frame_rate_type: Annotated[frame_rate_type_1.FrameRateType | None, NoExplicitNull] = None + scan_type: Annotated[scan_type_1.ScanType | None, NoExplicitNull] = None + color_space: Annotated[ + ColorSpace | None, Field(description='Color space of the video'), NoExplicitNull ] = None - frame_rate_type: frame_rate_type_1.FrameRateType | None = None - scan_type: scan_type_1.ScanType | None = None - color_space: Annotated[ColorSpace | None, Field(description='Color space of the video')] = None hdr_format: Annotated[ HdrFormat | None, Field(description="HDR format if applicable, or 'sdr' for standard dynamic range"), + NoExplicitNull, ] = None chroma_subsampling: Annotated[ - ChromaSubsampling | None, Field(description='Chroma subsampling format') + ChromaSubsampling | None, Field(description='Chroma subsampling format'), NoExplicitNull + ] = None + video_bit_depth: Annotated[ + VideoBitDepth | None, Field(description='Video bit depth'), NoExplicitNull ] = None - video_bit_depth: Annotated[VideoBitDepth | None, Field(description='Video bit depth')] = None gop_interval_seconds: Annotated[ - StrictFloat | None, Field(description='GOP/keyframe interval in seconds') + StrictFloat | None, Field(description='GOP/keyframe interval in seconds'), NoExplicitNull ] = None - gop_type: gop_type_1.GopType | None = None - moov_atom_position: moov_atom_position_1.MoovAtomPosition | None = None + gop_type: Annotated[gop_type_1.GopType | None, NoExplicitNull] = None + moov_atom_position: Annotated[moov_atom_position_1.MoovAtomPosition | None, NoExplicitNull] = ( + None + ) has_audio: Annotated[ - StrictBool | None, Field(description='Whether the video contains an audio track') + StrictBool | None, + Field(description='Whether the video contains an audio track'), + NoExplicitNull, ] = None audio_codec: Annotated[ str | None, Field(description='Audio codec used (aac, aac_lc, he_aac, pcm, mp3, ac3, eac3, etc.)'), + NoExplicitNull, ] = None audio_sampling_rate_hz: Annotated[ - SchemaInt | None, Field(description='Audio sampling rate in Hz (e.g., 44100, 48000)') + SchemaInt | None, + Field(description='Audio sampling rate in Hz (e.g., 44100, 48000)'), + NoExplicitNull, ] = None audio_channels: Annotated[ audio_channel_layout.AudioChannelLayout | None, Field(description='Audio channel configuration'), + NoExplicitNull, + ] = None + audio_bit_depth: Annotated[ + AudioBitDepth | None, Field(description='Audio bit depth'), NoExplicitNull ] = None - audio_bit_depth: Annotated[AudioBitDepth | None, Field(description='Audio bit depth')] = None audio_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Audio bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Audio bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None audio_loudness_lufs: Annotated[ - StrictFloat | None, Field(description='Integrated loudness in LUFS') + StrictFloat | None, Field(description='Integrated loudness in LUFS'), NoExplicitNull ] = None audio_true_peak_dbfs: Annotated[ - StrictFloat | None, Field(description='True peak level in dBFS') + StrictFloat | None, Field(description='True peak level in dBFS'), NoExplicitNull ] = None captions_url: Annotated[ - AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)') + AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)'), NoExplicitNull ] = None transcript_url: Annotated[ - AnyUrl | None, Field(description='URL to text transcript of the video content') + AnyUrl | None, + Field(description='URL to text transcript of the video content'), + NoExplicitNull, ] = None audio_description_url: Annotated[ AnyUrl | None, Field(description='URL to audio description track for visually impaired users'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/webhook_asset.py b/src/adcp/types/domains/core/assets/webhook_asset.py index a1dc08d0c..e30b32d79 100644 --- a/src/adcp/types/domains/core/assets/webhook_asset.py +++ b/src/adcp/types/domains/core/assets/webhook_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/webhook_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field from ...enums import http_method, universal_macro, webhook_response_type, webhook_security_method @@ -18,10 +18,12 @@ class Security(AdCPBaseModel): webhook_security_method.WebhookSecurityMethod, Field(description='Authentication method') ] hmac_header: Annotated[ - str | None, Field(description="Header name for HMAC signature (e.g., 'X-Signature')") + str | None, + Field(description="Header name for HMAC signature (e.g., 'X-Signature')"), + NoExplicitNull, ] = None api_key_header: Annotated[ - str | None, Field(description="Header name for API key (e.g., 'X-API-Key')") + str | None, Field(description="Header name for API key (e.g., 'X-API-Key')"), NoExplicitNull ] = None @@ -36,22 +38,25 @@ class WebhookAsset(AdCPBaseModel): ), ] = 'webhook' url: Annotated[AnyUrl, Field(description='Webhook URL to call for dynamic content')] - method: Annotated[http_method.HttpMethod | None, Field(description='HTTP method')] = ( - http_method.HttpMethod.POST - ) + method: Annotated[ + http_method.HttpMethod | None, Field(description='HTTP method'), NoExplicitNull + ] = http_method.HttpMethod.POST timeout_ms: Annotated[ SchemaInt | None, Field(description='Maximum time to wait for response in milliseconds', ge=10, le=5000), + NoExplicitNull, ] = 500 supported_macros: Annotated[ list[universal_macro.UniversalMacro | str] | None, Field( description='Universal macros that can be passed to webhook (e.g., DEVICE_TYPE, COUNTRY). See docs/creative/universal-macros.mdx for full list.' ), + NoExplicitNull, ] = None required_macros: Annotated[ list[universal_macro.UniversalMacro | str] | None, Field(description='Universal macros that must be provided for webhook to function'), + NoExplicitNull, ] = None response_type: Annotated[ webhook_response_type.WebhookResponseType, @@ -63,4 +68,5 @@ class WebhookAsset(AdCPBaseModel): Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/assets/zip_asset.py b/src/adcp/types/domains/core/assets/zip_asset.py index c6f97d094..a3c7fe7cf 100644 --- a/src/adcp/types/domains/core/assets/zip_asset.py +++ b/src/adcp/types/domains/core/assets/zip_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/assets/zip_asset.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictBool from .. import provenance as provenance_1 @@ -14,21 +14,26 @@ class Accessibility(AdCPBaseModel): alt_text: Annotated[ - str | None, Field(description='Text alternative describing the creative content') + str | None, + Field(description='Text alternative describing the creative content'), + NoExplicitNull, ] = None keyboard_navigable: Annotated[ StrictBool | None, Field(description='Whether the creative can be fully operated via keyboard'), + NoExplicitNull, ] = None motion_control: Annotated[ StrictBool | None, Field( description='Whether the creative respects prefers-reduced-motion or provides pause/stop controls' ), + NoExplicitNull, ] = None screen_reader_tested: Annotated[ StrictBool | None, Field(description='Whether the creative has been tested with screen readers'), + NoExplicitNull, ] = None @@ -49,24 +54,28 @@ class ZipAsset(AdCPBaseModel): description='Maximum file size in kilobytes. Receivers should reject zips exceeding this.', ge=0, ), + NoExplicitNull, ] = None entry_point: Annotated[ str | None, Field( description="Relative path to the entry file within the zip (typically 'index.html'). Receivers default to 'index.html' if absent." ), + NoExplicitNull, ] = None allowed_inner_extensions: Annotated[ list[str] | None, Field( description="File extensions permitted inside the zip (e.g., ['html', 'css', 'js', 'png', 'jpg', 'svg', 'webp', 'json', 'woff2']). Receivers may reject zips containing other extensions." ), + NoExplicitNull, ] = None backup_image_url: Annotated[ AnyUrl | None, Field( description='Fallback image URL for environments that cannot render the bundled creative (e.g., non-HTML5 endpoints, ad blockers). Recommended for HTML5 banners.' ), + NoExplicitNull, ] = None digest: Annotated[ str | None, @@ -74,14 +83,17 @@ class ZipAsset(AdCPBaseModel): description='Optional SHA-256 content digest of the zip archive (sha256:) for integrity verification. Lets receivers detect tampered or stale archives.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None accessibility: Annotated[ Accessibility | None, Field(description='Self-declared accessibility properties for this opaque creative'), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this asset, overrides manifest-level provenance' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/async_response_refs/creative/get_creative_features_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/creative/get_creative_features_async_response_submitted.py index 6b0bd0095..fa1ce8bcf 100644 --- a/src/adcp/types/domains/core/async_response_refs/creative/get_creative_features_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/creative/get_creative_features_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/creative/get_creative_features_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,6 +36,7 @@ class GetCreativeFeaturesSubmitted(AdCPBaseModel): description='Provider-generated identity allocated when the evaluation is accepted. Optional in AdCP 3.x and required in AdCP 4.0. Providers SHOULD emit it in 3.x; when present, exact replays and the terminal result MUST carry this same value. Distinct from task_id and idempotency_key.', min_length=1, ), + NoExplicitNull, ] = None message: Annotated[ str | None, @@ -43,6 +44,7 @@ class GetCreativeFeaturesSubmitted(AdCPBaseModel): description='Optional human-readable explanation of why the evaluation is submitted. Plain text only; callers treat it as untrusted provider input.', max_length=2000, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_input_required.py index abe340e98..01c43e48c 100644 --- a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/creative/sync_creatives_async_response_input_required.json -# timestamp: 2026-05-29T23:56:01+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -25,7 +25,9 @@ class SyncCreativesInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why buyer input is needed') + Reason | None, + Field(description='Reason code indicating why buyer input is needed'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_submitted.py index 9c87f5d91..6343ab683 100644 --- a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/creative/sync_creatives_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,12 +36,14 @@ class SyncCreativesSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Batch ingestion queued; typical turnaround 15-30 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_working.py index e306bbcdf..ee4511dd9 100644 --- a/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/creative/sync_creatives_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/creative/sync_creatives_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -18,20 +18,30 @@ class SyncCreativesWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None creatives_processed: Annotated[ - SchemaInt | None, Field(description='Number of creatives processed so far', ge=0) + SchemaInt | None, + Field(description='Number of creatives processed so far', ge=0), + NoExplicitNull, ] = None creatives_total: Annotated[ - SchemaInt | None, Field(description='Total number of creatives to process', ge=0) + SchemaInt | None, + Field(description='Total number of creatives to process', ge=0), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_input_required.py index a322fbc3e..756c42a3a 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/build_creative_async_response_input_required.json -# timestamp: 2026-05-29T23:56:01+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -26,13 +26,16 @@ class BuildCreativeInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional validation errors or warnings explaining why input is required.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_submitted.py index 2fb45a85a..81c3d461e 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/build_creative_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,12 +36,14 @@ class BuildCreativeSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Generative build queued; typical turnaround 3–5 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_working.py index 653e18b6b..2c34d23c6 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/build_creative_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/build_creative_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -18,17 +18,24 @@ class BuildCreativeWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step or phase of the operation (e.g., 'generating_assets', 'resolving_macros', 'rendering_preview')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_input_required.py index bb7da427a..a46c2575f 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/create_media_buy_async_response_input_required.json -# timestamp: 2026-05-29T23:56:01+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -25,13 +25,16 @@ class CreateMediaBuyInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional validation errors or warnings for debugging purposes. Helps explain why input is required.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_submitted.py index 19fa10e0a..a91a1329e 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/create_media_buy_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,12 +36,14 @@ class CreateMediaBuySubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Awaiting IO signature from sales team; typical turnaround 2–4 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_working.py index b09e8be39..84b2a4e2c 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/create_media_buy_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/create_media_buy_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -18,14 +18,20 @@ class CreateMediaBuyWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_input_required.py index 33e346407..885d729ba 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/get_products_async_response_input_required.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -25,14 +25,19 @@ class GetProductsInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None partial_results: Annotated[ list[product.Product] | None, Field(description='Partial product results that may help inform the clarification'), + NoExplicitNull, ] = None suggestions: Annotated[ - list[str] | None, Field(description='Suggested values or options for the required input') + list[str] | None, + Field(description='Suggested values or options for the required input'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_submitted.py index 5e4be1c86..b76ef7ad8 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/get_products_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ... import context as context_1 @@ -36,15 +36,19 @@ class GetProductsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Custom curation queued; typical turnaround 10–30 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None estimated_completion: Annotated[ - AwareDatetime | None, Field(description='Estimated completion time for the search') + AwareDatetime | None, + Field(description='Estimated completion time for the search'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_working.py index 49a7e2e4c..718c99c54 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/get_products_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/get_products_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -20,18 +20,22 @@ class GetProductsWorking(AdCPBaseModel): percentage: Annotated[ StrictFloat | None, Field(description='Progress percentage of the search operation', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step in the search process (e.g., 'searching_inventory', 'validating_availability')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the search process') + SchemaInt | None, + Field(description='Total number of steps in the search process'), + NoExplicitNull, ] = None step_number: Annotated[ - SchemaInt | None, Field(description='Current step number (1-indexed)') + SchemaInt | None, Field(description='Current step number (1-indexed)'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_input_required.py index 332b83101..258025591 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/sync_catalogs_async_response_input_required.json -# timestamp: 2026-05-29T23:56:01+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -30,6 +30,7 @@ class SyncCatalogsInputRequired(AdCPBaseModel): Field( description='Reason code indicating why buyer input is needed. APPROVAL_REQUIRED: platform requires explicit approval before activating the catalog. FEED_VALIDATION: feed URL returned unexpected format or schema errors. ITEM_REVIEW: platform flagged items for manual review. FEED_ACCESS: platform cannot access the feed URL (authentication, CORS, etc.).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_submitted.py index e50407f29..f63ab2260 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/sync_catalogs_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,12 +36,14 @@ class SyncCatalogsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Catalog ingestion queued; typical turnaround 5–15 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_working.py index ee29491da..c8c54b0d5 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/sync_catalogs_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/sync_catalogs_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -18,31 +18,44 @@ class SyncCatalogsWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step or phase of the operation (e.g., 'Fetching product feed', 'Validating items', 'Platform review')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None catalogs_processed: Annotated[ - SchemaInt | None, Field(description='Number of catalogs processed so far', ge=0) + SchemaInt | None, + Field(description='Number of catalogs processed so far', ge=0), + NoExplicitNull, ] = None catalogs_total: Annotated[ - SchemaInt | None, Field(description='Total number of catalogs to process', ge=0) + SchemaInt | None, + Field(description='Total number of catalogs to process', ge=0), + NoExplicitNull, ] = None items_processed: Annotated[ SchemaInt | None, Field(description='Total number of catalog items processed across all catalogs', ge=0), + NoExplicitNull, ] = None items_total: Annotated[ SchemaInt | None, Field(description='Total number of catalog items to process across all catalogs', ge=0), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_input_required.py b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_input_required.py index 570fd47d6..b32fddc3b 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_input_required.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/update_media_buy_async_response_input_required.json -# timestamp: 2026-05-29T23:56:01+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -24,7 +24,9 @@ class UpdateMediaBuyInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_submitted.py index ef9261393..ce3498f0a 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/update_media_buy_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ... import context as context_1 @@ -36,12 +36,14 @@ class UpdateMediaBuySubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Awaiting operator re-approval; typical turnaround 2–4 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_working.py index dbe6e7e2c..6be340535 100644 --- a/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/media_buy/update_media_buy_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/media_buy/update_media_buy_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -18,14 +18,20 @@ class UpdateMediaBuyWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_submitted.py b/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_submitted.py index 35ec0661d..de2e72d89 100644 --- a/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_submitted.py +++ b/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/signals/get_signals_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ... import context as context_1 @@ -36,16 +36,19 @@ class GetSignalsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Provider discovery queued; typical turnaround 10-30 minutes.' Plain text only. Callers MUST treat this as untrusted agent input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile agent may inject prompt-injection payloads aimed at the caller's agent.", max_length=2000, ), + NoExplicitNull, ] = None estimated_completion: Annotated[ AwareDatetime | None, Field(description='Estimated completion time for the signal discovery task.'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories or partial provider unavailability). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_working.py b/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_working.py index 51c7e5023..1da0051ed 100644 --- a/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_working.py +++ b/src/adcp/types/domains/core/async_response_refs/signals/get_signals_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/async_response_refs/signals/get_signals_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ... import context as context_1 @@ -22,19 +22,22 @@ class GetSignalsWorking(AdCPBaseModel): Field( description='Progress percentage of the signal discovery operation.', ge=0.0, le=100.0 ), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description='Current step in the signal discovery process, such as `querying_providers`, `ranking_signals`, or `checking_deployments`.' ), + NoExplicitNull, ] = None total_steps: Annotated[ SchemaInt | None, Field(description='Total number of steps in the signal discovery process.'), + NoExplicitNull, ] = None step_number: Annotated[ - SchemaInt | None, Field(description='Current step number (1-indexed).') + SchemaInt | None, Field(description='Current step number (1-indexed).'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/attestation_capabilities.py b/src/adcp/types/domains/core/attestation_capabilities.py index 9cd9b6537..e72a95e99 100644 --- a/src/adcp/types/domains/core/attestation_capabilities.py +++ b/src/adcp/types/domains/core/attestation_capabilities.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/attestation_capabilities.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, RootModel from . import attestation_issuer @@ -53,9 +53,9 @@ class AcceptedVerifier(AdCPBaseModel): extra='forbid', ) agent_url: AnyUrl - claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None - proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None - ext: ext_1.ExtensionObject | None = None + claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1), NoExplicitNull] = None + proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1), NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AcceptedIssuer(AdCPBaseModel): @@ -69,6 +69,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.', min_length=1, ), + NoExplicitNull, ] = None proof_formats: Annotated[ list[AnyUrl] | None, @@ -76,6 +77,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.', min_length=1, ), + NoExplicitNull, ] = None credential_origins: Annotated[ list[CredentialOrigin] | None, @@ -83,6 +85,7 @@ class AcceptedIssuer(AdCPBaseModel): description='Canonical HTTPS origins from which credential_uri locators may be fetched for this issuer. Exact origin matching happens after URL canonicalization and before DNS resolution. Paths in the credential URI may vary; userinfo is forbidden.', min_length=1, ), + NoExplicitNull, ] = None resolvers: Annotated[ list[Resolver] | None, @@ -90,8 +93,9 @@ class AcceptedIssuer(AdCPBaseModel): description='Evaluator-approved resolver endpoints for issuer_credential_id delivery. Presentations carry only resolver_id; they cannot replace url or authentication policy.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationCapabilities(AdCPBaseModel): @@ -132,6 +136,7 @@ class AttestationCapabilities(AdCPBaseModel): description="Verifier agents the evaluator may call. A presenter's verify_agent nomination must match one of these canonicalized URLs, but the evaluator remains verifier-of-record and chooses whether to use the nominated agent, another accepted agent, or local verification.", min_length=1, ), + NoExplicitNull, ] = None max_embedded_credential_bytes: Annotated[ SchemaInt | None, @@ -140,5 +145,6 @@ class AttestationCapabilities(AdCPBaseModel): ge=1024, le=1048576, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/attestation_evaluation.py b/src/adcp/types/domains/core/attestation_evaluation.py index 9a0536ab7..5521a0fa8 100644 --- a/src/adcp/types/domains/core/attestation_evaluation.py +++ b/src/adcp/types/domains/core/attestation_evaluation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/attestation_evaluation.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictFloat @@ -51,6 +51,7 @@ class ActionBinding(AdCPBaseModel): max_length=1024, min_length=1, ), + NoExplicitNull, ] = None action_digest: Annotated[ str | None, @@ -58,6 +59,7 @@ class ActionBinding(AdCPBaseModel): description='SHA-256 digest of the domain-defined canonical action preimage.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -78,12 +80,14 @@ class AttestationEvaluation(AdCPBaseModel): description='SHA-256 of the exact resolved or embedded credential bytes according to proof_format. REQUIRED by the normative contract when outcome is verified, expired, revoked, invalid, subject_mismatch, or digest_mismatch; omitted when no credential bytes were obtained. For dual delivery, this is the resolved credential digest; reference_digest independently binds the embedded bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None proof_format: Annotated[ AnyUrl | None, Field( description='Absolute URI identifying the credential/proof format used to interpret and hash obtained credential bytes. REQUIRED whenever credential_digest is present.' ), + NoExplicitNull, ] = None outcome: Annotated[ Outcome, @@ -103,6 +107,7 @@ class AttestationEvaluation(AdCPBaseModel): Field( description='Accepted verifier agent actually used, when verification was delegated. Omit for local verification or when no verifier was called.' ), + NoExplicitNull, ] = None reason_codes: Annotated[ list[ReasonCode] | None, @@ -110,6 +115,7 @@ class AttestationEvaluation(AdCPBaseModel): description='Open, machine-readable reason identifiers supplied by the evaluator. Consumers MUST branch on outcome, not on free-form or evaluator-specific reason codes.', min_length=1, ), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, @@ -118,23 +124,27 @@ class AttestationEvaluation(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None revocation_checked_at: Annotated[ AwareDatetime | None, Field( description='When the evaluator last checked authoritative revocation state. Its freshness is governed by proof format and evaluator policy.' ), + NoExplicitNull, ] = None valid_until: Annotated[ AwareDatetime | None, Field( description='Upper bound on reuse of this evaluation, considering credential expiry, revocation-cache TTL, and evaluator policy. Consumers MUST re-evaluate after this time.' ), + NoExplicitNull, ] = None action_binding: Annotated[ ActionBinding | None, Field( description='Optional binding to the consuming action or readback. Domain consumers that rely on an evaluation MUST carry either an action id or an action digest so the result cannot be transplanted to an unrelated decision.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/attestation_issuer.py b/src/adcp/types/domains/core/attestation_issuer.py index f1e8bd57b..2009ac580 100644 --- a/src/adcp/types/domains/core/attestation_issuer.py +++ b/src/adcp/types/domains/core/attestation_issuer.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/attestation_issuer.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from . import brand_ref @@ -29,7 +29,7 @@ class AttestationIssuer2(AdCPBaseModel): description='Canonical HTTPS endpoint of the issuing agent. Evaluators compare it using AdCP URL canonicalization rules.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationIssuer3(AdCPBaseModel): @@ -48,7 +48,7 @@ class AttestationIssuer3(AdCPBaseModel): description='Canonical HTTPS origin with no path, query, fragment, or userinfo. This identifies the issuer; it does not authorize a fetch.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationIssuer1(AdCPBaseModel): @@ -59,7 +59,7 @@ class AttestationIssuer1(AdCPBaseModel): Literal['brand'], Field(description='The issuer is identified by an AdCP BrandRef.') ] = 'brand' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None AttestationIssuer = Annotated[ diff --git a/src/adcp/types/domains/core/attestation_reference.py b/src/adcp/types/domains/core/attestation_reference.py index 1a90530f7..74760a7a7 100644 --- a/src/adcp/types/domains/core/attestation_reference.py +++ b/src/adcp/types/domains/core/attestation_reference.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/attestation_reference.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, model_validator from . import attestation_issuer, attestation_subject @@ -20,8 +20,8 @@ class ValidityHint(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - not_before: AwareDatetime | None = None - expires_at: AwareDatetime | None = None + not_before: Annotated[AwareDatetime | None, NoExplicitNull] = None + expires_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class VerifyAgent(AdCPBaseModel): @@ -42,7 +42,7 @@ class Locator(AdCPBaseModel): description='HTTPS URI of the credential. Before any fetch, evaluators MUST match its canonical origin to credential_origins on the accepted issuer capability and then apply the attestation fetch contract.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Locator1(AdCPBaseModel): @@ -67,7 +67,7 @@ class Locator1(AdCPBaseModel): pattern='^[A-Za-z0-9._:-]+$', ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class EmbeddedCredential(AdCPBaseModel): @@ -87,7 +87,7 @@ class EmbeddedCredential(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationReference(AdCPBaseModel): @@ -108,12 +108,14 @@ class AttestationReference(AdCPBaseModel): description='Stable locator for resolving the credential. Credential URLs are permitted only when their canonical origin is allowlisted for the matched issuer. Issuer-scoped IDs name a resolver_id already published by the evaluator; the presentation cannot introduce a resolver URL.', discriminator='type', ), + NoExplicitNull, ] = None embedded_credential: Annotated[ EmbeddedCredential | None, Field( description='Optional inline credential for private, authenticated, or offline delivery. It may accompany locator or be the only delivery path. Its format must be supported by the evaluator, and the evaluator MUST verify the credential exactly as it would a resolved credential.' ), + NoExplicitNull, ] = None content_digest: Annotated[ str | None, @@ -121,6 +123,7 @@ class AttestationReference(AdCPBaseModel): description='Optional SHA-256 digest pin for the exact credential bytes, formatted as sha256:. The credential format defines its canonical byte representation. A mismatch is invalid and MUST NOT fall back to the unpinned credential. REQUIRED when both locator and embedded_credential are present; both byte representations MUST match this digest.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None credential_version: Annotated[ str | None, @@ -129,20 +132,23 @@ class AttestationReference(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None validity_hint: Annotated[ ValidityHint | None, Field( description="Optional planning-time validity hint copied from issuer metadata. Evaluators MUST use the resolved or embedded credential's signed validity and revocation state as authoritative." ), + NoExplicitNull, ] = None verify_agent: Annotated[ VerifyAgent | None, Field( description='Optional presenter nomination of a verifier already published by the evaluator. This is a representation, not routing authority: agent_url MUST match an accepted_verifiers[] entry after canonicalization, and the evaluator may choose another accepted verifier or verify locally.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> AttestationReference: diff --git a/src/adcp/types/domains/core/attestation_subject.py b/src/adcp/types/domains/core/attestation_subject.py index ff4620b51..1519bf545 100644 --- a/src/adcp/types/domains/core/attestation_subject.py +++ b/src/adcp/types/domains/core/attestation_subject.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/attestation_subject.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from . import brand_ref @@ -21,7 +21,7 @@ class AttestationSubject2(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationSubject3(AdCPBaseModel): @@ -55,8 +55,9 @@ class AttestationSubject3(AdCPBaseModel): description='Optional SHA-256 pin for the exact content or immutable snapshot identified by this resource subject. This is part of the complete typed subject identity and is distinct from AttestationReference.content_digest, which pins credential bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationSubject1(AdCPBaseModel): @@ -65,7 +66,7 @@ class AttestationSubject1(AdCPBaseModel): ) type: Literal['brand'] = 'brand' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None AttestationSubject = Annotated[ diff --git a/src/adcp/types/domains/core/attribution_window.py b/src/adcp/types/domains/core/attribution_window.py index a78e4ecbf..ed602aa29 100644 --- a/src/adcp/types/domains/core/attribution_window.py +++ b/src/adcp/types/domains/core/attribution_window.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/attribution_window.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import attribution_model @@ -22,16 +22,19 @@ class AttributionWindow(AdCPBaseModel): Field( description='Post-click attribution window. Conversions occurring within this duration after a click are attributed to the ad.' ), + NoExplicitNull, ] = None post_view: Annotated[ duration.Duration | None, Field( description='Post-view attribution window. Conversions occurring within this duration after an ad impression (without click) are attributed to the ad.' ), + NoExplicitNull, ] = None model: Annotated[ attribution_model.AttributionModel | None, Field( description="Attribution model used to assign credit when multiple touchpoints exist. SHOULD be populated when committing to a specific model; when absent, the seller's default applies." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/audience_activation_method.py b/src/adcp/types/domains/core/audience_activation_method.py index 8b1aae52c..35393c94e 100644 --- a/src/adcp/types/domains/core/audience_activation_method.py +++ b/src/adcp/types/domains/core/audience_activation_method.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/audience_activation_method.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field from ..enums import cloud_storage_protocol @@ -57,13 +57,16 @@ class ConsumerIdentity(AdCPBaseModel): extra='forbid', ) cloud: Annotated[ - Cloud | None, Field(description="Cloud family hosting this identity's account.") + Cloud | None, + Field(description="Cloud family hosting this identity's account."), + NoExplicitNull, ] = None region: Annotated[ str | None, Field( description="Vendor/cloud region identifier. Format follows the cloud family's region naming; not constrained by this schema." ), + NoExplicitNull, ] = None identity: Annotated[ str, @@ -89,6 +92,7 @@ class AudienceActivationMethod3(AdCPBaseModel): description='Supported transfer directions. buyer_to_seller: buyer writes to a seller-hosted bucket. seller_to_buyer: seller reads from a buyer-hosted bucket. Absent means unspecified (resolve during account setup), not neither.', min_length=1, ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference, @@ -113,6 +117,7 @@ class AudienceActivationMethod4(AdCPBaseModel): description="Principals the buyer grants access to in the vendor's system. identity is always required. cloud and region are optional, paired deployment metadata: omit both for global principals (for example, an IAM principal or federated identity). Their operational meaning is vendor-specific — they may constrain direct-share reachability or select a fulfillment route, or may be routing and cost hints only. They are not compliance boundaries; data-transfer assessments key on the recipient entity's jurisdiction, not the grantee account's region. Optional: sellers MAY instead communicate identities during account setup.", min_length=1, ), + NoExplicitNull, ] = None @@ -141,6 +146,7 @@ class AudienceActivationMethod6(AdCPBaseModel): max_length=256, min_length=1, ), + NoExplicitNull, ] = None bind_expiry_days: Annotated[ SchemaInt | None, @@ -148,6 +154,7 @@ class AudienceActivationMethod6(AdCPBaseModel): description="Window after which the seller MAY expire an unfulfilled platform_segment bind (per-audience action: failed on sync_audiences). Initial vendor distribution is days-scale; windows shorter than the vendor's documented distribution latency are non-conformant.", ge=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/audience_characteristic.py b/src/adcp/types/domains/core/audience_characteristic.py index a6be90ca3..e2ca29480 100644 --- a/src/adcp/types/domains/core/audience_characteristic.py +++ b/src/adcp/types/domains/core/audience_characteristic.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/audience_characteristic.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, RootModel, StrictBool, StrictFloat, model_validator @@ -39,8 +39,8 @@ class Range(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: StrictFloat | None = None - max: StrictFloat | None = None + min: Annotated[StrictFloat | None, NoExplicitNull] = None + max: Annotated[StrictFloat | None, NoExplicitNull] = None class Taxonomy(AdCPBaseModel): @@ -48,8 +48,8 @@ class Taxonomy(AdCPBaseModel): extra='forbid', ) system: AnyUrl - version: Annotated[str | None, Field(min_length=1)] = None - value_id: Annotated[str | None, Field(min_length=1)] = None + version: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + value_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AudienceCharacteristic(AdCPBaseModel): @@ -65,21 +65,26 @@ class AudienceCharacteristic(AdCPBaseModel): value: Annotated[ str | StrictFloat | StrictBool | Value | None, Field(description='Scalar or set value for the dimension.'), + NoExplicitNull, ] = None range: Annotated[ Range | None, Field( description='Inclusive numeric interval. For dimension `age`, values are completed years and MUST be non-negative integers, and min MUST be less than or equal to max. JSON Schema enforces the age value and bound types; implementations enforce the relational min <= max constraint.' ), + NoExplicitNull, ] = None taxonomy: Annotated[ Taxonomy | None, Field( description='External system that defines the dimension or value. Taxonomy metadata is descriptive and does not create exact demographic targeting semantics.' ), + NoExplicitNull, ] = None label: Annotated[ - str | None, Field(description='Human-readable display label; never the comparison key.') + str | None, + Field(description='Human-readable display label; never the comparison key.'), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/audience_evidence.py b/src/adcp/types/domains/core/audience_evidence.py index 826e8a853..a6a263e09 100644 --- a/src/adcp/types/domains/core/audience_evidence.py +++ b/src/adcp/types/domains/core/audience_evidence.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/audience_evidence.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel, StrictFloat from ..enums import audience_evidence_methodology, audience_resolution_method, audience_subject_type @@ -35,7 +35,7 @@ class Baseline(AdCPBaseModel): system: AnyUrl population_id: Annotated[str, Field(min_length=1)] version: Annotated[str, Field(min_length=1)] - description: str | None = None + description: Annotated[str | None, NoExplicitNull] = None class EvidenceType(StrEnum): @@ -52,7 +52,7 @@ class Subject12(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject13(AdCPBaseModel): @@ -86,8 +86,9 @@ class Subject13(AdCPBaseModel): description='Optional SHA-256 pin for the exact content or immutable snapshot identified by this resource subject. This is part of the complete typed subject identity and is distinct from AttestationReference.content_digest, which pins credential bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject22(Subject12): @@ -108,7 +109,7 @@ class Subject24(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject26(Subject22): @@ -152,7 +153,7 @@ class Subject34(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject36(Subject32): @@ -185,6 +186,7 @@ class Subject36(Subject32): description='Optional SHA-256 pin for the exact content or immutable snapshot identified by this resource subject. This is part of the complete typed subject identity and is distinct from AttestationReference.content_digest, which pins credential bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -200,7 +202,7 @@ class Subject11(AdCPBaseModel): ) type: Literal['brand'] = 'brand' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject14(AdCPBaseModel): @@ -211,7 +213,7 @@ class Subject14(AdCPBaseModel): resource_type: Literal['https://adcontextprotocol.org/claims/subjects/audience-evidence'] = 'https://adcontextprotocol.org/claims/subjects/audience-evidence' content_digest: Annotated[str, Field(pattern='^sha256:[a-f0-9]{64}$')] brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject15(Subject11): @@ -338,6 +340,7 @@ class Subject35(Subject31): description='Optional SHA-256 pin for the exact content or immutable snapshot identified by this resource subject. This is part of the complete typed subject identity and is distinct from AttestationReference.content_digest, which pins credential bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -435,6 +438,7 @@ class AudienceEvidence(AdCPBaseModel): Field( description='Optional subject-resolution method used by the study or estimate. This describes population methodology, not a promise that the product can execute that resolution at serving time.' ), + NoExplicitNull, ] = None provider: Annotated[ brand_ref.BrandReference, Field(description='Brand responsible for producing the evidence.') @@ -446,6 +450,7 @@ class AudienceEvidence(AdCPBaseModel): description='Number of observations or respondents underlying the estimate, when applicable.', ge=1, ), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, @@ -454,6 +459,7 @@ class AudienceEvidence(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime, @@ -464,6 +470,7 @@ class AudienceEvidence(AdCPBaseModel): Field( description='Documentation for interpreting the methodology. It is disclosure, not executable routing.' ), + NoExplicitNull, ] = None attestation_refs: Annotated[ list[AttestationRef] | None, @@ -472,5 +479,6 @@ class AudienceEvidence(AdCPBaseModel): max_length=10, min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/audience_evidence_pin.py b/src/adcp/types/domains/core/audience_evidence_pin.py index cc35d766b..f9fa94cab 100644 --- a/src/adcp/types/domains/core/audience_evidence_pin.py +++ b/src/adcp/types/domains/core/audience_evidence_pin.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/audience_evidence_pin.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -20,4 +20,4 @@ class AudienceEvidencePin(AdCPBaseModel): snapshot_id: Annotated[str, Field(min_length=1)] version: Annotated[str, Field(min_length=1)] content_digest: Annotated[str, Field(pattern='^sha256:[a-f0-9]{64}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/audience_evidence_requirements.py b/src/adcp/types/domains/core/audience_evidence_requirements.py index fb55a146c..d06b227d8 100644 --- a/src/adcp/types/domains/core/audience_evidence_requirements.py +++ b/src/adcp/types/domains/core/audience_evidence_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/audience_evidence_requirements.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat from ..enums import audience_evidence_methodology, audience_resolution_method, audience_subject_type @@ -45,6 +45,7 @@ class MaximumAge(Duration): Field( description="Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." ), + NoExplicitNull, ] = None @@ -70,6 +71,7 @@ class AudienceEvidenceRequirements(AdCPBaseModel): description='Allowed methodologies. excluded_methodologies always wins if a value appears in both lists.', min_length=1, ), + NoExplicitNull, ] = None excluded_methodologies: Annotated[ list[audience_evidence_methodology.AudienceEvidenceMethodology] | None, @@ -77,16 +79,18 @@ class AudienceEvidenceRequirements(AdCPBaseModel): description='Disallowed methodologies. Exclusion wins over accepted_methodologies when the lists overlap.', min_length=1, ), + NoExplicitNull, + ] = None + accepted_evidence_types: Annotated[ + list[AcceptedEvidenceType] | None, Field(min_length=1), NoExplicitNull ] = None - accepted_evidence_types: Annotated[list[AcceptedEvidenceType] | None, Field(min_length=1)] = ( - None - ) accepted_providers: Annotated[ list[brand_ref.BrandReference] | None, Field( description='Allowed evidence providers. excluded_providers always wins if the same canonical BrandRef appears in both lists.', min_length=1, ), + NoExplicitNull, ] = None excluded_providers: Annotated[ list[brand_ref.BrandReference] | None, @@ -94,29 +98,35 @@ class AudienceEvidenceRequirements(AdCPBaseModel): description='Disallowed evidence providers. Exclusion wins over accepted_providers when the lists overlap.', min_length=1, ), + NoExplicitNull, ] = None accepted_subject_types: Annotated[ - list[audience_subject_type.AudienceSubjectType] | None, Field(min_length=1) + list[audience_subject_type.AudienceSubjectType] | None, Field(min_length=1), NoExplicitNull ] = None accepted_resolution_methods: Annotated[ - list[audience_resolution_method.AudienceResolutionMethod] | None, Field(min_length=1) + list[audience_resolution_method.AudienceResolutionMethod] | None, + Field(min_length=1), + NoExplicitNull, ] = None - minimum_confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0)] = None + minimum_confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0), NoExplicitNull] = None maximum_age: Annotated[ MaximumAge | None, Field( description='Maximum age at evaluation time measured from last_updated. Campaign-relative duration is not valid for evidence age.' ), + NoExplicitNull, ] = None methodology_documentation_required: Annotated[ StrictBool | None, Field(description='Whether an admissible item must carry methodology_url.'), + NoExplicitNull, ] = False independent_attestation_required: Annotated[ StrictBool | None, Field( description="Whether an admissible item must have at least one exact reference/evaluation pair whose outcome is verified, whose reference is published in the evidence, and whose claim type and issuer match the buyer's accepted lists as well as the seller's issuer and resolver policy. Setting true requires accepted_attestation_issuers." ), + NoExplicitNull, ] = False accepted_attestation_issuers: Annotated[ list[attestation_issuer.AttestationIssuer] | None, @@ -124,6 +134,7 @@ class AudienceEvidenceRequirements(AdCPBaseModel): description="Buyer allowlist for independent attestation issuers. An exact reference/evaluation pair satisfies the requirement only when reference.issuer matches an entry here. This list narrows and never broadens the seller's accepted issuer policy.", min_length=1, ), + NoExplicitNull, ] = None accepted_attestation_claim_types: Annotated[ list[AnyUrl] | None, @@ -131,5 +142,6 @@ class AudienceEvidenceRequirements(AdCPBaseModel): description='Optional acceptable claim types when attestation is used. Buyer-supplied claim types further constrain but never broaden seller policy.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/audience_evidence_selection.py b/src/adcp/types/domains/core/audience_evidence_selection.py index 06ba87388..50bd26a0b 100644 --- a/src/adcp/types/domains/core/audience_evidence_selection.py +++ b/src/adcp/types/domains/core/audience_evidence_selection.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/audience_evidence_selection.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import attestation_reference, audience_evidence @@ -84,6 +84,7 @@ class AudienceEvidenceSelection(AdCPBaseModel): Field( description='Optional inline immutable snapshot. Its identity, version, and digest MUST equal the surrounding fields.' ), + NoExplicitNull, ] = None attestation_evaluations: Annotated[ list[AttestationEvaluation] | None, @@ -92,5 +93,6 @@ class AudienceEvidenceSelection(AdCPBaseModel): max_length=10, min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/audience_member.py b/src/adcp/types/domains/core/audience_member.py index 022ae6742..eb7f05c12 100644 --- a/src/adcp/types/domains/core/audience_member.py +++ b/src/adcp/types/domains/core/audience_member.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/audience_member.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..enums import uid_type @@ -40,6 +40,7 @@ class AudienceMember(AdCPBaseModel): description='SHA-256 hash of lowercase, trimmed email address. Pseudonymous PII, not anonymous — the email namespace is small enough that an unsalted SHA-256 is recoverable via precomputed dictionaries. Treat as PII for retention, consent, and access-control purposes. See docs/reference/privacy-considerations#unsalted-hashed-identifiers-are-pseudonymous-not-anonymous.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None hashed_phone: Annotated[ str | None, @@ -47,6 +48,7 @@ class AudienceMember(AdCPBaseModel): description='SHA-256 hash of E.164-formatted phone number (e.g. +12065551234). Pseudonymous PII, not anonymous — the E.164 namespace is small enough that an unsalted SHA-256 is recoverable via precomputed dictionaries. Treat as PII for retention, consent, and access-control purposes. See docs/reference/privacy-considerations#unsalted-hashed-identifiers-are-pseudonymous-not-anonymous.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None uids: Annotated[ list[Uid] | None, @@ -54,8 +56,9 @@ class AudienceMember(AdCPBaseModel): description='Universal ID values (MAIDs, RampID, UID2, etc.) for user matching.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> AudienceMember: diff --git a/src/adcp/types/domains/core/audience_selector.py b/src/adcp/types/domains/core/audience_selector.py index 0fb308a27..f0c4400c5 100644 --- a/src/adcp/types/domains/core/audience_selector.py +++ b/src/adcp/types/domains/core/audience_selector.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/audience_selector.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from . import signal_id as signal_id_1 @@ -33,6 +33,7 @@ class AudienceSelector4(AdCPBaseModel): Field( description="Optional grouping hint for the governance agent (e.g., 'demographic', 'behavioral', 'contextual', 'financial')" ), + NoExplicitNull, ] = None @@ -46,6 +47,7 @@ class AudienceSelector1(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New selectors SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -53,6 +55,7 @@ class AudienceSelector1(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[Literal['binary'], Field(description='Discriminator for binary signals')] = 'binary' value: Annotated[ @@ -73,6 +76,7 @@ class AudienceSelector2(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New selectors SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -80,6 +84,7 @@ class AudienceSelector2(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['categorical'], Field(description='Discriminator for categorical signals') @@ -103,6 +108,7 @@ class AudienceSelector3(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New selectors SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -110,6 +116,7 @@ class AudienceSelector3(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['numeric'], Field(description='Discriminator for numeric signals') @@ -119,12 +126,14 @@ class AudienceSelector3(AdCPBaseModel): Field( description='Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided.' ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description='Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/audience_source.py b/src/adcp/types/domains/core/audience_source.py index d4ac99053..7d709856c 100644 --- a/src/adcp/types/domains/core/audience_source.py +++ b/src/adcp/types/domains/core/audience_source.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/audience_source.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import brand_ref @@ -34,6 +34,7 @@ class AudienceSource1(AdCPBaseModel): Field( description='ISO 8601 time after which the buyer will revoke the grant. Declarative — tells the seller when the pipe closes so re-read behavior is predictable. Expiry bounds the access window, not retention: revocation does not claw back matched membership, and retention remains governed by the buyer-seller data processing agreement.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/authorized_agent_base.py b/src/adcp/types/domains/core/authorized_agent_base.py index 46dcfef5a..91a2594bd 100644 --- a/src/adcp/types/domains/core/authorized_agent_base.py +++ b/src/adcp/types/domains/core/authorized_agent_base.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/authorized_agent_base.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, Field from . import agent_encryption_key, agent_signing_key @@ -33,6 +33,7 @@ class AuthorizedAgentBaseFields(AdCPBaseModel): description='Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.', min_length=1, ), + NoExplicitNull, ] = None encryption_keys: Annotated[ list[agent_encryption_key.AgentEncryptionKey] | None, @@ -40,10 +41,12 @@ class AuthorizedAgentBaseFields(AdCPBaseModel): description='X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.', min_length=1, ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/bidding_policy.py b/src/adcp/types/domains/core/bidding_policy.py index d6d448201..5b131de30 100644 --- a/src/adcp/types/domains/core/bidding_policy.py +++ b/src/adcp/types/domains/core/bidding_policy.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/bidding_policy.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T12:09:21+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -68,6 +68,7 @@ class BiddingPolicy(AdCPBaseModel): Field( description='Explicitly use seller/provider automatic bidding at this authored scope. At package scope this is a complete override of a media-buy policy, not inheritance. It MUST be the only field in the block and MUST be preserved on readback.' ), + NoExplicitNull, ] = None bid_amount: Annotated[ StrictFloat | None, @@ -75,6 +76,7 @@ class BiddingPolicy(AdCPBaseModel): description="Manual auction bid denominated in the media-buy currency and expressed per the selected pricing option's auction unit. For example, a CPM option interprets the amount per thousand impressions. This is the amount submitted to the auction, not a promise that the clearing price equals it. Requires an auction-priced pricing option whose currency equals the media-buy currency.", gt=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictFloat | None, @@ -82,16 +84,19 @@ class BiddingPolicy(AdCPBaseModel): description="Hard per-auction ceiling denominated in the media-buy currency and expressed per the selected pricing option's auction unit. This is the only canonical hard auction ceiling and MUST NOT be translated into an average outcome-cost control. Requires an auction-priced pricing option whose currency equals the media-buy currency. May stand alone or supplement cost_per/roas only when the relevant scope capability advertises that combination.", gt=0.0, ), + NoExplicitNull, ] = None cost_per: Annotated[ CostPer | None, Field( description="Average cost control per result of the scope-bound primary optimization goal. At seller-optimized media-buy scope it binds to budget_allocation.optimization_goals; at package scope it binds to that package's optimization_goals; at fixed media-buy scope it binds independently to each inheriting package and is valid only when their primary-goal result units are compatible. Metric goals are compatible only when metric and every result-defining qualifier match; vendor_metric goals only when vendor and metric_id match; event goals only when the event_type/custom_event_name set and resolved attribution_window match. Primary is the earliest array entry among goals tied for the lowest explicit numeric priority; unprioritized goals follow explicitly prioritized goals; when all priorities are absent, the first entry is primary." ), + NoExplicitNull, ] = None roas: Annotated[ Roas | None, Field( description='Dimensionless return-on-ad-spend control bound to the same scope-specific primary goal rules as cost_per. The bound goal must be value-bearing; a fixed media-buy default requires a value-bearing primary goal on every inheriting package. Every referenced value-bearing event source MUST declare value_currencies containing the media-buy currency. The seller validates this at buy creation; each buy consumes only exact-currency records, while other declared currencies remain available to other buys. Sellers MUST NOT perform currency conversion.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/bidding_policy_capability.py b/src/adcp/types/domains/core/bidding_policy_capability.py index 02de476bf..2303ae026 100644 --- a/src/adcp/types/domains/core/bidding_policy_capability.py +++ b/src/adcp/types/domains/core/bidding_policy_capability.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/bidding_policy_capability.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -61,6 +61,7 @@ class PolicyProfile(AdCPBaseModel): description='Standalone policy modes accepted in this exact scope and allocation context. Combination-only components belong only in supported_combinations and need not appear here. `automatic` means the seller accepts and preserves an explicit `{automatic:true}` authored block; omission only invokes inheritance/default behavior.', min_length=1, ), + NoExplicitNull, ] = None cost_per_strengths: Annotated[ list[CostPerStrength] | None, @@ -68,6 +69,7 @@ class PolicyProfile(AdCPBaseModel): description='Supported standalone cost_per strengths in this profile. Required exactly when modes includes cost_per.', min_length=1, ), + NoExplicitNull, ] = None roas_strengths: Annotated[ list[RoasStrength] | None, @@ -75,6 +77,7 @@ class PolicyProfile(AdCPBaseModel): description='Supported standalone roas strengths in this profile. Required exactly when modes includes roas.', min_length=1, ), + NoExplicitNull, ] = None supported_combinations: Annotated[ list[MaxBidWithCostPer | MaxBidWithRoas] | None, @@ -82,6 +85,7 @@ class PolicyProfile(AdCPBaseModel): description='Multi-field policies supported in this exact scope and allocation context. Each entry independently qualifies the strengths supported in that combination; standalone strength support does not imply combination support. Absence means no multi-field combination is claimed.', min_length=1, ), + NoExplicitNull, ] = None @@ -92,10 +96,12 @@ class ScopeCapability(AdCPBaseModel): fixed: Annotated[ PolicyProfile | None, Field(description='Policies supported when budget allocation is fixed or omitted.'), + NoExplicitNull, ] = None seller_optimized: Annotated[ PolicyProfile | None, Field(description='Policies supported when budget_allocation.mode is seller_optimized.'), + NoExplicitNull, ] = None @@ -108,10 +114,12 @@ class BiddingPolicyCapability(AdCPBaseModel): Field( description='Policies accepted at media-buy scope and inherited by packages that omit a package override.' ), + NoExplicitNull, ] = None package: Annotated[ ScopeCapability | None, Field( description='Policies accepted as package-authored policies, including explicit automatic overrides.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/brand_key.py b/src/adcp/types/domains/core/brand_key.py index 57dfbd434..bf5e2b589 100644 --- a/src/adcp/types/domains/core/brand_key.py +++ b/src/adcp/types/domains/core/brand_key.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/brand_key.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -34,6 +34,7 @@ class BrandKey(AdCPBaseModel): Field( description='Brand within a house-of-brands manifest. Omit for a single-brand domain.' ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -41,4 +42,5 @@ class BrandKey(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit when the identity is global or the house does not split the brand geographically. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account and proposal identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/brand_ref.py b/src/adcp/types/domains/core/brand_ref.py index 9ce889552..7ef0dfc49 100644 --- a/src/adcp/types/domains/core/brand_ref.py +++ b/src/adcp/types/domains/core/brand_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/brand_ref.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, EmailStr, Field @@ -23,35 +23,38 @@ class DataSubjectContestation(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - url: AnyUrl | None = None - email: EmailStr | None = None - languages: list[str] | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + email: Annotated[EmailStr | None, NoExplicitNull] = None + languages: Annotated[list[str] | None, NoExplicitNull] = None class Colors(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None - accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$')] = None + primary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + secondary: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None + accent: Annotated[str | None, Field(pattern='^#[0-9a-fA-F]{6}$'), NoExplicitNull] = None class BrandKitOverride(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - logo: Annotated[image_asset.ImageAsset | None, Field(description='Override logo asset.')] = None - colors: Annotated[Colors | None, Field(description='Override brand colors (hex strings).')] = ( - None - ) + logo: Annotated[ + image_asset.ImageAsset | None, Field(description='Override logo asset.'), NoExplicitNull + ] = None + colors: Annotated[ + Colors | None, Field(description='Override brand colors (hex strings).'), NoExplicitNull + ] = None voice: Annotated[ str | None, Field( description='Override brand-voice description for surface-composed text/audio output.' ), + NoExplicitNull, ] = None - tagline: Annotated[str | None, Field(description='Override tagline.')] = None + tagline: Annotated[str | None, Field(description='Override tagline.'), NoExplicitNull] = None class BrandReference(AdCPBaseModel): @@ -70,6 +73,7 @@ class BrandReference(AdCPBaseModel): Field( description='Brand identifier within the house portfolio. Optional for single-brand domains.' ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -77,22 +81,26 @@ class BrandReference(AdCPBaseModel): description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.', min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, Field( description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json." ), + NoExplicitNull, ] = None data_subject_contestation: Annotated[ DataSubjectContestation | None, Field( description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing." ), + NoExplicitNull, ] = None brand_kit_override: Annotated[ BrandKitOverride | None, Field( description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/brand_response_authorization_result.py b/src/adcp/types/domains/core/brand_response_authorization_result.py index 33a5de9f6..21271220e 100644 --- a/src/adcp/types/domains/core/brand_response_authorization_result.py +++ b/src/adcp/types/domains/core/brand_response_authorization_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/brand_response_authorization_result.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -38,7 +38,9 @@ class BrandResponseAuthorizationResult1(AdCPBaseModel): ), ] = 'trusted' reason: Annotated[ - Reason | None, Field(description='Machine-readable reason for an untrusted result.') + Reason | None, + Field(description='Machine-readable reason for an untrusted result.'), + NoExplicitNull, ] = None kid: Annotated[str, Field(description='JWS kid evaluated by the cross-check.', min_length=1)] jwks_uri: Annotated[ @@ -61,13 +63,16 @@ class BrandResponseAuthorizationResult2(AdCPBaseModel): ] = 'untrusted' reason: Annotated[Reason, Field(description='Machine-readable reason for an untrusted result.')] kid: Annotated[ - str | None, Field(description='JWS kid evaluated by the cross-check.', min_length=1) + str | None, + Field(description='JWS kid evaluated by the cross-check.', min_length=1), + NoExplicitNull, ] = None jwks_uri: Annotated[ AnyUrl | None, Field( description='JWKS URI selected only from the matched brand.json agent entry, or its same-origin default.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/budget_allocation.py b/src/adcp/types/domains/core/budget_allocation.py index 1a7055a44..1cca97cbe 100644 --- a/src/adcp/types/domains/core/budget_allocation.py +++ b/src/adcp/types/domains/core/budget_allocation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/budget_allocation.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import event_type as event_type_1 @@ -138,6 +138,7 @@ class TargetFrequency(AdCPBaseModel): description='Minimum frequency for an entity to be considered meaningfully reached within the window. Impressions that would bring an entity below this threshold are treated as high-value (growing reach). When omitted, the seller uses their platform default (typically 1).', ge=1, ), + NoExplicitNull, ] = None max: Annotated[ SchemaInt | None, @@ -145,6 +146,7 @@ class TargetFrequency(AdCPBaseModel): description='Frequency at which an entity is considered saturated within the window. Impressions toward entities at or above this threshold are treated as lower-value. When both min and max are present, max must be greater than or equal to min. When omitted, the seller determines the saturation point.', ge=1, ), + NoExplicitNull, ] = None window: Annotated[ duration.Duration, @@ -176,18 +178,21 @@ class EventSource(AdCPBaseModel): Field( description="Required when event_type is 'custom'. Platform-specific name for the custom event." ), + NoExplicitNull, ] = None value_field: Annotated[ str | None, Field( description="Which field in the event's custom_data carries the monetary value. The seller must use this field for value extraction and aggregation when computing ROAS and conversion value metrics. Required on at least one entry when target.kind is 'per_ad_spend' or 'maximize_value' — sellers must reject these target kinds when no event source entry includes value_field. When canonical bidding.roas binds to this goal, every referenced value-bearing source MUST declare value_currencies containing the media-buy currency; the seller validates this at buy creation. Once that source contract is declared, log_event rejects monetary records whose currency is missing or absent from the list. For each buy, only records exactly matching its media-buy currency contribute to ROAS; records in other declared currencies remain available to matching buys, and sellers MUST NOT convert between them. When present without a value-oriented target, the seller may use it for delivery reporting but must not change the objective. Common values: 'value', 'order_total', 'profit_margin'. This is not passed as a parameter to underlying platform APIs — the seller maps it to their platform's value ingestion mechanism." ), + NoExplicitNull, ] = None value_factor: Annotated[ StrictFloat | None, Field( description="Unit-scaling multiplier the seller must apply to value_field before aggregation. Use -1 for refund events (negate the value), 0.01 for values in cents, -0.01 for refunds in cents. It MUST NOT be used for currency conversion. A value of 0 zeroes out this source's value contribution (the source still counts for event dedup). Defaults to 1. This is not passed as a parameter to underlying platform APIs — the seller applies it when computing aggregated value metrics." ), + NoExplicitNull, ] = 1 @@ -209,12 +214,14 @@ class OptimizationGoal2(AdCPBaseModel): description='Target cost or return for this event goal. When omitted, the seller optimizes for maximum conversion count within budget — regardless of whether value_field is present on event sources. The presence of value_field alone does not change the optimization objective; it only makes value available for reporting. An explicit target of maximize_value or per_ad_spend is required to steer toward value.', discriminator='kind', ), + NoExplicitNull, ] = None attribution_window: Annotated[ attribution_window_1.AttributionWindow | None, Field( description="Attribution window for this optimization goal — references the canonical `attribution-window` shape (post_click, post_view, model). Values must match an option declared in the seller's `conversion_tracking.attribution_windows` capability. Sellers MUST reject windows not in their declared capabilities. When the entire field is omitted, the seller uses their default window." ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -222,6 +229,7 @@ class OptimizationGoal2(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None @@ -245,24 +253,28 @@ class OptimizationGoal1(AdCPBaseModel): Field( description="Viewability standard the goal is judged against. Required when metric is 'viewable_rate'; optional for 'viewed_seconds' (seller default standard when omitted); not allowed for other metrics. Must be in metric_optimization.supported_viewability_standards when declared. A goal below a same-standard viewability performance_standard never relaxes that standard." ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference | None, Field( description="Measurement vendor judging a viewable_rate or viewed_seconds goal; not allowed for other metrics. When omitted, the seller's default viewability measurement applies. Sellers MUST reject a vendor they cannot optimize against rather than substitute another." ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description="Unit for reach measurement. Required when metric is 'reach'. Must be a value declared in the product's metric_optimization.supported_reach_units." ), + NoExplicitNull, ] = None target_frequency: Annotated[ TargetFrequency | None, Field( description="Target frequency band for reach optimization. Only applicable when metric is 'reach'. Frames frequency as an optimization signal: the seller should treat impressions toward entities already within the [min, max] band as lower-value, and impressions toward unreached entities as higher-value. This shifts budget toward fresh reach rather than re-reaching known users. When omitted, the seller maximizes unique reach without a frequency constraint. A hard cap can still be layered via targeting_overlay.frequency_cap if a ceiling is needed." ), + NoExplicitNull, ] = None view_duration_seconds: Annotated[ StrictFloat | None, @@ -270,6 +282,7 @@ class OptimizationGoal1(AdCPBaseModel): description="Minimum video view duration in seconds that qualifies as a completed_view for this goal. Only applicable when metric is 'completed_views'. When omitted, the seller uses their platform default (typically 2–15 seconds). Common values: 2 (Snap/LinkedIn default), 6 (TikTok), 15 (Snap 15-second views, Meta ThruPlay). Sellers declare which durations they support in metric_optimization.supported_view_durations. Sellers must reject goals with unsupported values — silent rounding would create measurement discrepancies.", gt=0.0, ), + NoExplicitNull, ] = None target: Annotated[ Target | Target3 | None, @@ -277,6 +290,7 @@ class OptimizationGoal1(AdCPBaseModel): description='Target for this metric. When omitted, the seller optimizes for maximum metric volume within budget.', discriminator='kind', ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -284,6 +298,7 @@ class OptimizationGoal1(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None @@ -310,6 +325,7 @@ class OptimizationGoal3(AdCPBaseModel): description="Target for this vendor metric. When omitted, the seller optimizes for maximum metric volume / score within budget. `cost_per` and `threshold_rate` semantics mirror the same target kinds on the `metric` kind — units are vendor-defined and depend on the vendor's `measurement.metrics[]` declaration for this `metric_id`.", discriminator='kind', ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -317,6 +333,7 @@ class OptimizationGoal3(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/budget_range.py b/src/adcp/types/domains/core/budget_range.py index a7580e938..0dd64575e 100644 --- a/src/adcp/types/domains/core/budget_range.py +++ b/src/adcp/types/domains/core/budget_range.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/budget_range.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat, model_validator @@ -17,8 +17,8 @@ class BudgetRange(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - min: Annotated[StrictFloat | None, Field(ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(ge=0.0)] = None + min: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + max: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/business_entity.py b/src/adcp/types/domains/core/business_entity.py index 8d5f43ecb..c1cbf241d 100644 --- a/src/adcp/types/domains/core/business_entity.py +++ b/src/adcp/types/domains/core/business_entity.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/business_entity.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, EmailStr, Field from . import ext as ext_1 @@ -23,7 +23,7 @@ class Address(AdCPBaseModel): city: Annotated[str, Field(max_length=100)] postal_code: Annotated[str, Field(max_length=20)] region: Annotated[ - str | None, Field(description='State, province, or region', max_length=100) + str | None, Field(description='State, province, or region', max_length=100), NoExplicitNull ] = None country: Annotated[ str, Field(description='ISO 3166-1 alpha-2 country code', pattern='^[A-Z]{2}$') @@ -44,11 +44,11 @@ class Contact(AdCPBaseModel): role: Annotated[ Role, Field(description="Contact's functional role in the business relationship") ] - name: Annotated[str | None, Field(description='Full name of the contact', max_length=200)] = ( - None - ) - email: Annotated[EmailStr | None, Field(max_length=254)] = None - phone: Annotated[str | None, Field(max_length=30)] = None + name: Annotated[ + str | None, Field(description='Full name of the contact', max_length=200), NoExplicitNull + ] = None + email: Annotated[EmailStr | None, Field(max_length=254), NoExplicitNull] = None + phone: Annotated[str | None, Field(max_length=30), NoExplicitNull] = None class Bank(AdCPBaseModel): @@ -62,6 +62,7 @@ class Bank(AdCPBaseModel): description='International Bank Account Number (SEPA markets)', pattern='^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$', ), + NoExplicitNull, ] = None bic: Annotated[ str | None, @@ -69,6 +70,7 @@ class Bank(AdCPBaseModel): description='Bank Identifier Code / SWIFT code (SEPA markets)', pattern='^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$', ), + NoExplicitNull, ] = None routing_number: Annotated[ str | None, @@ -76,9 +78,12 @@ class Bank(AdCPBaseModel): description='Bank routing number for non-SEPA markets (e.g., US ABA routing number, Canadian transit/institution number)', max_length=30, ), + NoExplicitNull, ] = None account_number: Annotated[ - str | None, Field(description='Bank account number for non-SEPA markets', max_length=30) + str | None, + Field(description='Bank account number for non-SEPA markets', max_length=30), + NoExplicitNull, ] = None @@ -95,6 +100,7 @@ class BusinessEntity(AdCPBaseModel): description='VAT identification number (e.g., DE123456789 for Germany, FR12345678901 for France). Required for B2B invoicing in the EU. Must be normalized: no spaces, dots, or dashes.', pattern='^[A-Z]{2}[A-Z0-9]{2,13}$', ), + NoExplicitNull, ] = None tax_id: Annotated[ str | None, @@ -102,6 +108,7 @@ class BusinessEntity(AdCPBaseModel): description='Tax identification number for jurisdictions that do not use VAT (e.g., US EIN)', max_length=30, ), + NoExplicitNull, ] = None registration_number: Annotated[ str | None, @@ -109,9 +116,12 @@ class BusinessEntity(AdCPBaseModel): description='Company registration number (e.g., HRB 12345 for German Handelsregister)', max_length=50, ), + NoExplicitNull, ] = None address: Annotated[ - Address | None, Field(description='Postal address for invoicing and legal correspondence') + Address | None, + Field(description='Postal address for invoicing and legal correspondence'), + NoExplicitNull, ] = None contacts: Annotated[ list[Contact] | None, @@ -119,11 +129,13 @@ class BusinessEntity(AdCPBaseModel): description='Contacts for billing, legal, and operational matters. Contains personal data subject to GDPR and equivalent regulations. Implementations MUST use this data only for invoicing and account management.', max_length=10, ), + NoExplicitNull, ] = None bank: Annotated[ Bank | None, Field( description='Bank account details for payment processing. Write-only: included in requests to provide payment coordinates, but MUST NOT be echoed in responses. Sellers store these details and confirm receipt without returning them.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/cancellation_policy.py b/src/adcp/types/domains/core/cancellation_policy.py index a053919fe..2a4312f9c 100644 --- a/src/adcp/types/domains/core/cancellation_policy.py +++ b/src/adcp/types/domains/core/cancellation_policy.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/cancellation_policy.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from . import duration @@ -37,6 +37,7 @@ class CancellationFee(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None amount: Annotated[ StrictFloat | None, @@ -44,6 +45,7 @@ class CancellationFee(AdCPBaseModel): description="Fixed fee amount in the buy's currency. Required when type is 'fixed_fee'.", ge=0.0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/canonical_account_ref.py b/src/adcp/types/domains/core/canonical_account_ref.py index e8972c2a6..425c6b81f 100644 --- a/src/adcp/types/domains/core/canonical_account_ref.py +++ b/src/adcp/types/domains/core/canonical_account_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_account_ref.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import brand_key @@ -33,6 +33,7 @@ class CanonicalAccountReference2(AdCPBaseModel): Field( description='Optional operator-owned business unit, agency seat, or platform account. Only id participates in identity; name is mutable display metadata.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -40,6 +41,7 @@ class CanonicalAccountReference2(AdCPBaseModel): description='Immutable ISO 4217 transaction currency when the advertiser object is currency-bound. When present, this is part of the natural key and all media buys on the account use it. Omit for per-media-buy currency selection.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, @@ -47,8 +49,9 @@ class CanonicalAccountReference2(AdCPBaseModel): description='Immutable account timezone. Include it in the natural key only for buyer-selected account_fixed provisioning.', min_length=1, ), + NoExplicitNull, ] = None - sandbox: StrictBool | None = False + sandbox: Annotated[StrictBool | None, NoExplicitNull] = False CanonicalAccountReference = Annotated[ diff --git a/src/adcp/types/domains/core/canonical_audience_evidence.py b/src/adcp/types/domains/core/canonical_audience_evidence.py index 3b07cb61a..a31e1f918 100644 --- a/src/adcp/types/domains/core/canonical_audience_evidence.py +++ b/src/adcp/types/domains/core/canonical_audience_evidence.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_audience_evidence.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictFloat @@ -35,7 +35,7 @@ class Baseline(AdCPBaseModel): system: AnyUrl population_id: Annotated[str, Field(min_length=1)] version: Annotated[str, Field(min_length=1)] - description: str | None = None + description: Annotated[str | None, NoExplicitNull] = None class EvidenceType(StrEnum): @@ -65,18 +65,21 @@ class CanonicalAudienceEvidence(AdCPBaseModel): evidence_type: EvidenceType methodology: audience_evidence_methodology.AudienceEvidenceMethodology subject_type: audience_subject_type.AudienceSubjectType - resolution_method: audience_resolution_method.AudienceResolutionMethod | None = None + resolution_method: Annotated[ + audience_resolution_method.AudienceResolutionMethod | None, NoExplicitNull + ] = None provider: brand_key.BrandKey measurement_window: date_range.DateRange - sample_size: Annotated[SchemaInt | None, Field(ge=1)] = None - confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0)] = None + sample_size: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0), NoExplicitNull] = None last_updated: AwareDatetime - methodology_url: AnyUrl | None = None + methodology_url: Annotated[AnyUrl | None, NoExplicitNull] = None attestation_digests: Annotated[ list[AttestationDigest] | None, Field( description='Portable-attestation reference digests available through the evidence provider; credential bodies are not inlined into product discovery.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_audience_evidence_selection.py b/src/adcp/types/domains/core/canonical_audience_evidence_selection.py index 870e13462..bc1ae73db 100644 --- a/src/adcp/types/domains/core/canonical_audience_evidence_selection.py +++ b/src/adcp/types/domains/core/canonical_audience_evidence_selection.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_audience_evidence_selection.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -35,8 +35,10 @@ class CanonicalAudienceEvidenceSelection(AdCPBaseModel): version: Annotated[str, Field(min_length=1)] content_digest: Annotated[str, Field(pattern='^sha256:[a-f0-9]{64}$')] decision_use: DecisionUse - evidence: canonical_audience_evidence.CanonicalAudienceEvidence | None = None + evidence: Annotated[ + canonical_audience_evidence.CanonicalAudienceEvidence | None, NoExplicitNull + ] = None verified_attestation_digests: Annotated[ - list[VerifiedAttestationDigest] | None, Field(min_length=1) + list[VerifiedAttestationDigest] | None, Field(min_length=1), NoExplicitNull ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_delivery_forecast.py b/src/adcp/types/domains/core/canonical_delivery_forecast.py index fbf98ddee..03a2c45c9 100644 --- a/src/adcp/types/domains/core/canonical_delivery_forecast.py +++ b/src/adcp/types/domains/core/canonical_delivery_forecast.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_delivery_forecast.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import demographic_system as demographic_system_1 @@ -22,13 +22,19 @@ class CanonicalDeliveryForecast(AdCPBaseModel): extra='forbid', ) points: Annotated[list[canonical_forecast_point.CanonicalForecastPoint], Field(min_length=1)] - forecast_range_unit: forecast_range_unit_1.ForecastRangeUnit | None = None + forecast_range_unit: Annotated[ + forecast_range_unit_1.ForecastRangeUnit | None, NoExplicitNull + ] = None method: forecast_method.ForecastMethod currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] - demographic_system: demographic_system_1.DemographicSystem | None = None - demographic: str | None = None - measurement_source: Annotated[str | None, Field(max_length=64, pattern='^[a-z0-9_]+$')] = None - reach_unit: reach_unit_1.ReachUnit | None = None - generated_at: AwareDatetime | None = None - valid_until: AwareDatetime | None = None - ext: ext_1.ExtensionObject | None = None + demographic_system: Annotated[demographic_system_1.DemographicSystem | None, NoExplicitNull] = ( + None + ) + demographic: Annotated[str | None, NoExplicitNull] = None + measurement_source: Annotated[ + str | None, Field(max_length=64, pattern='^[a-z0-9_]+$'), NoExplicitNull + ] = None + reach_unit: Annotated[reach_unit_1.ReachUnit | None, NoExplicitNull] = None + generated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + valid_until: Annotated[AwareDatetime | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_forecast_point.py b/src/adcp/types/domains/core/canonical_forecast_point.py index 2513975a5..48fbecadd 100644 --- a/src/adcp/types/domains/core/canonical_forecast_point.py +++ b/src/adcp/types/domains/core/canonical_forecast_point.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_forecast_point.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Dict -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import availability_status as availability_status_1 @@ -25,7 +25,7 @@ class Metrics(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - coverage_rate: forecast_rate_range.ForecastRateRange | None = None + coverage_rate: Annotated[forecast_rate_range.ForecastRateRange | None, NoExplicitNull] = None @@ -34,25 +34,30 @@ class Viewability(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - vendor: brand_key.BrandKey | None = None - measurable_impressions: forecast_range.ForecastRange | None = None - viewable_impressions: forecast_range.ForecastRange | None = None - viewable_rate: forecast_rate_range.ForecastRateRange | None = None - viewed_seconds: forecast_range.ForecastRange | None = None - standard: viewability_standard.ViewabilityStandard | None = None + vendor: Annotated[brand_key.BrandKey | None, NoExplicitNull] = None + measurable_impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + viewable_impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + viewable_rate: Annotated[forecast_rate_range.ForecastRateRange | None, NoExplicitNull] = None + viewed_seconds: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + standard: Annotated[viewability_standard.ViewabilityStandard | None, NoExplicitNull] = None class CanonicalForecastPoint(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - label: Annotated[str | None, Field(max_length=128)] = None - budget: Annotated[StrictFloat | None, Field(ge=0.0)] = None - product_id: str | None = None - dimensions: forecast_point_dimensions.ForecastPointDimensions | None = None - availability_status: availability_status_1.AvailabilityStatus | None = None + label: Annotated[str | None, Field(max_length=128), NoExplicitNull] = None + budget: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + product_id: Annotated[str | None, NoExplicitNull] = None + dimensions: Annotated[ + forecast_point_dimensions.ForecastPointDimensions | None, NoExplicitNull + ] = None + availability_status: Annotated[ + availability_status_1.AvailabilityStatus | None, NoExplicitNull + ] = None metrics: Metrics - viewability: Viewability | None = None - vendor_metric_values: ( - list[canonical_forecast_vendor_metric_value.CanonicalForecastVendorMetricValue] | None - ) = None + viewability: Annotated[Viewability | None, NoExplicitNull] = None + vendor_metric_values: Annotated[ + list[canonical_forecast_vendor_metric_value.CanonicalForecastVendorMetricValue] | None, + NoExplicitNull, + ] = None diff --git a/src/adcp/types/domains/core/canonical_forecast_vendor_metric_value.py b/src/adcp/types/domains/core/canonical_forecast_vendor_metric_value.py index 0132ddf65..88946a282 100644 --- a/src/adcp/types/domains/core/canonical_forecast_vendor_metric_value.py +++ b/src/adcp/types/domains/core/canonical_forecast_vendor_metric_value.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_forecast_vendor_metric_value.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from typing import Any +from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict from . import brand_key, forecast_range, vendor_metric_id @@ -19,6 +19,6 @@ class CanonicalForecastVendorMetricValue(AdCPBaseModel): vendor: brand_key.BrandKey metric_id: vendor_metric_id.VendorMetricId value: forecast_range.ForecastRange - unit: str | None = None - measurable_impressions: forecast_range.ForecastRange | None = None - breakdown: dict[str, Any] | None = None + unit: Annotated[str | None, NoExplicitNull] = None + measurable_impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + breakdown: Annotated[dict[str, Any] | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_format_option.py b/src/adcp/types/domains/core/canonical_format_option.py index 85983d6f1..676f2b26b 100644 --- a/src/adcp/types/domains/core/canonical_format_option.py +++ b/src/adcp/types/domains/core/canonical_format_option.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_format_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import channels @@ -42,16 +42,18 @@ class CanonicalFormatOption(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - format_option_id: Annotated[str | None, Field(min_length=1)] = None + format_option_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None publisher_domain: Annotated[ str | None, Field(pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$'), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored production commitment for first-class manifest tracker execution. Presence requires a stable format_option_id. Creative-agent projections MUST strip this authority-bearing field.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -59,21 +61,29 @@ class CanonicalFormatOption(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='When true, every technical creative-acceptance constraint for this declaration layer is expressed and omitted technical fields mean no constraint. When false or absent, omitted technical fields are undeclared and MUST NOT be inferred. The effective contract is complete only when every applicable declaration layer asserts true.' ), + NoExplicitNull, ] = None - display_name: Annotated[str | None, Field(min_length=1)] = None - sample_render_url: AnyUrl | None = None - applies_to_channels: Annotated[list[channels.MediaChannel] | None, Field(min_length=1)] = None - seller_preference: SellerPreference | None = None - locale_policy: creative_locale_policy.CreativeLocalePolicy | None = None - canonical_formats_only: StrictBool | None = False - experimental: StrictBool | None = False + display_name: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + sample_render_url: Annotated[AnyUrl | None, NoExplicitNull] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, Field(min_length=1), NoExplicitNull + ] = None + seller_preference: Annotated[SellerPreference | None, NoExplicitNull] = None + locale_policy: Annotated[creative_locale_policy.CreativeLocalePolicy | None, NoExplicitNull] = ( + None + ) + canonical_formats_only: Annotated[StrictBool | None, NoExplicitNull] = False + experimental: Annotated[StrictBool | None, NoExplicitNull] = False format_kind: FormatKind params: dict[str, Any] - format_shape: Annotated[str | None, Field(min_length=1)] = None - format_schema: platform_extension_ref.PlatformExtensionReference | None = None + format_shape: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/core/canonical_measurement_terms.py b/src/adcp/types/domains/core/canonical_measurement_terms.py index 96c9c47b0..e4808c386 100644 --- a/src/adcp/types/domains/core/canonical_measurement_terms.py +++ b/src/adcp/types/domains/core/canonical_measurement_terms.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_measurement_terms.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import makegood_remedy @@ -18,9 +18,11 @@ class BillingMeasurement(AdCPBaseModel): extra='forbid', ) vendor: brand_key.BrandKey - max_variance_percent: Annotated[StrictFloat | None, Field(ge=0.0, lt=100.0)] = None - measurement_window: Annotated[str | None, Field(min_length=1)] = None - finalization_deadline_hours: Annotated[SchemaInt | None, Field(ge=0)] = None + max_variance_percent: Annotated[StrictFloat | None, Field(ge=0.0, lt=100.0), NoExplicitNull] = ( + None + ) + measurement_window: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + finalization_deadline_hours: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class MakegoodPolicy(AdCPBaseModel): @@ -34,5 +36,5 @@ class CanonicalMeasurementTerms(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - billing_measurement: BillingMeasurement | None = None - makegood_policy: MakegoodPolicy | None = None + billing_measurement: Annotated[BillingMeasurement | None, NoExplicitNull] = None + makegood_policy: Annotated[MakegoodPolicy | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_media_buy_action.py b/src/adcp/types/domains/core/canonical_media_buy_action.py index 75b00f1c0..182a845a8 100644 --- a/src/adcp/types/domains/core/canonical_media_buy_action.py +++ b/src/adcp/types/domains/core/canonical_media_buy_action.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/canonical_media_buy_action.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:13:02+00:00 from __future__ import annotations diff --git a/src/adcp/types/domains/core/canonical_media_buy_action_fields.py b/src/adcp/types/domains/core/canonical_media_buy_action_fields.py index 278979e8b..f9917c16e 100644 --- a/src/adcp/types/domains/core/canonical_media_buy_action_fields.py +++ b/src/adcp/types/domains/core/canonical_media_buy_action_fields.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_media_buy_action_fields.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import canonical_media_buy_action_mode @@ -32,13 +32,18 @@ class CanonicalMediaBuyActionFields(AdCPBaseModel): task: Task action: str mode: canonical_media_buy_action_mode.CanonicalMediaBuyActionMode - sla: sla_window.SlaWindow | None = None - change_term_id: media_buy_change_term_id.MediaBuyChangeTermId | None = None - terms_ref: media_buy_legacy_terms_ref.MediaBuyTermsReference | None = None + sla: Annotated[sla_window.SlaWindow | None, NoExplicitNull] = None + change_term_id: Annotated[ + media_buy_change_term_id.MediaBuyChangeTermId | None, NoExplicitNull + ] = None + terms_ref: Annotated[ + media_buy_legacy_terms_ref.MediaBuyTermsReference | None, NoExplicitNull + ] = None applicable_package_ids: Annotated[ list[applicable_package_id.ApplicablePackageId] | None, Field( description='Exact eligible packages for a package-scoped action; omission means all relevant packages.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/canonical_media_buy_features.py b/src/adcp/types/domains/core/canonical_media_buy_features.py index 75711473a..4cad892e7 100644 --- a/src/adcp/types/domains/core/canonical_media_buy_features.py +++ b/src/adcp/types/domains/core/canonical_media_buy_features.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_media_buy_features.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Dict -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import bidding_policy_capability @@ -17,36 +17,43 @@ class CanonicalMediaBuyFeatures(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - property_filtering: StrictBool | None = None - catalog_management: StrictBool | None = None + property_filtering: Annotated[StrictBool | None, NoExplicitNull] = None + catalog_management: Annotated[StrictBool | None, NoExplicitNull] = None reporting_commitment_snapshots: Annotated[ StrictBool | None, Field( description='Seller preserves the accepted per-purchase reporting contract and exposes it on proposal and MediaBuy readback.' ), + NoExplicitNull, ] = None seller_optimized_budget: Annotated[ StrictBool | None, Field( description='Core seller-optimized shared-budget contract: shared total_budget, seller allocation across packages, media-buy pacing, and allocation echo.' ), + NoExplicitNull, ] = None seller_optimized_package_budgets: Annotated[ StrictBool | None, Field( description='Package budget caps inside seller-optimized buys; implies seller_optimized_budget.' ), + NoExplicitNull, ] = None seller_optimized_min_spend_targets: Annotated[ StrictBool | None, Field( description='Package minimum-spend targets inside seller-optimized buys; implies seller_optimized_budget.' ), + NoExplicitNull, ] = None seller_optimized_package_pacing: Annotated[ StrictBool | None, Field( description='Package pacing inside seller-optimized buys; implies seller_optimized_budget.' ), + NoExplicitNull, + ] = None + bidding_policy: Annotated[ + bidding_policy_capability.BiddingPolicyCapability | None, NoExplicitNull ] = None - bidding_policy: bidding_policy_capability.BiddingPolicyCapability | None = None diff --git a/src/adcp/types/domains/core/canonical_metric_qualifier.py b/src/adcp/types/domains/core/canonical_metric_qualifier.py index c58737f95..ade981c1b 100644 --- a/src/adcp/types/domains/core/canonical_metric_qualifier.py +++ b/src/adcp/types/domains/core/canonical_metric_qualifier.py @@ -1,10 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_metric_qualifier.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types.base import AdCPBaseModel +from typing import Annotated + +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict from ..enums import attribution_methodology as attribution_methodology_1 @@ -18,8 +20,12 @@ class CanonicalMetricQualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_optimization_goal.py b/src/adcp/types/domains/core/canonical_optimization_goal.py index 200de5597..be23c45d7 100644 --- a/src/adcp/types/domains/core/canonical_optimization_goal.py +++ b/src/adcp/types/domains/core/canonical_optimization_goal.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_optimization_goal.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import event_type as event_type_1 @@ -53,8 +53,8 @@ class TargetFrequency(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[SchemaInt | None, Field(ge=1)] = None - max: Annotated[SchemaInt | None, Field(ge=1)] = None + min: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None window: duration.Duration @@ -64,13 +64,13 @@ class CanonicalOptimizationGoal1(AdCPBaseModel): ) kind: Literal['metric'] = 'metric' metric: Metric - standard: viewability_standard.ViewabilityStandard | None = None - vendor: brand_key.BrandKey | None = None - reach_unit: reach_unit_1.ReachUnit | None = None - target_frequency: TargetFrequency | None = None - view_duration_seconds: Annotated[StrictFloat | None, Field(gt=0.0)] = None - target: Target | None = None - priority: Annotated[SchemaInt | None, Field(ge=1)] = None + standard: Annotated[viewability_standard.ViewabilityStandard | None, NoExplicitNull] = None + vendor: Annotated[brand_key.BrandKey | None, NoExplicitNull] = None + reach_unit: Annotated[reach_unit_1.ReachUnit | None, NoExplicitNull] = None + target_frequency: Annotated[TargetFrequency | None, NoExplicitNull] = None + view_duration_seconds: Annotated[StrictFloat | None, Field(gt=0.0), NoExplicitNull] = None + target: Annotated[Target | None, NoExplicitNull] = None + priority: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None class EventSource(AdCPBaseModel): @@ -79,9 +79,9 @@ class EventSource(AdCPBaseModel): ) event_source_id: Annotated[str, Field(min_length=1)] event_type: event_type_1.EventType - custom_event_name: Annotated[str | None, Field(min_length=1)] = None - value_field: Annotated[str | None, Field(min_length=1)] = None - value_factor: StrictFloat | None = 1 + custom_event_name: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + value_field: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + value_factor: Annotated[StrictFloat | None, NoExplicitNull] = 1 class CanonicalOptimizationGoal2(AdCPBaseModel): @@ -90,9 +90,11 @@ class CanonicalOptimizationGoal2(AdCPBaseModel): ) kind: Literal['event'] = 'event' event_sources: Annotated[list[EventSource], Field(min_length=1)] - target: Target10 | None = None - attribution_window: attribution_window_1.AttributionWindow | None = None - priority: Annotated[SchemaInt | None, Field(ge=1)] = None + target: Annotated[Target10 | None, NoExplicitNull] = None + attribution_window: Annotated[attribution_window_1.AttributionWindow | None, NoExplicitNull] = ( + None + ) + priority: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None class CanonicalOptimizationGoal3(AdCPBaseModel): @@ -102,8 +104,8 @@ class CanonicalOptimizationGoal3(AdCPBaseModel): kind: Literal['vendor_metric'] = 'vendor_metric' vendor: brand_key.BrandKey metric_id: vendor_metric_id.VendorMetricId - target: Target11 | None = None - priority: Annotated[SchemaInt | None, Field(ge=1)] = None + target: Annotated[Target11 | None, NoExplicitNull] = None + priority: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None CanonicalOptimizationGoal = Annotated[ diff --git a/src/adcp/types/domains/core/canonical_performance_standard.py b/src/adcp/types/domains/core/canonical_performance_standard.py index 193960353..e0696d395 100644 --- a/src/adcp/types/domains/core/canonical_performance_standard.py +++ b/src/adcp/types/domains/core/canonical_performance_standard.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_performance_standard.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import performance_standard_metric, viewability_standard @@ -19,5 +19,5 @@ class CanonicalPerformanceStandard(AdCPBaseModel): ) metric: performance_standard_metric.PerformanceStandardMetric threshold: Annotated[StrictFloat, Field(ge=0.0, le=1.0)] - standard: viewability_standard.ViewabilityStandard | None = None + standard: Annotated[viewability_standard.ViewabilityStandard | None, NoExplicitNull] = None vendor: brand_key.BrandKey diff --git a/src/adcp/types/domains/core/canonical_placement.py b/src/adcp/types/domains/core/canonical_placement.py index 738eabc38..4c9ad4ac4 100644 --- a/src/adcp/types/domains/core/canonical_placement.py +++ b/src/adcp/types/domains/core/canonical_placement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_placement.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ..enums import ( @@ -57,6 +57,7 @@ class CanonicalDoohPlacementAttributes(AdCPBaseModel): description='Scheduled duration of one ad slot in seconds; not the creative-duration contract.', ge=1, ), + NoExplicitNull, ] = None loop_duration_seconds: Annotated[ SchemaInt | None, @@ -64,13 +65,15 @@ class CanonicalDoohPlacementAttributes(AdCPBaseModel): description='Duration of the full ad loop rotation in seconds and the canonical source for loop duration.', ge=1, ), + NoExplicitNull, ] = None - screen_resolution: CanonicalDoohScreenResolution | None = None + screen_resolution: Annotated[CanonicalDoohScreenResolution | None, NoExplicitNull] = None motion: Annotated[ dooh_motion_type.DoohMotionType | None, Field( description='Physical motion capability of a visual DOOH screen, not an accepted-format declaration. Omit for audio-only placements.' ), + NoExplicitNull, ] = None @@ -92,25 +95,34 @@ class CanonicalProductPlacement1(AdCPBaseModel): Field( description='Defining sales agent for seller_inline identity. Recommended for new declarations; optional only to preserve legacy product-context placements.' ), + NoExplicitNull, ] = None - name: Annotated[str | None, Field(min_length=1)] = None - description: str | None = None + name: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, NoExplicitNull] = None mode: Mode - tags: list[str] | None = None + tags: Annotated[list[str] | None, NoExplicitNull] = None format_options: Annotated[ - list[canonical_format_option.CanonicalFormatOption] | None, Field(min_length=1) + list[canonical_format_option.CanonicalFormatOption] | None, + Field(min_length=1), + NoExplicitNull, ] = None video_placement_types: Annotated[ - list[video_placement_type.VideoPlacementType] | None, Field(min_length=1) + list[video_placement_type.VideoPlacementType] | None, Field(min_length=1), NoExplicitNull ] = None audio_distribution_types: Annotated[ - list[audio_distribution_type.AudioDistributionType] | None, Field(min_length=1) + list[audio_distribution_type.AudioDistributionType] | None, + Field(min_length=1), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ - list[sponsored_placement_type.SponsoredPlacementType] | None, Field(min_length=1) + list[sponsored_placement_type.SponsoredPlacementType] | None, + Field(min_length=1), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ - list[social_placement_surface.SocialPlacementSurface] | None, Field(min_length=1) + list[social_placement_surface.SocialPlacementSurface] | None, + Field(min_length=1), + NoExplicitNull, ] = None identifiers: Annotated[ list[Identifier] | None, @@ -118,8 +130,11 @@ class CanonicalProductPlacement1(AdCPBaseModel): description='Optional external inventory identifiers for this placement. Externally governed values should be authority-prefixed; seller-local values are scoped by the surrounding publisher namespace. For publisher_ref placements, the effective set is the union of publisher and product declarations, de-duplicated by exact (type, value).', min_length=1, ), + NoExplicitNull, + ] = None + dooh_placement_attributes: Annotated[ + CanonicalDoohPlacementAttributes | None, NoExplicitNull ] = None - dooh_placement_attributes: CanonicalDoohPlacementAttributes | None = None class CanonicalProductPlacement2(AdCPBaseModel): @@ -134,31 +149,41 @@ class CanonicalProductPlacement2(AdCPBaseModel): description='For publisher_ref, the adagents.json publisher namespace. For seller_inline, optional inventory-publisher attribution only.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None seller_agent: Annotated[ seller_agent_ref.SellerAgentReference | None, Field( description='Defining sales agent for seller_inline identity. Recommended for new declarations; optional only to preserve legacy product-context placements.' ), + NoExplicitNull, ] = None name: Annotated[str, Field(min_length=1)] - description: str | None = None + description: Annotated[str | None, NoExplicitNull] = None mode: Mode - tags: list[str] | None = None + tags: Annotated[list[str] | None, NoExplicitNull] = None format_options: Annotated[ - list[canonical_format_option.CanonicalFormatOption] | None, Field(min_length=1) + list[canonical_format_option.CanonicalFormatOption] | None, + Field(min_length=1), + NoExplicitNull, ] = None video_placement_types: Annotated[ - list[video_placement_type.VideoPlacementType] | None, Field(min_length=1) + list[video_placement_type.VideoPlacementType] | None, Field(min_length=1), NoExplicitNull ] = None audio_distribution_types: Annotated[ - list[audio_distribution_type.AudioDistributionType] | None, Field(min_length=1) + list[audio_distribution_type.AudioDistributionType] | None, + Field(min_length=1), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ - list[sponsored_placement_type.SponsoredPlacementType] | None, Field(min_length=1) + list[sponsored_placement_type.SponsoredPlacementType] | None, + Field(min_length=1), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ - list[social_placement_surface.SocialPlacementSurface] | None, Field(min_length=1) + list[social_placement_surface.SocialPlacementSurface] | None, + Field(min_length=1), + NoExplicitNull, ] = None identifiers: Annotated[ list[Identifier] | None, @@ -166,8 +191,11 @@ class CanonicalProductPlacement2(AdCPBaseModel): description='Optional external inventory identifiers for this placement. Externally governed values should be authority-prefixed; seller-local values are scoped by the surrounding publisher namespace. For publisher_ref placements, the effective set is the union of publisher and product declarations, de-duplicated by exact (type, value).', min_length=1, ), + NoExplicitNull, + ] = None + dooh_placement_attributes: Annotated[ + CanonicalDoohPlacementAttributes | None, NoExplicitNull ] = None - dooh_placement_attributes: CanonicalDoohPlacementAttributes | None = None CanonicalProductPlacement = Annotated[ diff --git a/src/adcp/types/domains/core/canonical_pricing_option.py b/src/adcp/types/domains/core/canonical_pricing_option.py index 09fed7ff6..d235185c3 100644 --- a/src/adcp/types/domains/core/canonical_pricing_option.py +++ b/src/adcp/types/domains/core/canonical_pricing_option.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_pricing_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -36,15 +36,19 @@ class CanonicalPricingOption(AdCPBaseModel): pricing_option_id: Annotated[str, Field(min_length=1)] pricing_model: PricingModel currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] - fixed_price: Annotated[StrictFloat | None, Field(ge=0.0)] = None - floor_price: Annotated[StrictFloat | None, Field(ge=0.0)] = None - price_guidance: price_guidance_1.PriceGuidance | None = None - min_spend_per_package: Annotated[StrictFloat | None, Field(ge=0.0)] = None - price_breakdown: price_breakdown_1.PriceBreakdown | None = None - eligible_adjustments: list[adjustment_kind.PriceAdjustmentKind] | None = None - parameters: dict[str, Any] | None = None - event_type: event_type_1.EventType | None = None - custom_event_name: Annotated[str | None, Field(min_length=1)] = None - event_source_id: Annotated[str | None, Field(min_length=1)] = None - commission_rate: Annotated[StrictFloat | None, Field(gt=0.0, le=1.0)] = None - commission_basis_description: Annotated[str | None, Field(max_length=1000, min_length=1)] = None + fixed_price: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + floor_price: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + price_guidance: Annotated[price_guidance_1.PriceGuidance | None, NoExplicitNull] = None + min_spend_per_package: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + price_breakdown: Annotated[price_breakdown_1.PriceBreakdown | None, NoExplicitNull] = None + eligible_adjustments: Annotated[ + list[adjustment_kind.PriceAdjustmentKind] | None, NoExplicitNull + ] = None + parameters: Annotated[dict[str, Any] | None, NoExplicitNull] = None + event_type: Annotated[event_type_1.EventType | None, NoExplicitNull] = None + custom_event_name: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + event_source_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + commission_rate: Annotated[StrictFloat | None, Field(gt=0.0, le=1.0), NoExplicitNull] = None + commission_basis_description: Annotated[ + str | None, Field(max_length=1000, min_length=1), NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/core/canonical_product.py b/src/adcp/types/domains/core/canonical_product.py index 68373692c..b1797ccc7 100644 --- a/src/adcp/types/domains/core/canonical_product.py +++ b/src/adcp/types/domains/core/canonical_product.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_product.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictBool @@ -61,6 +61,7 @@ class PublisherProperty1(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -68,6 +69,7 @@ class PublisherProperty1(AdCPBaseModel): description="Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['all'], @@ -94,9 +96,9 @@ class CatalogMatch(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - matched_gtins: list[MatchedGtin] | None = None - matched_ids: list[str] | None = None - matched_count: Annotated[SchemaInt | None, Field(ge=0)] = None + matched_gtins: Annotated[list[MatchedGtin] | None, NoExplicitNull] = None + matched_ids: Annotated[list[str] | None, NoExplicitNull] = None + matched_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None submitted_count: Annotated[SchemaInt, Field(ge=0)] @@ -131,6 +133,7 @@ class PublisherProperty3(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -138,6 +141,7 @@ class PublisherProperty3(AdCPBaseModel): description="Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['by_tag'], Field(description='Discriminator indicating selection by property tags') @@ -168,52 +172,71 @@ class CanonicalProduct(AdCPBaseModel): ) product_id: Annotated[str, Field(min_length=1)] name: Annotated[str, Field(min_length=1)] - description: str | None = None - publisher_properties: Annotated[list[PublisherProperty] | None, Field(min_length=1)] = None - channels: list[channels_1.MediaChannel] | None = None + description: Annotated[str | None, NoExplicitNull] = None + publisher_properties: Annotated[ + list[PublisherProperty] | None, Field(min_length=1), NoExplicitNull + ] = None + channels: Annotated[list[channels_1.MediaChannel] | None, NoExplicitNull] = None video_placement_types: Annotated[ - list[video_placement_type.VideoPlacementType] | None, Field(min_length=1) + list[video_placement_type.VideoPlacementType] | None, Field(min_length=1), NoExplicitNull ] = None audio_distribution_types: Annotated[ - list[audio_distribution_type.AudioDistributionType] | None, Field(min_length=1) + list[audio_distribution_type.AudioDistributionType] | None, + Field(min_length=1), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ - list[sponsored_placement_type.SponsoredPlacementType] | None, Field(min_length=1) + list[sponsored_placement_type.SponsoredPlacementType] | None, + Field(min_length=1), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ - list[social_placement_surface.SocialPlacementSurface] | None, Field(min_length=1) + list[social_placement_surface.SocialPlacementSurface] | None, + Field(min_length=1), + NoExplicitNull, ] = None format_options: Annotated[ - list[canonical_format_option.CanonicalFormatOption] | None, Field(min_length=1) + list[canonical_format_option.CanonicalFormatOption] | None, + Field(min_length=1), + NoExplicitNull, + ] = None + placements: Annotated[ + list[canonical_placement.CanonicalProductPlacement] | None, NoExplicitNull ] = None - placements: list[canonical_placement.CanonicalProductPlacement] | None = None collections: Annotated[ list[collection_selector.CollectionSelector] | None, Field( description='Collections available in this product, each referencing collections declared in an adagents.json by domain and explicit collection_ids. The domain-only bulk-grant selector form is for authorization scoping, not product composition. A selected-mode collection_selection names collections from this set.', min_length=1, ), + NoExplicitNull, ] = None collection_targeting_allowed: Annotated[ StrictBool | None, Field( description="Whether buyers can select a subset of this product's collections through targeting_overlay.collection_list or targeting_overlay.collection_selection. When false, the product is a fixed bundle (a collection_selection that exactly restates the complete bundle remains an inherent match)." ), + NoExplicitNull, ] = False - delivery_type: delivery_type_1.DeliveryType | None = None - exclusivity: exclusivity_1.Exclusivity | None = None + delivery_type: Annotated[delivery_type_1.DeliveryType | None, NoExplicitNull] = None + exclusivity: Annotated[exclusivity_1.Exclusivity | None, NoExplicitNull] = None pricing_options: Annotated[ - list[canonical_pricing_option.CanonicalPricingOption] | None, Field(min_length=1) + list[canonical_pricing_option.CanonicalPricingOption] | None, + Field(min_length=1), + NoExplicitNull, + ] = None + forecast: Annotated[ + canonical_delivery_forecast.CanonicalDeliveryForecast | None, NoExplicitNull + ] = None + reporting_capabilities: Annotated[ + canonical_reporting_capabilities.CanonicalReportingCapabilities | None, NoExplicitNull ] = None - forecast: canonical_delivery_forecast.CanonicalDeliveryForecast | None = None - reporting_capabilities: ( - canonical_reporting_capabilities.CanonicalReportingCapabilities | None - ) = None measurement_terms: Annotated[ canonical_measurement_terms.CanonicalMeasurementTerms | None, Field( description='Default billing measurement and makegood terms inherited by a direct purchase unless a negotiated proposal replaces them.' ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[canonical_performance_standard.CanonicalPerformanceStandard] | None, @@ -221,28 +244,36 @@ class CanonicalProduct(AdCPBaseModel): description='Default performance thresholds and measurement vendors inherited by a direct purchase.', min_length=1, ), + NoExplicitNull, + ] = None + catalog_types: Annotated[ + list[catalog_type.CatalogType] | None, Field(min_length=1), NoExplicitNull + ] = None + signal_targeting_allowed: Annotated[StrictBool | None, NoExplicitNull] = None + signal_targeting_rules: Annotated[ + signal_targeting_rules_1.SignalTargetingRules | None, NoExplicitNull + ] = None + demographic_targeting: Annotated[ + demographic_targeting_capability.DemographicTargetingCapability | None, NoExplicitNull ] = None - catalog_types: Annotated[list[catalog_type.CatalogType] | None, Field(min_length=1)] = None - signal_targeting_allowed: StrictBool | None = None - signal_targeting_rules: signal_targeting_rules_1.SignalTargetingRules | None = None - demographic_targeting: ( - demographic_targeting_capability.DemographicTargetingCapability | None - ) = None overlay_support: Annotated[ targeting_overlay_support.TargetingOverlaySupport | None, Field( description='Binding product-scoped targeting dimensions the buyer may set independently on a package after discovery.' ), + NoExplicitNull, ] = None media_buy_support: Annotated[ media_buy_support_1.ProductMediaBuySupport | None, Field(description='Binding product participation in shared MediaBuy-level controls.'), + NoExplicitNull, ] = None identity: Annotated[ product_identity.ProductIdentity | None, Field( description='Experimental product-scoped identity and reach-measurement facts. See Product.identity for the cross-object delivery and frequency-cap rules.' ), + NoExplicitNull, ] = None execution_requirements: Annotated[ list[product_execution_requirement.ProductExecutionRequirement] | None, @@ -250,9 +281,12 @@ class CanonicalProduct(AdCPBaseModel): description='Experimental account resources a package on this product needs before it can be created. See Product.execution_requirements for completeness, binding, and rejection rules.', min_length=1, ), + NoExplicitNull, ] = None audience_evidence: Annotated[ - list[canonical_audience_evidence.CanonicalAudienceEvidence] | None, Field(min_length=1) + list[canonical_audience_evidence.CanonicalAudienceEvidence] | None, + Field(min_length=1), + NoExplicitNull, ] = None audience_evidence_selections: Annotated[ list[canonical_audience_evidence_selection.CanonicalAudienceEvidenceSelection] | None, @@ -260,31 +294,37 @@ class CanonicalProduct(AdCPBaseModel): description='Exact evidence snapshots that affected eligibility or ranking. Returned whenever evidence requirements affected the result, even if not requested explicitly.', min_length=1, ), + NoExplicitNull, ] = None - max_optimization_goals: Annotated[SchemaInt | None, Field(ge=0)] = None - catalog_match: CatalogMatch | None = None + max_optimization_goals: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + catalog_match: Annotated[CatalogMatch | None, NoExplicitNull] = None list_applications: Annotated[ list[inventory_list_application.InventoryListApplication] | None, Field( description='Product-scoped receipts for every effective property- or collection-list targeting reference. Sellers MUST return one receipt per application regardless of response field projection; exclusion applications receive a receipt even when summary.matched is zero, while zero matches for any inclusion application make the product ineligible and it is not returned. Each receipt uses the same pre-list product inventory baseline; pricing and forecast reflect inventory remaining after all effective lists are composed.', min_length=1, ), + NoExplicitNull, ] = None - brief_relevance: str | None = None + brief_relevance: Annotated[str | None, NoExplicitNull] = None targeting_resolution: Annotated[ product_targeting_resolution.ProductTargetingResolution | None, Field( description='Discovery-time targeting resolution bound to this configured product. modifications sparsely disclose product-specific differences from criteria.targeting_overlay; absence means exact acceptance of the structured overlay. Request-level brief interpretation is returned once on the response-root targeting_resolution. Selecting product_id accepts the disclosed modifications; pricing and forecast MUST reflect them. Requires expires_at.' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Expiration of a request-specific configured offer. Canonical products have no is_custom flag; expires_at is what marks an offer as request-specific. After it, the buyer rediscovers.' ), + NoExplicitNull, + ] = None + allowed_actions: Annotated[ + list[canonical_product_action.CanonicalProductAction] | None, NoExplicitNull + ] = None + acceptance_policy_profile_ids: Annotated[ + acceptance_policy_profile_ids_1.AcceptancePolicyProfileIds | None, NoExplicitNull ] = None - allowed_actions: list[canonical_product_action.CanonicalProductAction] | None = None - acceptance_policy_profile_ids: ( - acceptance_policy_profile_ids_1.AcceptancePolicyProfileIds | None - ) = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/canonical_product_action.py b/src/adcp/types/domains/core/canonical_product_action.py index c15fdeb61..92f6691e5 100644 --- a/src/adcp/types/domains/core/canonical_product_action.py +++ b/src/adcp/types/domains/core/canonical_product_action.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_product_action.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import canonical_media_buy_action, canonical_media_buy_action_mode, media_buy_status @@ -23,18 +23,20 @@ class CanonicalProductAction(AdCPBaseModel): list[canonical_media_buy_action_mode.CanonicalMediaBuyActionMode], Field(min_length=1) ] allowed_statuses: Annotated[ - list[media_buy_status.MediaBuyStatus] | None, Field(min_length=1) + list[media_buy_status.MediaBuyStatus] | None, Field(min_length=1), NoExplicitNull ] = None - sla: sla_window.SlaWindow | None = None + sla: Annotated[sla_window.SlaWindow | None, NoExplicitNull] = None constraints: Annotated[ change_term_constraints.MediaBuyChangeTermConstraints | None, Field( description='Advisory machine-readable bounds for product selection; proposal change terms restate binding bounds.' ), + NoExplicitNull, ] = None terms_ref: Annotated[ str | None, Field( description='Optional advisory pointer to published commercial terms. It is not a proposal change-term identity.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/canonical_projection_ref.py b/src/adcp/types/domains/core/canonical_projection_ref.py index ba7572552..45113aca3 100644 --- a/src/adcp/types/domains/core/canonical_projection_ref.py +++ b/src/adcp/types/domains/core/canonical_projection_ref.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_projection_ref.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import canonical_projection_slot_override @@ -37,6 +37,7 @@ class CanonicalProjectionReference(AdCPBaseModel): Field( description="Where the rendered asset bytes come from on the projected v2 declaration. Default (when omitted) is `buyer_uploaded` — the canonical's default. Set explicitly when the v1 named format doesn't follow that default. Required for generative entries (`agent_synthesized` or `seller_pre_rendered_from_brief`) because their asset shape doesn't carry image/video/audio bytes, and for published-post reference entries (`publisher_owned_reference`) because their asset shape carries a post reference rather than uploaded bytes. Projection without this hint produces a lossy v2 declaration that claims buyer-uploaded bytes." ), + NoExplicitNull, ] = None slots_override: Annotated[ list[canonical_projection_slot_override.CanonicalProjectionSlotOverride] | None, @@ -44,4 +45,5 @@ class CanonicalProjectionReference(AdCPBaseModel): description="When the v1 named format's slot shape differs from the canonical's default slots, this carries the override that the projected v2 declaration's `params.slots[]` should use. REPLACES (does not merge with) the canonical's default slots — projection-time semantics. The slot vocabulary follows `asset-group-vocabulary.json`. Asset IDs in the v1 format's `assets[*]` MUST resolve (directly or via the vocabulary's aliases) to the `asset_group_id` values declared here.", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/canonical_projection_slot_override.py b/src/adcp/types/domains/core/canonical_projection_slot_override.py index 25df5ee65..c8f797fc9 100644 --- a/src/adcp/types/domains/core/canonical_projection_slot_override.py +++ b/src/adcp/types/domains/core/canonical_projection_slot_override.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_projection_slot_override.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -29,13 +29,17 @@ class CanonicalProjectionSlotOverride(AdCPBaseModel): required: Annotated[ StrictBool | None, Field(description='Whether the slot is required in the projected declaration.'), + NoExplicitNull, ] = False max_chars: Annotated[ - SchemaInt | None, Field(description='Max character count for text slots.', ge=1) + SchemaInt | None, + Field(description='Max character count for text slots.', ge=1), + NoExplicitNull, ] = None consumed_for_production: Annotated[ StrictBool | None, Field( description="When false, slot is for moderation/review only and is NOT consumed by the seller's renderer (e.g., a brand-safety brief that informs review but doesn't appear in the rendered ad)." ), + NoExplicitNull, ] = True diff --git a/src/adcp/types/domains/core/canonical_proposal.py b/src/adcp/types/domains/core/canonical_proposal.py index 8e085820c..198266239 100644 --- a/src/adcp/types/domains/core/canonical_proposal.py +++ b/src/adcp/types/domains/core/canonical_proposal.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_proposal.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import proposal_status as proposal_status_1 @@ -26,9 +26,9 @@ class TotalBudgetGuidance(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[StrictFloat | None, Field(ge=0.0)] = None - recommended: Annotated[StrictFloat | None, Field(ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(ge=0.0)] = None + min: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + recommended: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + max: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] @@ -45,8 +45,9 @@ class CanonicalProposal(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None - media_buy_id: Annotated[str | None, Field(min_length=1)] = None + media_buy_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None opportunity_id: Annotated[ str | None, Field( @@ -55,24 +56,26 @@ class CanonicalProposal(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None - base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1)] = None + base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None proposal_status: Annotated[ proposal_status_1.ProposalStatus, Field( description='draft is indicative and unreserved; committed has firm terms with inventory reserved until expires_at; accepted is the historical snapshot attached to a MediaBuy.' ), ] - accepted_at: AwareDatetime | None = None + accepted_at: Annotated[AwareDatetime | None, NoExplicitNull] = None expires_at: Annotated[ AwareDatetime | None, Field( description='For a draft, the indicative-terms freshness deadline. For a committed proposal, the inventory-hold deadline.' ), + NoExplicitNull, ] = None name: Annotated[str, Field(max_length=500, min_length=1)] - description: Annotated[str | None, Field(max_length=2000)] = None - brief_alignment: Annotated[str | None, Field(max_length=2000)] = None + description: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + brief_alignment: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None commercial_terms: commercial_terms_1.CommercialTerms terms_digest: Annotated[ str, @@ -81,16 +84,18 @@ class CanonicalProposal(AdCPBaseModel): pattern='^sha256:[A-Za-z0-9_-]{43}$', ), ] - insertion_order: insertion_order_1.InsertionOrder | None = None + insertion_order: Annotated[insertion_order_1.InsertionOrder | None, NoExplicitNull] = None total_budget_guidance: Annotated[ TotalBudgetGuidance | None, Field( description="Optional budget guidance for this proposal — the planning answer to criteria.outcome_target and to open-budget briefs. commercial_terms_1.total_budget remains the concrete figure the plan is priced at; this band expresses the seller's recommended range around it. When criteria.outcome_target carries cost_per, the cost answer is commercial_terms_1.bidding.cost_per and this band's currency equals cost_per.currency." ), + NoExplicitNull, ] = None forecast: Annotated[ canonical_delivery_forecast.CanonicalDeliveryForecast | None, Field( description="Aggregate forecasted delivery for the proposal. For outcome_target requests, points carry the goal's metric or event key in metrics; with cost_per, that is the goal volume planned under the commercial_terms_1.bidding policy, and currency equals cost_per.currency." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/canonical_reporting_capabilities.py b/src/adcp/types/domains/core/canonical_reporting_capabilities.py index 5130f4082..1480e782c 100644 --- a/src/adcp/types/domains/core/canonical_reporting_capabilities.py +++ b/src/adcp/types/domains/core/canonical_reporting_capabilities.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canonical_reporting_capabilities.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ..enums import available_metric, reporting_frequency @@ -55,29 +55,36 @@ class CanonicalReportingCapabilities(AdCPBaseModel): Field( description='Product-scoped subset of get_adcp_capabilities.media_buy.reporting_delivery.offerings[].offering_id that packages using this product can satisfy. This binds seller-wide managed-delivery offerings to product/package eligibility. An empty array explicitly declares no managed offering; absence means product-level applicability is unknown and MUST NOT be inferred from the seller-wide list. Account, seat, credential, or provider constraints may narrow support further during sync_accounts validation.' ), + NoExplicitNull, ] = None available_metrics: list[available_metric.AvailableMetric] - vendor_metrics: list[VendorMetric] | None = None - supports_creative_breakdown: StrictBool | None = None - supports_format_breakdown: StrictBool | None = None - supports_keyword_breakdown: StrictBool | None = None - supports_geo_breakdown: geo_breakdown_support.GeographicBreakdownSupport | None = None - supports_device_type_breakdown: StrictBool | None = None - supports_device_platform_breakdown: StrictBool | None = None - supports_audience_breakdown: StrictBool | None = None - supports_demographic_breakdown: ( - demographic_reporting_capability.DemographicReportingCapability | None - ) = None - supports_placement_breakdown: StrictBool | None = None - supports_property_breakdown: StrictBool | None = None - supports_collection_breakdown: StrictBool | None = None - supports_installment_breakdown: StrictBool | None = None - supports_collection_property_breakdown: StrictBool | None = None - supports_installment_property_breakdown: StrictBool | None = None - supports_placement_property_breakdown: StrictBool | None = None - supports_spot_breakdown: spot_reporting_capability.SpotReportingCapability | None = None + vendor_metrics: Annotated[list[VendorMetric] | None, NoExplicitNull] = None + supports_creative_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_format_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_keyword_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_geo_breakdown: Annotated[ + geo_breakdown_support.GeographicBreakdownSupport | None, NoExplicitNull + ] = None + supports_device_type_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_device_platform_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_audience_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_demographic_breakdown: Annotated[ + demographic_reporting_capability.DemographicReportingCapability | None, NoExplicitNull + ] = None + supports_placement_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_property_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_collection_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_installment_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_collection_property_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_installment_property_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_placement_property_breakdown: Annotated[StrictBool | None, NoExplicitNull] = None + supports_spot_breakdown: Annotated[ + spot_reporting_capability.SpotReportingCapability | None, NoExplicitNull + ] = None date_range_support: DateRangeSupport - windowed_pull_granularities: list[reporting_frequency.ReportingFrequency] | None = None + windowed_pull_granularities: Annotated[ + list[reporting_frequency.ReportingFrequency] | None, NoExplicitNull + ] = None measurement_windows: Annotated[ - list[measurement_window.MeasurementWindow] | None, Field(min_length=1) + list[measurement_window.MeasurementWindow] | None, Field(min_length=1), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/core/canonical_reporting_commitment.py b/src/adcp/types/domains/core/canonical_reporting_commitment.py index e7d121d35..04e0cb474 100644 --- a/src/adcp/types/domains/core/canonical_reporting_commitment.py +++ b/src/adcp/types/domains/core/canonical_reporting_commitment.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/canonical_reporting_commitment.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import available_metric @@ -19,8 +19,10 @@ class CanonicalReportingCommitment1(AdCPBaseModel): ) scope: Literal['standard'] = 'standard' metric_id: available_metric.AvailableMetric - qualifier: canonical_metric_qualifier.CanonicalMetricQualifier | None = None - effective_at: AwareDatetime | None = None + qualifier: Annotated[ + canonical_metric_qualifier.CanonicalMetricQualifier | None, NoExplicitNull + ] = None + effective_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class CanonicalReportingCommitment2(AdCPBaseModel): @@ -30,8 +32,10 @@ class CanonicalReportingCommitment2(AdCPBaseModel): scope: Literal['vendor'] = 'vendor' vendor: brand_key.BrandKey metric_id: vendor_metric_id.VendorMetricId - qualifier: canonical_metric_qualifier.CanonicalMetricQualifier | None = None - effective_at: AwareDatetime | None = None + qualifier: Annotated[ + canonical_metric_qualifier.CanonicalMetricQualifier | None, NoExplicitNull + ] = None + effective_at: Annotated[AwareDatetime | None, NoExplicitNull] = None CanonicalReportingCommitment = Annotated[ diff --git a/src/adcp/types/domains/core/canvas_constraint.py b/src/adcp/types/domains/core/canvas_constraint.py index 27dd15603..9183a1f1d 100644 --- a/src/adcp/types/domains/core/canvas_constraint.py +++ b/src/adcp/types/domains/core/canvas_constraint.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/canvas_constraint.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -31,7 +31,7 @@ class Region(AdCPBaseModel): y: Annotated[StrictFloat, Field(ge=0.0)] width: Annotated[StrictFloat, Field(gt=0.0)] height: Annotated[StrictFloat, Field(gt=0.0)] - unit: Unit | None = Unit.px + unit: Annotated[Unit | None, NoExplicitNull] = Unit.px class CanvasConstraint(AdCPBaseModel): @@ -39,6 +39,6 @@ class CanvasConstraint(AdCPBaseModel): extra='allow', ) constraint: Constraint - state_id: Annotated[str | None, Field(pattern='^[a-z][a-z0-9_]*$')] = None - breakpoint_id: Annotated[str | None, Field(pattern='^[a-z][a-z0-9_]*$')] = None + state_id: Annotated[str | None, Field(pattern='^[a-z][a-z0-9_]*$'), NoExplicitNull] = None + breakpoint_id: Annotated[str | None, Field(pattern='^[a-z][a-z0-9_]*$'), NoExplicitNull] = None region: Region diff --git a/src/adcp/types/domains/core/capabilities_changed_webhook.py b/src/adcp/types/domains/core/capabilities_changed_webhook.py index 7bc784bb7..fc16c9071 100644 --- a/src/adcp/types/domains/core/capabilities_changed_webhook.py +++ b/src/adcp/types/domains/core/capabilities_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/capabilities_changed_webhook.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -104,5 +104,6 @@ class CapabilitiesChangedWebhook(AdCPBaseModel): description='Optional advisory JSON Pointer-style paths that identify the top-level or nested capability subtrees that changed (for example `/account/sandbox` or `/supported_protocols`). Receivers MAY use this for logging or selective downstream invalidation, but MUST still treat the full `get_adcp_capabilities` response as the authoritative replacement snapshot.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/catalog.py b/src/adcp/types/domains/core/catalog.py index 9cd49ead5..9b6d401d7 100644 --- a/src/adcp/types/domains/core/catalog.py +++ b/src/adcp/types/domains/core/catalog.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/catalog.json -# timestamp: 2026-10-04T00:56:30+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field @@ -32,12 +32,14 @@ class Catalog(AdCPBaseModel): Field( description="Buyer's identifier for this catalog. Required when syncing via sync_catalogs. When used in creatives, references a previously synced catalog on the account." ), + NoExplicitNull, ] = None name: Annotated[ str | None, Field( description="Human-readable name for this catalog (e.g., 'Summer Products 2025', 'Amsterdam Store Locations')." ), + NoExplicitNull, ] = None type: Annotated[ catalog_type.CatalogType, @@ -50,18 +52,21 @@ class Catalog(AdCPBaseModel): Field( description="URL to an external catalog feed. The platform fetches and resolves items from this URL. For offering-type catalogs, the feed contains an array of Offering objects. For other types, the feed format is determined by feed_format. When omitted with type 'product', the platform uses its synced copy of the brand's product catalog." ), + NoExplicitNull, ] = None feed_format: Annotated[ feed_format_1.FeedFormat | None, Field( description='Format of the external feed at url. Required when url points to a non-AdCP feed (e.g., Google Merchant Center XML, Meta Product Catalog). Omit for offering-type catalogs where the feed is native AdCP JSON.' ), + NoExplicitNull, ] = None update_frequency: Annotated[ update_frequency_1.UpdateFrequency | None, Field( description='How often the platform should re-fetch the feed from url. Only applicable when url is provided. Platforms may use this as a hint for polling schedules.' ), + NoExplicitNull, ] = None items: Annotated[ list[dict[str, Any]] | None, @@ -69,6 +74,7 @@ class Catalog(AdCPBaseModel): description="Inline catalog data. The item schema depends on the catalog type: Offering objects for 'offering', StoreItem for 'store', HotelItem for 'hotel', FlightItem for 'flight', JobItem for 'job', VehicleItem for 'vehicle', RealEstateItem for 'real_estate', EducationItem for 'education', DestinationItem for 'destination', AppItem for 'app', or freeform objects for 'product', 'inventory', and 'promotion'. Mutually exclusive with url — provide one or the other, not both. Implementations should validate items against the type-specific schema.", min_length=1, ), + NoExplicitNull, ] = None ids: Annotated[ list[str] | None, @@ -76,6 +82,7 @@ class Catalog(AdCPBaseModel): description='Filter catalog to exact canonical item keys. The key field is offering_id for offering, store_id for store, hotel_id for hotel, flight_id for flight, job_id for job, vehicle_id for vehicle, listing_id for real_estate, program_id for education, destination_id for destination, and app_id for app. Product, inventory, and promotion catalogs use the stable normalized source identifier retained during ingestion (for example a retailer SKU). The same canonical key is used by catalog availability item_id.', min_length=1, ), + NoExplicitNull, ] = None gtins: Annotated[ list[Gtin] | None, @@ -83,6 +90,7 @@ class Catalog(AdCPBaseModel): description="Filter product-type catalogs by GTIN identifiers for cross-retailer catalog matching. Accepts standard GTIN formats (GTIN-8, UPC-A/GTIN-12, EAN-13/GTIN-13, GTIN-14). Only applicable when type is 'product'.", min_length=1, ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -90,18 +98,21 @@ class Catalog(AdCPBaseModel): description='Filter catalog to items with these tags. Tags are matched using OR logic — items matching any tag are included.', min_length=1, ), + NoExplicitNull, ] = None category: Annotated[ str | None, Field( description="Filter catalog to items in this category (e.g., 'beverages/soft-drinks', 'chef-positions')." ), + NoExplicitNull, ] = None query: Annotated[ str | None, Field( description="Natural language filter for catalog items (e.g., 'all pasta sauces under $5', 'amsterdam vacancies')." ), + NoExplicitNull, ] = None conversion_events: Annotated[ list[event_type.EventType] | None, @@ -109,12 +120,14 @@ class Catalog(AdCPBaseModel): description="Event types that represent conversions for items in this catalog. Declares what events the platform should attribute to catalog items — e.g., a job catalog converts via submit_application, a product catalog via purchase. The event's content_ids field carries the item IDs that connect back to catalog items. Use content_id_type to declare what identifier type content_ids values represent.", min_length=1, ), + NoExplicitNull, ] = None content_id_type: Annotated[ content_id_type_1.ContentIdType | None, Field( description="Identifier type that the event's content_ids field should be matched against for items in this catalog. For example, 'gtin' means content_ids values are Global Trade Item Numbers, 'sku' means retailer SKUs. Omit when using a custom identifier scheme not listed in the enum." ), + NoExplicitNull, ] = None feed_field_mappings: Annotated[ list[catalog_field_mapping.CatalogFieldMapping] | None, @@ -122,4 +135,5 @@ class Catalog(AdCPBaseModel): description='Declarative normalization rules for external feeds. Maps non-standard feed field names, date formats, price encodings, and image URLs to the AdCP catalog item schema. Applied during sync_catalogs ingestion. Supports field renames, named transforms (date, divide, boolean, split), static literal injection, and assignment of image URLs to typed asset pools.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/catalog_field_mapping.py b/src/adcp/types/domains/core/catalog_field_mapping.py index e02382404..cac824da7 100644 --- a/src/adcp/types/domains/core/catalog_field_mapping.py +++ b/src/adcp/types/domains/core/catalog_field_mapping.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/catalog_field_mapping.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -29,18 +29,21 @@ class CatalogFieldMapping(AdCPBaseModel): Field( description='Field name in the external feed record. Omit when injecting a static literal value (use the value property instead).' ), + NoExplicitNull, ] = None catalog_field: Annotated[ str | None, Field( description="Target field on the catalog item schema, using dot notation for nested fields (e.g., 'name', 'price.amount', 'location.city'). Mutually exclusive with asset_group_id." ), + NoExplicitNull, ] = None asset_group_id: Annotated[ str | None, Field( description="Places the feed field value (a URL) into a typed asset pool on the catalog item's assets array. The value is wrapped as an image or video asset in a group with this ID. Use standard group IDs: 'images_landscape', 'images_vertical', 'images_square', 'logo', 'video'. Mutually exclusive with catalog_field." ), + NoExplicitNull, ] = None value: Annotated[ Any | None, @@ -53,18 +56,21 @@ class CatalogFieldMapping(AdCPBaseModel): Field( description='Named transform to apply to the feed field value before writing to the catalog schema. See transform-specific parameters (format, timezone, by, separator).' ), + NoExplicitNull, ] = None format: Annotated[ str | None, Field( description="For transform 'date': the input date format string (e.g., 'YYYYMMDD', 'MM/DD/YYYY', 'DD-MM-YYYY'). Output is always ISO 8601 (e.g., '2025-03-01'). Uses Unicode date pattern tokens." ), + NoExplicitNull, ] = None timezone: Annotated[ str | None, Field( description="For transform 'date': the timezone of the input value. IANA timezone identifier (e.g., 'UTC', 'America/New_York', 'Europe/Amsterdam'). Defaults to UTC when omitted." ), + NoExplicitNull, ] = None by: Annotated[ StrictFloat | None, @@ -72,12 +78,14 @@ class CatalogFieldMapping(AdCPBaseModel): description="For transform 'divide': the divisor to apply (e.g., 100 to convert integer cents to decimal dollars).", gt=0.0, ), + NoExplicitNull, ] = None separator: Annotated[ str | None, Field( description="For transform 'split': the separator character or string to split on. Defaults to ','." ), + NoExplicitNull, ] = ',' default: Annotated[ Any | None, @@ -85,4 +93,4 @@ class CatalogFieldMapping(AdCPBaseModel): description='Fallback value to use when feed_field is absent, null, or empty. Applied after any transform would have been applied. Allows optional feed fields to have a guaranteed baseline value.' ), ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/catalog_item_availability_state.py b/src/adcp/types/domains/core/catalog_item_availability_state.py index 48a4ec100..52e3d05b7 100644 --- a/src/adcp/types/domains/core/catalog_item_availability_state.py +++ b/src/adcp/types/domains/core/catalog_item_availability_state.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/catalog_item_availability_state.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import catalog_item_availability_error @@ -49,6 +49,7 @@ class CatalogItemAvailabilityState(AdCPBaseModel): Field( description='Current buyer-authored overlay state. active does not imply seller approval or delivery eligibility.' ), + NoExplicitNull, ] = None overlay_revision: Annotated[ SchemaInt | None, @@ -56,19 +57,23 @@ class CatalogItemAvailabilityState(AdCPBaseModel): description='Current optimistic-concurrency token. Revision 0 is the initial active state. Every applied suppress, applied restore, and automatic expiry increments it exactly once; unchanged updates, reads, and idempotent replays do not increment it.', ge=0, ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Current automatic expiry, present only while availability is suppressed with an expiry.' ), + NoExplicitNull, ] = None updated_at: Annotated[ AwareDatetime | None, Field(description='Seller timestamp of the state represented by overlay_revision.'), + NoExplicitNull, ] = None errors: Annotated[ list[catalog_item_availability_error.CatalogItemAvailabilityError] | None, Field(min_length=1), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/catalog_item_availability_update.py b/src/adcp/types/domains/core/catalog_item_availability_update.py index 0e4d5164f..afa7f41f1 100644 --- a/src/adcp/types/domains/core/catalog_item_availability_update.py +++ b/src/adcp/types/domains/core/catalog_item_availability_update.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/catalog_item_availability_update.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import ext as ext_1 @@ -85,11 +85,13 @@ class CatalogItemAvailabilityUpdate(AdCPBaseModel): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Optional expiry for a suppress overlay. At this instant the seller automatically removes the buyer-authored suppression as if it received restore. Valid only with action suppress; omit for an indefinite suppression. Sellers MUST reject a timestamp that is not in the future when the request is processed. On a repeated suppress, this value is complete replacement state: a changed timestamp replaces the prior expiry, and omission clears a prior expiry to make suppression indefinite. Either change returns status applied, not unchanged.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/catalog_item_availability_update_result.py b/src/adcp/types/domains/core/catalog_item_availability_update_result.py index 675646cfc..38a5821dc 100644 --- a/src/adcp/types/domains/core/catalog_item_availability_update_result.py +++ b/src/adcp/types/domains/core/catalog_item_availability_update_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/catalog_item_availability_update_result.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import catalog_item_availability_error @@ -56,6 +56,7 @@ class CatalogItemAvailabilityUpdateResult(AdCPBaseModel): availability: Annotated[ Availability | None, Field(description='Persisted buyer-authored state after an applied or unchanged result.'), + NoExplicitNull, ] = None overlay_revision: Annotated[ SchemaInt | None, @@ -63,23 +64,27 @@ class CatalogItemAvailabilityUpdateResult(AdCPBaseModel): description="Persisted state revision after an applied or unchanged result. Applied increments the request's expected_overlay_revision exactly once; unchanged preserves it.", ge=0, ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Persisted expiry after this update, present only for a suppressed state with an expiry.' ), + NoExplicitNull, ] = None applied_at: Annotated[ AwareDatetime | None, Field( description='Seller timestamp when the buyer-availability state took effect. Required for applied. Optional for unchanged when the seller knows the timestamp of the already-persisted state.' ), + NoExplicitNull, ] = None errors: Annotated[ list[catalog_item_availability_error.CatalogItemAvailabilityError] | None, Field( description='Why this item update failed. Required when status is failed.', min_length=1 ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/catalog_item_delivery_metrics.py b/src/adcp/types/domains/core/catalog_item_delivery_metrics.py index d8ce20ece..fff368f08 100644 --- a/src/adcp/types/domains/core/catalog_item_delivery_metrics.py +++ b/src/adcp/types/domains/core/catalog_item_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/catalog_item_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from ..enums import content_id_type as content_id_type_1 @@ -19,6 +20,7 @@ class CatalogItemDeliveryMetrics(DeliveryMetrics): content_id_type: Annotated[ content_id_type_1.ContentIdType | None, Field(description='Identifier type for this content_id'), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/catalog_selection.py b/src/adcp/types/domains/core/catalog_selection.py index 3db938d81..43bf583e2 100644 --- a/src/adcp/types/domains/core/catalog_selection.py +++ b/src/adcp/types/domains/core/catalog_selection.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/catalog_selection.json -# timestamp: 2026-10-04T00:56:30+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -33,6 +33,7 @@ class CatalogSelection(AdCPBaseModel): Field( description="Catalog type. Structural types: 'offering' (AdCP Offering objects), 'product' (ecommerce entries), 'inventory' (stock per location), 'store' (physical locations), 'promotion' (deals and pricing). Vertical types: 'hotel', 'flight', 'job', 'vehicle', 'real_estate', 'education', 'destination', 'app' — each with an industry-specific item schema." ), + NoExplicitNull, ] = None ids: Annotated[ list[str] | None, @@ -40,6 +41,7 @@ class CatalogSelection(AdCPBaseModel): description='Filter catalog to exact canonical item keys. The key field is offering_id for offering, store_id for store, hotel_id for hotel, flight_id for flight, job_id for job, vehicle_id for vehicle, listing_id for real_estate, program_id for education, destination_id for destination, and app_id for app. Product, inventory, and promotion catalogs use the stable normalized source identifier retained during ingestion (for example a retailer SKU). The same canonical key is used by catalog availability item_id.', min_length=1, ), + NoExplicitNull, ] = None gtins: Annotated[ list[Gtin] | None, @@ -47,6 +49,7 @@ class CatalogSelection(AdCPBaseModel): description="Filter product-type catalogs by GTIN identifiers for cross-retailer catalog matching. Accepts standard GTIN formats (GTIN-8, UPC-A/GTIN-12, EAN-13/GTIN-13, GTIN-14). Only applicable when type is 'product'.", min_length=1, ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -54,12 +57,14 @@ class CatalogSelection(AdCPBaseModel): description='Filter catalog to items with these tags. Tags are matched using OR logic — items matching any tag are included.', min_length=1, ), + NoExplicitNull, ] = None category: Annotated[ str | None, Field( description="Filter catalog to items in this category (e.g., 'beverages/soft-drinks', 'chef-positions')." ), + NoExplicitNull, ] = None query: Annotated[ str | None, @@ -67,4 +72,5 @@ class CatalogSelection(AdCPBaseModel): description="Natural language filter for catalog items (e.g., 'all pasta sauces under $5', 'amsterdam vacancies').", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/catchment.py b/src/adcp/types/domains/core/catchment.py index 533729c7b..8dcf33199 100644 --- a/src/adcp/types/domains/core/catchment.py +++ b/src/adcp/types/domains/core/catchment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/catchment.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat, model_validator from ..enums import distance_unit @@ -68,32 +68,37 @@ class Catchment(AdCPBaseModel): Field( description="Human-readable label for this catchment (e.g., '15-min drive', '1km walking radius')." ), + NoExplicitNull, ] = None travel_time: Annotated[ TravelTime | None, Field( description='Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks, accounting for road connectivity, transit schedules, and terrain.' ), + NoExplicitNull, ] = None transport_mode: Annotated[ transport_mode_1.TransportMode | None, Field( description='Transportation mode for isochrone calculation. Required when travel_time is provided.' ), + NoExplicitNull, ] = None radius: Annotated[ Radius | None, Field( description="Simple radius from the store location. The platform draws a circle of this distance around the store's coordinates." ), + NoExplicitNull, ] = None geometry: Annotated[ Geometry | None, Field( description='Pre-computed GeoJSON geometry defining the catchment boundary. Use this when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom trade area boundaries. Supports Polygon and MultiPolygon types.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> Catchment: diff --git a/src/adcp/types/domains/core/collection.py b/src/adcp/types/domains/core/collection.py index 093ddae7a..0e736a853 100644 --- a/src/adcp/types/domains/core/collection.py +++ b/src/adcp/types/domains/core/collection.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import collection_cadence, collection_kind, collection_relationship, collection_status @@ -44,6 +44,7 @@ class Collection(AdCPBaseModel): description="Publisher namespace that owns this collection. Required for supply-path verification when the declaration is read from a cross-origin authoritative document; it must match the publisher whose origin delegated retrieval. A pointer alone cannot claim another publisher's collection. May be omitted for a publisher-origin document, where that origin supplies the namespace.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None collection_id: Annotated[ str, @@ -57,83 +58,101 @@ class Collection(AdCPBaseModel): Field( description="What kind of content program this is. Helps agents interpret installments correctly. A channel collection represents the persistent programmed stream; its installments, when present, are scheduled airings or programming blocks. Defaults to 'series' when absent." ), + NoExplicitNull, + ] = None + description: Annotated[ + str | None, Field(description='What the collection is about'), NoExplicitNull ] = None - description: Annotated[str | None, Field(description='What the collection is about')] = None genre: Annotated[ list[str] | None, Field( description='Genre tags. When genre_taxonomy is present, values are taxonomy IDs (e.g., IAB Content Taxonomy 3.0 codes). Otherwise free-form.' ), + NoExplicitNull, ] = None genre_taxonomy: Annotated[ str | None, Field( description="Taxonomy system for genre values (e.g., 'iab_content_3.0'). When present, genre values should be valid taxonomy IDs. Recommended for machine-readable brand safety evaluation." ), + NoExplicitNull, ] = None language: Annotated[ - str | None, Field(description="Primary language (BCP 47 tag, e.g., 'en', 'es-MX')") + str | None, + Field(description="Primary language (BCP 47 tag, e.g., 'en', 'es-MX')"), + NoExplicitNull, ] = None content_rating: Annotated[ content_rating_1.ContentRating | None, Field( description='Baseline content rating for the collection. Individual installments may override this.' ), + NoExplicitNull, ] = None cadence: Annotated[ collection_cadence.CollectionCadence | None, Field(description='How frequently the collection releases new installments'), + NoExplicitNull, ] = None season: Annotated[ str | None, Field( description="Current or most recent season identifier (e.g., '3', '2026', 'spring_2026'). A lightweight label — not a full season object." ), + NoExplicitNull, ] = None status: Annotated[ collection_status.CollectionStatus | None, Field(description='Lifecycle status of the collection'), + NoExplicitNull, ] = None production_quality: Annotated[ production_quality_1.ProductionQuality | None, Field( description='Production quality tier. Seller-declared. Maps to OpenRTB content.prodq (professional=1, prosumer=2, ugc=3).' ), + NoExplicitNull, ] = None talent: Annotated[ list[talent_1.Talent] | None, Field( description='Hosts, recurring cast, creators associated with the collection. Each talent entry may include a brand_url linking to their brand.json identity.' ), + NoExplicitNull, ] = None special: Annotated[ special_1.Special | None, Field( description='When present, this collection is a special — content anchored to a real-world event or occasion. Individual installments may override with their own event context.' ), + NoExplicitNull, ] = None limited_series: Annotated[ limited_series_1.LimitedSeries | None, Field( description='When present, this collection is a limited series — a bounded run with a defined arc, installment count, and end date.' ), + NoExplicitNull, ] = None distribution: Annotated[ list[collection_distribution.CollectionDistribution] | None, Field( description="Where this collection is distributed. Each entry maps the collection to a publisher platform using host property IDs, identifiers, or both. For channel collections this is the carriage map. It is a publisher assertion for discovery, not sales authorization; buyers validate owner-sold carriage against the host publisher's collection-scoped authorized_agents declaration. Collections SHOULD include at least one platform-independent identifier (imdb_id, gracenote_id, eidr_id) when available." ), + NoExplicitNull, ] = None deadline_policy: Annotated[ deadline_policy_1.DeadlinePolicy | None, Field( description="Default deadline rules for installments of this collection. Agents compute absolute deadlines from each installment's scheduled_at and these lead times. Installments with explicit deadlines override this policy. Only meaningful when installments carry scheduled_at; a continuously programmed channel collection without enumerated installments has no deadlines to derive." ), + NoExplicitNull, ] = None related_collections: Annotated[ list[RelatedCollection] | None, Field( description="Relationships to other collections (spin-offs, companion collections, etc.). Each entry references another collection by collection_id within the same publisher's adagents.json." ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/collection_delivery_metrics.py b/src/adcp/types/domains/core/collection_delivery_metrics.py index b18260eec..028ec8c01 100644 --- a/src/adcp/types/domains/core/collection_delivery_metrics.py +++ b/src/adcp/types/domains/core/collection_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from . import collection_ref as collection_ref_1 @@ -19,6 +20,7 @@ class CollectionDeliveryMetrics(DeliveryMetrics): Field( description='Current human-readable collection name. Convenience metadata only; collection_ref is stable identity.' ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/collection_distribution.py b/src/adcp/types/domains/core/collection_distribution.py index b89aa5e22..08511d4a7 100644 --- a/src/adcp/types/domains/core/collection_distribution.py +++ b/src/adcp/types/domains/core/collection_distribution.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_distribution.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..enums import distribution_identifier_type @@ -43,6 +43,7 @@ class CollectionDistribution(AdCPBaseModel): description="Publisher-scoped property IDs from publisher_domain's adagents.json that carry this collection. For owner-sold channel inventory, these identify the host apps without requiring the host to publish channel-owner placement definitions.", min_length=1, ), + NoExplicitNull, ] = None identifiers: Annotated[ list[Identifier] | None, @@ -50,6 +51,7 @@ class CollectionDistribution(AdCPBaseModel): description='Identifiers for the collection on this publisher, including publisher-scoped channel/EPG identifiers and platform-independent metadata identifiers', min_length=1, ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/collection_list_ref.py b/src/adcp/types/domains/core/collection_list_ref.py index 622bb3130..876e2220c 100644 --- a/src/adcp/types/domains/core/collection_list_ref.py +++ b/src/adcp/types/domains/core/collection_list_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_list_ref.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -23,4 +23,5 @@ class CollectionListReference(AdCPBaseModel): Field( description='JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/collection_property_delivery_metrics.py b/src/adcp/types/domains/core/collection_property_delivery_metrics.py index ba0a0ff65..5ed6e4294 100644 --- a/src/adcp/types/domains/core/collection_property_delivery_metrics.py +++ b/src/adcp/types/domains/core/collection_property_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_property_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from . import collection_ref as collection_ref_1 @@ -19,6 +20,7 @@ class CollectionPropertyDeliveryMetrics(DeliveryMetrics): collection_name: Annotated[ str | None, Field(description='Current human-readable collection name. Convenience metadata only.'), + NoExplicitNull, ] = None publisher_domain: Annotated[ str, @@ -36,10 +38,12 @@ class CollectionPropertyDeliveryMetrics(DeliveryMetrics): Field( description="Canonical publisher-scoped catalog identity when available. Its publisher_domain MUST equal the row's publisher_domain." ), + NoExplicitNull, ] = None property_name: Annotated[ str | None, Field(description='Current human-readable property name. Convenience metadata only.'), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/collection_selection.py b/src/adcp/types/domains/core/collection_selection.py index 334f3ed50..01388a560 100644 --- a/src/adcp/types/domains/core/collection_selection.py +++ b/src/adcp/types/domains/core/collection_selection.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_selection.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import collection_selector @@ -25,7 +25,7 @@ class CollectionSelection1(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CollectionSelection2(AdCPBaseModel): @@ -33,7 +33,7 @@ class CollectionSelection2(AdCPBaseModel): extra='forbid', ) mode: Literal['default'] = 'default' - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None CollectionSelection = Annotated[ diff --git a/src/adcp/types/domains/core/collection_selector.py b/src/adcp/types/domains/core/collection_selector.py index 08e0e9415..6a64c0473 100644 --- a/src/adcp/types/domains/core/collection_selector.py +++ b/src/adcp/types/domains/core/collection_selector.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/collection_selector.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -27,4 +27,5 @@ class CollectionSelector(AdCPBaseModel): description='Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file. Omit to reference all collections declared in that file.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/committed_metric.py b/src/adcp/types/domains/core/committed_metric.py index edb267c8c..dfea203f0 100644 --- a/src/adcp/types/domains/core/committed_metric.py +++ b/src/adcp/types/domains/core/committed_metric.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/committed_metric.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import attribution_methodology as attribution_methodology_1 @@ -24,24 +24,29 @@ class Qualifier(AdCPBaseModel): viewability_standard: Annotated[ viewability_standard_1.ViewabilityStandard | None, Field(description='Viewability standard for this vendor metric, as a closed enum.'), + NoExplicitNull, ] = None completion_source: Annotated[ completion_source_1.CompletionSource | None, Field(description='Completion attestation source for this vendor metric.'), + NoExplicitNull, ] = None attribution_methodology: Annotated[ attribution_methodology_1.AttributionMethodology | None, Field(description='Attribution methodology for this vendor outcome metric.'), + NoExplicitNull, ] = None attribution_window: Annotated[ duration.Duration | None, Field( description='Attribution window for this vendor outcome metric, as a structured duration.' ), + NoExplicitNull, ] = None lift_dimension: Annotated[ lift_dimension_1.LiftDimension | None, Field(description='Brand-lift dimension for this vendor metric.'), + NoExplicitNull, ] = None @@ -54,30 +59,35 @@ class QualifierModel(AdCPBaseModel): Field( description='Viewability standard the seller commits to for this metric. MUST be set when `metric_id` ∈ {`viewable_impressions`, `viewable_rate`, `measurable_impressions`, `viewed_seconds`, `viewed_seconds_percentiles`, `viewed_seconds_histogram`} and the seller commits to a specific standard; absence means the contract leaves the standard unspecified and reconciliation falls back to whatever standard the delivery report carries on its `viewability.standard` field. A distribution whose standard remains unspecified MUST NOT be combined with any other row, including another row with an unspecified standard.' ), + NoExplicitNull, ] = None completion_source: Annotated[ completion_source_1.CompletionSource | None, Field( description="Source of `completion_rate` attestation. MUST be set when `metric_id` is `completion_rate` and the seller commits to a specific source — `seller_attested` when the player/ad server's own completion event is the contract, `vendor_attested` when a third-party measurement vendor (anchored on the matching `performance_standard.vendor` BrandRef) is the contract. Absence means the contract leaves the source unspecified and reconciliation falls back to whatever the delivery report happens to carry." ), + NoExplicitNull, ] = None attribution_methodology: Annotated[ attribution_methodology_1.AttributionMethodology | None, Field( description='How attribution between ad exposure and outcome events was computed. SHOULD be set when `metric_id` is an outcome metric (`conversions`, `conversion_value`, `roas`, `cost_per_acquisition`, `incremental_sales_lift`, `brand_lift`, `foot_traffic`, `conversion_lift`, `brand_search_lift`, `units_sold`, `new_to_brand_rate`, `new_to_brand_units`, `leads`) and the seller commits to a specific methodology. `deterministic_purchase` is the retail-media default; `modeled` covers MMM and clean-room outputs; `probabilistic` and `panel_based` cover their respective methodologies. Two outcome rows with the same `metric_id` and different `attribution_methodology` are not interchangeable and must not be summed.' ), + NoExplicitNull, ] = None attribution_window: Annotated[ duration.Duration | None, Field( description="Time window over which outcome attribution is computed. **Object-valued, not string** — MUST be a structured duration object like `{interval: 14, unit: 'days'}`, NEVER a shorthand string like `'14d'`. SHOULD be set when `metric_id` is an outcome metric and the seller commits to a specific window. Common windows: `{interval: 7, unit: 'days'}`, `{interval: 14, unit: 'days'}`, `{interval: 30, unit: 'days'}`, `{interval: 90, unit: 'days'}`. Two outcome rows with the same `metric_id` and `attribution_methodology` but different `attribution_window` represent the same metric measured over different periods — the join on `(metric_id, qualifier)` keeps them as separate rows so buyers don't accidentally aggregate across windows." ), + NoExplicitNull, ] = None lift_dimension: Annotated[ lift_dimension_1.LiftDimension | None, Field( description='Which dimension of `brand_lift` this row represents — awareness, consideration, favorability, purchase intent, or ad recall. MUST be set when `metric_id` is `brand_lift` and the seller commits to (or is reporting) a specific dimension. Brand-lift vendors (Kantar, Upwave, Cint, DoubleVerify) report each dimension separately with its own sample size and confidence interval; combining them into a single number is a category error. Two `brand_lift` rows under different `lift_dimension` are different surveyed outcomes and must not be summed.' ), + NoExplicitNull, ] = None @@ -97,6 +107,7 @@ class CommittedMetric1(AdCPBaseModel): Field( description="Disambiguates metrics whose definition varies by qualifier. Today carries five keys — `viewability_standard` (MRC vs GroupM viewability), `completion_source` (seller- vs vendor-attested completion), `attribution_methodology` (how attribution was computed for outcome metrics), `attribution_window` (the time window over which outcomes were attributed), and `lift_dimension` (which dimension of brand_lift this row represents — awareness, consideration, etc.). Required when the underlying `metric_id` has multiple incompatible measurement paths AND the seller commits to a specific one. Symmetric on `missing_metrics`. Reserved for additive qualifiers in future minors — schema is closed (`additionalProperties: false`); new keys ship explicitly. **Heterogeneous value types**: qualifier values can be either string enums (`viewability_standard`, `completion_source`, `attribution_methodology`, `lift_dimension`) or structured objects (`attribution_window` is a duration `{interval, unit}`). Consumers MUST dispatch on key name to know value shape; structured-value qualifiers join on canonical (key-sorted) deep equality so `{interval: 14, unit: 'days'}` and `{unit: 'days', interval: 14}` resolve to the same partition. Rate-style metrics (`new_to_brand_rate`, `engagement_rate`, etc.) inherit the methodology of their numerator — when a rate carries `attribution_methodology` qualifier, it applies to the underlying conversions/events being rated." ), + NoExplicitNull, ] = None committed_at: Annotated[ AwareDatetime, @@ -133,12 +144,14 @@ class CommittedMetric2(AdCPBaseModel): Field( description="Optional pin of the vendor's `get_adcp_capabilities.measurement.metrics[].methodology_version` that this commitment is contracted against. When present, the seller commits to reporting values computed under this methodology version; a vendor-side methodology change that alters the value definition is a contract change and SHOULD surface as a new appended entry with its own `committed_at`, never a silent substitution. Absence means the contract does not pin a version and buyers MUST treat methodology changes as untracked. Opaque string — compare for equality, do not parse." ), + NoExplicitNull, ] = None qualifier: Annotated[ Qualifier1 | None, Field( description='Optional qualifier disambiguating commitments to the same vendor metric measured under different methodologies or windows. Same closed key set as standard-scope entries; new keys ship explicitly.' ), + NoExplicitNull, ] = None committed_at: Annotated[ AwareDatetime, diff --git a/src/adcp/types/domains/core/compact_task_input_required.py b/src/adcp/types/domains/core/compact_task_input_required.py index 2973fa742..d2fe0b687 100644 --- a/src/adcp/types/domains/core/compact_task_input_required.py +++ b/src/adcp/types/domains/core/compact_task_input_required.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/compact_task_input_required.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import context as context_1 @@ -18,8 +18,8 @@ class CompactTaskInputRequired(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - reason: Annotated[str | None, Field(max_length=200, min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + reason: Annotated[str | None, Field(max_length=200, min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/compact_task_submitted.py b/src/adcp/types/domains/core/compact_task_submitted.py index 7daf42857..31af1a265 100644 --- a/src/adcp/types/domains/core/compact_task_submitted.py +++ b/src/adcp/types/domains/core/compact_task_submitted.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/compact_task_submitted.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from . import context as context_1 @@ -21,7 +22,7 @@ class CompactTaskSubmitted(AdcpVersionEnvelope, ProtocolEnvelope): ) status: Literal['submitted'] = 'submitted' task_id: Annotated[str, Field(min_length=1)] - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/compact_task_working.py b/src/adcp/types/domains/core/compact_task_working.py index 6e5601ed3..4871accce 100644 --- a/src/adcp/types/domains/core/compact_task_working.py +++ b/src/adcp/types/domains/core/compact_task_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/compact_task_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from . import context as context_1 @@ -17,9 +17,9 @@ class CompactTaskWorking(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - percentage: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0)] = None - current_step: Annotated[str | None, Field(max_length=500)] = None - total_steps: Annotated[SchemaInt | None, Field(ge=1)] = None - step_number: Annotated[SchemaInt | None, Field(ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + percentage: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0), NoExplicitNull] = None + current_step: Annotated[str | None, Field(max_length=500), NoExplicitNull] = None + total_steps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + step_number: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/creative_approval_scope.py b/src/adcp/types/domains/core/creative_approval_scope.py index ed2909a68..f5872c8e8 100644 --- a/src/adcp/types/domains/core/creative_approval_scope.py +++ b/src/adcp/types/domains/core/creative_approval_scope.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_approval_scope.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import creative_approval_status @@ -23,5 +23,7 @@ class ScopedCreativeApproval(AdCPBaseModel): ] approval_status: creative_approval_status.CreativeApprovalStatus rejection_reason: Annotated[ - str | None, Field(description='Human-readable explanation when this scope is rejected.') + str | None, + Field(description='Human-readable explanation when this scope is rejected.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_asset.py b/src/adcp/types/domains/core/creative_asset.py index a198d3825..88706d85e 100644 --- a/src/adcp/types/domains/core/creative_asset.py +++ b/src/adcp/types/domains/core/creative_asset.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_asset.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StrictFloat, StringConstraints, model_validator from ..enums import creative_status @@ -29,11 +29,14 @@ class Input(AdCPBaseModel): ) name: Annotated[str, Field(description='Human-readable name for this preview variant')] macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to apply for this preview') + dict[str, str] | None, + Field(description='Macro values to apply for this preview'), + NoExplicitNull, ] = None context_description: Annotated[ str | None, Field(description='Natural language description of the context for AI-generated content'), + NoExplicitNull, ] = None @@ -58,24 +61,28 @@ class CreativeAsset(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' ), + NoExplicitNull, ] = None representation_selection: Annotated[ representation_selection_1.RepresentationSelection | None, Field( description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' ), + NoExplicitNull, ] = None assets: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], @@ -88,21 +95,26 @@ class CreativeAsset(AdCPBaseModel): Field( description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' ), + NoExplicitNull, ] = None inputs: Annotated[ list[Input] | None, Field( description='Preview contexts for generative formats - defines what scenarios to generate previews for' ), + NoExplicitNull, ] = None tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') + list[str] | None, + Field(description='User-defined tags for organization and searchability'), + NoExplicitNull, ] = None status: Annotated[ creative_status.CreativeStatus | None, Field( description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." ), + NoExplicitNull, ] = None weight: Annotated[ StrictFloat | None, @@ -111,6 +123,7 @@ class CreativeAsset(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -118,6 +131,7 @@ class CreativeAsset(AdCPBaseModel): description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -126,18 +140,21 @@ class CreativeAsset(AdCPBaseModel): description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', min_length=1, ), + NoExplicitNull, ] = None industry_identifiers: Annotated[ list[industry_identifier.IndustryIdentifier] | None, Field( description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' ), + NoExplicitNull, ] = None rights: Annotated[ list[rights_constraint.RightsConstraint] | None, @@ -145,6 +162,7 @@ class CreativeAsset(AdCPBaseModel): description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', min_length=1, ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/creative_assignment.py b/src/adcp/types/domains/core/creative_assignment.py index c2b5857fe..9e6f38425 100644 --- a/src/adcp/types/domains/core/creative_assignment.py +++ b/src/adcp/types/domains/core/creative_assignment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_assignment.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from . import placement_ref @@ -32,12 +32,14 @@ class CreativeAssignment(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None rotation_mode: Annotated[ RotationMode | None, Field( description='Package-scoped rotation policy repeated on assignment rows for wire compatibility. Omission means weighted, preserving existing weight behavior. Every assignment in a package MUST resolve to the same effective mode: weighted uses relative weights; even balances delivery across eligible assignments; sequential cycles through sequence_position in ascending order within each group; random makes an independent uniform selection from eligible assignments. Sellers MUST reject conflicting effective modes rather than choose one by array order.' ), + NoExplicitNull, ] = None group_id: Annotated[ str | None, @@ -45,6 +47,7 @@ class CreativeAssignment(AdCPBaseModel): description="Package-local creative pool identifier. The identifier has no meaning outside this package. Assignments that omit group_id belong to the package's default group; one eligible creative is selected from each applicable group per serving opportunity.", min_length=1, ), + NoExplicitNull, ] = None sequence_position: Annotated[ SchemaInt | None, @@ -52,6 +55,7 @@ class CreativeAssignment(AdCPBaseModel): description="One-based order within the assignment's package-local group. Required only for sequential rotation and unique within that group.", ge=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -59,6 +63,7 @@ class CreativeAssignment(AdCPBaseModel): description="Optional structured product-context refs routing this creative within already-purchased package inventory. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. This field never narrows purchased inventory; use targeting_overlay.placement_selection for that. Every ref MUST fall within the package's committed placement selection. New senders SHOULD include publisher_domain for publisher-catalog placements. When omitted, the creative runs across the purchased placements compatible with its format. If both placement_refs and legacy placement_ids are present, placement_refs wins.", min_length=1, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -67,4 +72,5 @@ class CreativeAssignment(AdCPBaseModel): description='Legacy shorthand routing IDs within already-purchased inventory. This field never narrows purchased inventory; use targeting_overlay.placement_selection. New senders SHOULD use placement_refs because IDs are publisher-scoped. If placement_refs is also present, receivers MUST ignore this field.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_brief.py b/src/adcp/types/domains/core/creative_brief.py index 5a5c4a3f4..826fbc934 100644 --- a/src/adcp/types/domains/core/creative_brief.py +++ b/src/adcp/types/domains/core/creative_brief.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_brief.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -27,11 +27,15 @@ class Messaging(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - headline: Annotated[str | None, Field(description='Primary headline')] = None - tagline: Annotated[str | None, Field(description='Supporting tagline or sub-headline')] = None - cta: Annotated[str | None, Field(description='Call-to-action text')] = None + headline: Annotated[str | None, Field(description='Primary headline'), NoExplicitNull] = None + tagline: Annotated[ + str | None, Field(description='Supporting tagline or sub-headline'), NoExplicitNull + ] = None + cta: Annotated[str | None, Field(description='Call-to-action text'), NoExplicitNull] = None key_messages: Annotated[ - list[str] | None, Field(description='Key messages to communicate in priority order') + list[str] | None, + Field(description='Key messages to communicate in priority order'), + NoExplicitNull, ] = None @@ -50,6 +54,7 @@ class RequiredDisclosure(AdCPBaseModel): Field( description='Where the disclosure should appear within the creative. prominent: clearly visible in the main creative area; footer: at the bottom of visual creatives; audio: spoken in audio/video creatives; subtitle: displayed in the subtitle or closed-caption track; overlay: superimposed on video content; end_card: displayed on video end card; pre_roll: spoken or displayed before main content; companion: in companion ad unit alongside primary creative' ), + NoExplicitNull, ] = None jurisdictions: Annotated[ list[Jurisdiction] | None, @@ -57,12 +62,14 @@ class RequiredDisclosure(AdCPBaseModel): description="Jurisdictions where this disclosure is required. ISO 3166-1 alpha-2 country codes or ISO 3166-2 subdivision codes (e.g., 'US', 'GB', 'US-NJ', 'CA-QC'). If omitted, the disclosure applies to all jurisdictions in the campaign.", min_length=1, ), + NoExplicitNull, ] = None regulation: Annotated[ str | None, Field( description="The regulation or legal authority requiring this disclosure (e.g., 'SEC Rule 156', 'FCA COBS 4.5', 'FDA 21 CFR 202')" ), + NoExplicitNull, ] = None min_duration_ms: Annotated[ SchemaInt | None, @@ -70,18 +77,21 @@ class RequiredDisclosure(AdCPBaseModel): description='Minimum display duration in milliseconds. For video/audio disclosures, how long the disclosure must be visible or audible. For static formats, how long the disclosure must remain on screen before any auto-advance.', ge=1, ), + NoExplicitNull, ] = None language: Annotated[ str | None, Field( description="Language of the disclosure text as a BCP 47 language tag (e.g., 'en', 'fr-CA', 'es'). When omitted, the disclosure is assumed to match the creative's language." ), + NoExplicitNull, ] = None persistence: Annotated[ disclosure_persistence.DisclosurePersistence | None, Field( description='How long the disclosure must persist during content playback or display. When omitted, the creative agent should use the most restrictive persistence mode supported by the target format.' ), + NoExplicitNull, ] = None @@ -95,6 +105,7 @@ class Compliance(AdCPBaseModel): description='Disclosures that must appear in creatives for this campaign. Each disclosure specifies the text, where it should appear, and which jurisdictions require it.', min_length=1, ), + NoExplicitNull, ] = None prohibited_claims: Annotated[ list[str] | None, @@ -102,6 +113,7 @@ class Compliance(AdCPBaseModel): description='Claims that must not appear in creatives for this campaign. Creative agents should ensure generated content avoids these claims.', min_length=1, ), + NoExplicitNull, ] = None @@ -115,32 +127,39 @@ class CreativeBrief(AdCPBaseModel): Field( description='Campaign objective that guides creative tone and call-to-action strategy' ), + NoExplicitNull, ] = None tone: Annotated[ str | None, Field( description="Desired tone for this campaign, modulating the brand's base tone (e.g., 'playful and festive', 'premium and aspirational')" ), + NoExplicitNull, ] = None audience: Annotated[ - str | None, Field(description='Target audience description for this campaign') + str | None, + Field(description='Target audience description for this campaign'), + NoExplicitNull, ] = None territory: Annotated[ str | None, Field(description='Creative territory or positioning the campaign should occupy'), + NoExplicitNull, ] = None messaging: Annotated[ - Messaging | None, Field(description='Messaging framework for the campaign') + Messaging | None, Field(description='Messaging framework for the campaign'), NoExplicitNull ] = None reference_assets: Annotated[ list[reference_asset.ReferenceAsset] | None, Field( description='Visual and strategic reference materials such as mood boards, product shots, example creatives, and strategy documents' ), + NoExplicitNull, ] = None compliance: Annotated[ Compliance | None, Field( description='Regulatory and legal compliance requirements for this campaign. Campaign-specific, regional, and product-based — distinct from brand-level disclaimers in brand.json.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_consumption.py b/src/adcp/types/domains/core/creative_consumption.py index 1028709f6..a3339dc6e 100644 --- a/src/adcp/types/domains/core/creative_consumption.py +++ b/src/adcp/types/domains/core/creative_consumption.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_consumption.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat @@ -17,13 +17,17 @@ class CreativeConsumption(AdCPBaseModel): tokens: Annotated[ SchemaInt | None, Field(description='LLM or generation tokens consumed during creative generation.', ge=0), + NoExplicitNull, ] = None images_generated: Annotated[ - SchemaInt | None, Field(description='Number of images produced during generation.', ge=0) + SchemaInt | None, + Field(description='Number of images produced during generation.', ge=0), + NoExplicitNull, ] = None renders: Annotated[ SchemaInt | None, Field(description='Number of render passes performed (video, animation).', ge=0), + NoExplicitNull, ] = None duration_seconds: Annotated[ StrictFloat | None, @@ -31,4 +35,5 @@ class CreativeConsumption(AdCPBaseModel): description='Processing time billed, in seconds. For compute-time pricing models.', ge=0.0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_delivery_metrics.py b/src/adcp/types/domains/core/creative_delivery_metrics.py index 3f5ce36b8..e44925b09 100644 --- a/src/adcp/types/domains/core/creative_delivery_metrics.py +++ b/src/adcp/types/domains/core/creative_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field, StrictFloat from .delivery_metrics import DeliveryMetrics @@ -20,6 +21,7 @@ class CreativeDeliveryMetrics(DeliveryMetrics): Field( description='Optional human-readable creative name current when the report is generated. Convenience metadata only: names may change and buyers MUST use creative_id as the stable identity.' ), + NoExplicitNull, ] = None weight: Annotated[ StrictFloat | None, @@ -28,6 +30,7 @@ class CreativeDeliveryMetrics(DeliveryMetrics): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/creative_filters.py b/src/adcp/types/domains/core/creative_filters.py index 380bb8dca..47fe442bf 100644 --- a/src/adcp/types/domains/core/creative_filters.py +++ b/src/adcp/types/domains/core/creative_filters.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_filters.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import asset_content_type, creative_status, indicator_type @@ -25,42 +25,52 @@ class CreativeFilters(AdCPBaseModel): description='Filter creatives by owning accounts. Useful for agencies managing multiple client accounts.', min_length=1, ), + NoExplicitNull, ] = None statuses: Annotated[ list[creative_status.CreativeStatus] | None, Field(description='Filter by creative approval statuses', min_length=1), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, Field(description='Filter by creative tags (all tags must match)', min_length=1), + NoExplicitNull, ] = None tags_any: Annotated[ list[str] | None, Field(description='Filter by creative tags (any tag must match)', min_length=1), + NoExplicitNull, ] = None name_contains: Annotated[ str | None, Field(description='Filter by creative names containing this text (case-insensitive)'), + NoExplicitNull, ] = None creative_ids: Annotated[ list[str] | None, Field(description='Filter by specific creative IDs', max_length=100, min_length=1), + NoExplicitNull, ] = None created_after: Annotated[ AwareDatetime | None, Field(description='Filter creatives created after this date (ISO 8601)'), + NoExplicitNull, ] = None created_before: Annotated[ AwareDatetime | None, Field(description='Filter creatives created before this date (ISO 8601)'), + NoExplicitNull, ] = None updated_after: Annotated[ AwareDatetime | None, Field(description='Filter creatives last updated after this date (ISO 8601)'), + NoExplicitNull, ] = None updated_before: Annotated[ AwareDatetime | None, Field(description='Filter creatives last updated before this date (ISO 8601)'), + NoExplicitNull, ] = None assigned_to_packages: Annotated[ list[str] | None, @@ -68,6 +78,7 @@ class CreativeFilters(AdCPBaseModel): description='Filter creatives assigned to any of these packages. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.', min_length=1, ), + NoExplicitNull, ] = None media_buy_ids: Annotated[ list[str] | None, @@ -75,18 +86,21 @@ class CreativeFilters(AdCPBaseModel): description='Filter creatives assigned to any of these media buys. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.', min_length=1, ), + NoExplicitNull, ] = None unassigned: Annotated[ StrictBool | None, Field( description='Filter for unassigned creatives when true, assigned creatives when false. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.' ), + NoExplicitNull, ] = None has_served: Annotated[ StrictBool | None, Field( description='When true, return only creatives that have served at least one impression. When false, return only creatives that have never served.' ), + NoExplicitNull, ] = None indicator_types: Annotated[ list[indicator_type.IndicatorType] | None, @@ -94,6 +108,7 @@ class CreativeFilters(AdCPBaseModel): description='Return creatives with at least one package assignment carrying any requested current indicator type. Values within this field use OR logic; this field composes with other filters using AND logic. Sales-agent-specific: sellers support this filter only when media_buy.relationship_notifications.projection_tasks includes list_creatives. Other agents SHOULD ignore it and apply remaining filters. Buyers needing exact results MUST verify capability support and paginate the outer result set; assignment_projection: matching bounds nested rows.', min_length=1, ), + NoExplicitNull, ] = None concept_ids: Annotated[ list[str] | None, @@ -101,6 +116,7 @@ class CreativeFilters(AdCPBaseModel): description='Filter by creative concept IDs. Concepts group related creatives across sizes and formats (e.g., Flashtalking concepts, Celtra campaign folders, CM360 creative groups).', min_length=1, ), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -109,6 +125,7 @@ class CreativeFilters(AdCPBaseModel): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Filter legacy named-format creatives. Use `format_kinds` for canonical libraries.', min_length=1, ), + NoExplicitNull, ] = None format_kinds: Annotated[ list[str] | None, @@ -116,6 +133,7 @@ class CreativeFilters(AdCPBaseModel): description='Filter by canonical format kinds. Returns creatives matching any supplied kind.', min_length=1, ), + NoExplicitNull, ] = None asset_types: Annotated[ list[asset_content_type.AssetContentType] | None, @@ -123,16 +141,19 @@ class CreativeFilters(AdCPBaseModel): description="Filter by asset types present on direct object values in the creative's top-level `assets` map. A creative matches when any directly assigned object has an `asset_type` in this array (OR within this field); this filter is conjunctive with every other active filter (AND across fields). Do not inspect array-valued slots or recurse into nested asset fields such as `cards[].media`; broader traversal is deferred. Agents that do not implement this filter MUST ignore it and apply the remaining filters rather than reject the request. Exact asset-type values use the shared AssetContentType vocabulary; `published_post` selects existing-published-post reference creatives without relying on publisher-specific format IDs.", min_length=1, ), + NoExplicitNull, ] = None has_variables: Annotated[ StrictBool | None, Field( description='When true, return only creatives with dynamic variables (DCO). When false, return only static creatives.' ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Vendor-namespaced extension parameters for seller- or platform-specific creative filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; avoid unbounded logging or labels, and do not interpolate values into caller-visible error strings, LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_localization.py b/src/adcp/types/domains/core/creative_localization.py index ac8e26459..2e009526e 100644 --- a/src/adcp/types/domains/core/creative_localization.py +++ b/src/adcp/types/domains/core/creative_localization.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_localization.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StringConstraints from . import locale_tag, localized_creative_asset @@ -106,6 +106,7 @@ class CreativeLocalization(AdCPBaseModel): max_length=50, min_length=1, ), + NoExplicitNull, ] = None default_locale_variant_id: Annotated[ str, diff --git a/src/adcp/types/domains/core/creative_localization_readback.py b/src/adcp/types/domains/core/creative_localization_readback.py index b8c16b238..dd765809d 100644 --- a/src/adcp/types/domains/core/creative_localization_readback.py +++ b/src/adcp/types/domains/core/creative_localization_readback.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_localization_readback.json -# timestamp: 2026-10-03T00:36:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StringConstraints from . import locale_tag, localized_creative_asset @@ -124,6 +124,7 @@ class CreativeLocalizationReadback(AdCPBaseModel): max_length=50, min_length=1, ), + NoExplicitNull, ] = None variants: Annotated[ list[Variants], diff --git a/src/adcp/types/domains/core/creative_manifest.py b/src/adcp/types/domains/core/creative_manifest.py index 24e57fb11..5f3800c93 100644 --- a/src/adcp/types/domains/core/creative_manifest.py +++ b/src/adcp/types/domains/core/creative_manifest.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_manifest.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StringConstraints, model_validator from . import brand_ref, creative_assets @@ -56,24 +56,28 @@ def _coerce_standalone_assets(cls, data: Any) -> Any: deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format path retained for 3.x compatibility. New manifests use canonical `format_kind` and, when product routing requires it, `format_option_ref`. Mutually exclusive with format_kind.', ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description="Canonical 3.2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_vast`, `seller_rendered_stateful_display`, `coordinated_placements`). Selects the contract against which the seller validates the manifest's assets. Mutually exclusive with deprecated `format_id`." ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product carries multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the manifest to a single declaration. Product-scoped refs require an enclosing target product/package context.' ), + NoExplicitNull, ] = None representation_selection: Annotated[ representation_selection_1.RepresentationSelection | None, Field( description='Present when this seller-bound manifest was selected from a CreativeRepresentationSet. Preserves creative, complete revision digest, and selected representation lineage through sync and reporting.' ), + NoExplicitNull, ] = None assets: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], @@ -86,32 +90,37 @@ def _coerce_standalone_assets(cls, data: Any) -> Any: Field( description="Component-addressed asset maps for `coordinated_placements`. Each key MUST match one `params.components[].component_id`; its value supplies that component's canonical slots. Shared assets remain in top-level `assets` and are injected only into components named by `shared_slots[].consumed_by`. This namespace allows two components to use the same canonical slot name, such as `image_main`, without collision. It MUST be absent for non-`coordinated_placements` manifests." ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description="Brand identity reference (BrandRef — `domain` plus optional `brand_id` for house-of-brands; plus optional inline `brand_kit_override` for per-creative tweaks where brand.json is missing/stale). When present, the seller pulls master brand identity (logo, palette, fonts, voice, and visual guidelines) from the brand's brand.json automatically; supported fields present in `brand_kit_override` take precedence, and all other master identity fields continue to come from brand.json. Catalogs supply product or item payload. Catalog item asset groups — including an item-level `logo` for a property or franchise — are item identity selected through format field bindings; they do not override brand.json's master logo or other brand identity fields. v2 formats no longer redeclare brand_logo / brand_colors / brand_voice as explicit slots — brand identity is implicit context." ), + NoExplicitNull, ] = None rights: Annotated[ list[rights_constraint.RightsConstraint] | None, Field( description='Rights constraints attached to this creative. Buyer-carried fields are informational until a serving party evaluates an issuer-bound attestation reference under its own policy. Only a verified, unexpired, unrevoked, digest-matched evaluation can support serving authorization; verification_url is never authority.' ), + NoExplicitNull, ] = None industry_identifiers: Annotated[ list[industry_identifier.IndustryIdentifier] | None, Field( description='Industry-standard or market-specific identifiers for this specific manifest (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). When present, overrides creative-level identifiers. Use when different format versions of the same source creative have distinct traffic identifiers (e.g., the :15 and :30 cuts, or separate TV and radio versions). Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' ), + NoExplicitNull, ] = None provenance: Annotated[ provenance_1.Provenance | None, Field( description='Provenance metadata for this creative manifest. Serves as the default provenance for all assets in this manifest. An asset with its own provenance replaces this object entirely (no field-level merging).' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> CreativeManifest: diff --git a/src/adcp/types/domains/core/creative_operation_format_declaration.py b/src/adcp/types/domains/core/creative_operation_format_declaration.py index 939a09e67..7b685a320 100644 --- a/src/adcp/types/domains/core/creative_operation_format_declaration.py +++ b/src/adcp/types/domains/core/creative_operation_format_declaration.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_operation_format_declaration.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, Field, StrictBool from ..enums import channels @@ -26,6 +26,7 @@ class CreativeOperationFormatDeclaration(AdCPBaseModel): Field( description='Stable publisher- or product-declaration identity that this creative operation can satisfy. When publisher_domain is present, this field is required.' ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -33,6 +34,7 @@ class CreativeOperationFormatDeclaration(AdCPBaseModel): description='Publisher namespace for format_option_id when this operation claims compatibility with a publisher declaration.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -40,20 +42,25 @@ class CreativeOperationFormatDeclaration(AdCPBaseModel): description='Creative-route processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. These capabilities describe build, validation, or preview processing and do not grant seller production authority.', min_length=1, ), + NoExplicitNull, ] = None - technical_requirements_complete: StrictBool | None = None - display_name: str | None = None - sample_render_url: AnyUrl | None = None - applies_to_channels: list[channels.MediaChannel] | None = None - seller_preference: SellerPreference | None = None - locale_policy: creative_locale_policy.CreativeLocalePolicy | None = None - canonical_formats_only: StrictBool | None = False - experimental: StrictBool | None = False - format_shape: str | None = None + technical_requirements_complete: Annotated[StrictBool | None, NoExplicitNull] = None + display_name: Annotated[str | None, NoExplicitNull] = None + sample_render_url: Annotated[AnyUrl | None, NoExplicitNull] = None + applies_to_channels: Annotated[list[channels.MediaChannel] | None, NoExplicitNull] = None + seller_preference: Annotated[SellerPreference | None, NoExplicitNull] = None + locale_policy: Annotated[creative_locale_policy.CreativeLocalePolicy | None, NoExplicitNull] = ( + None + ) + canonical_formats_only: Annotated[StrictBool | None, NoExplicitNull] = False + experimental: Annotated[StrictBool | None, NoExplicitNull] = False + format_shape: Annotated[str | None, NoExplicitNull] = None v1_format_ref: Annotated[ - list[format_id.FormatReferenceStructuredObject] | None, Field(min_length=1) + list[format_id.FormatReferenceStructuredObject] | None, Field(min_length=1), NoExplicitNull + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, NoExplicitNull ] = None - format_schema: platform_extension_ref.PlatformExtensionReference | None = None format_kind: str params: Annotated[ dict[str, Any], diff --git a/src/adcp/types/domains/core/creative_policy.py b/src/adcp/types/domains/core/creative_policy.py index e7e2b1f8b..70c2593ac 100644 --- a/src/adcp/types/domains/core/creative_policy.py +++ b/src/adcp/types/domains/core/creative_policy.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_policy.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import co_branding_requirement, landing_page_requirement @@ -21,24 +21,28 @@ class ProvenanceRequirements(AdCPBaseModel): Field( description='When true, the seller requires creatives to include a `digital_source_type` field in their provenance, set to a valid value from the `digital-source-type` enum (not null or absent). Submissions that omit this field are rejected with `PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING`. Supports EU AI Act Art. 50 and CA SB 942 compliance workflows where AI disclosure metadata must be present at the protocol level.' ), + NoExplicitNull, ] = None require_synthetic_depiction: Annotated[ StrictBool | None, Field( description='When true, the seller requires creatives to include an assessed `synthetic_depiction` boolean in their resolved provenance. Both true and false satisfy the requirement; absence means unassessed and is rejected with `PROVENANCE_SYNTHETIC_DEPICTION_MISSING`. This gate requires a declaration only — it does not establish consent, legality, or independent verification.' ), + NoExplicitNull, ] = None require_disclosure_metadata: Annotated[ StrictBool | None, Field( description='When true, the seller requires creatives to include a `disclosure` object in their provenance with `disclosure.required` set to a boolean value (true or false). When `disclosure.required` is true, at least one entry in `disclosure.jurisdictions` is expected. Submissions that omit `disclosure.required` are rejected with `PROVENANCE_DISCLOSURE_MISSING`.' ), + NoExplicitNull, ] = None require_embedded_provenance: Annotated[ StrictBool | None, Field( description='When true, the seller requires creatives to include at least one `embedded_provenance` entry. For pipelines where sidecar metadata is stripped by intermediaries, this ensures provenance data persists through delivery. Submissions that omit `embedded_provenance` are rejected with `PROVENANCE_EMBEDDED_MISSING`.' ), + NoExplicitNull, ] = None @@ -57,6 +61,7 @@ class AcceptedVerifier(AdCPBaseModel): Field( description="Optional canonical `feature_id` the seller will request against this agent (e.g., `encypher.markers_present_v2`). When present, the buyer's `verify_agent.feature_id` SHOULD either match this value or be omitted. When absent, the seller selects a feature from the agent's `governance.creative_features` catalog at evaluation time. Resolves the selector ambiguity that would otherwise let two compliant receivers reach different verdicts." ), + NoExplicitNull, ] = None providers: Annotated[ list[str] | None, @@ -64,6 +69,7 @@ class AcceptedVerifier(AdCPBaseModel): description="Optional `provider` labels this agent verifies (e.g., `['Encypher', 'Digimarc']`). When present, sellers SHOULD only invoke this agent for `embedded_provenance[]` / `watermarks[]` entries whose `provider` field matches one of these labels — letting buyers pre-flight whether their attached evidence is verifiable against the seller's allowlist. When absent, the agent is treated as provider-agnostic.", min_length=1, ), + NoExplicitNull, ] = None @@ -86,12 +92,14 @@ class CreativePolicy(AdCPBaseModel): Field( description='Whether creatives must include provenance metadata. When true, the seller requires buyers to attach provenance declarations to creative submissions. The seller may independently verify claims via get_creative_features.' ), + NoExplicitNull, ] = None provenance_requirements: Annotated[ ProvenanceRequirements | None, Field( description='Structured provenance requirements for creatives. Refines `provenance_required`: when `provenance_required` is true, the fields in this object specify which provenance features the seller requires. When `provenance_required` is false or absent, this object SHOULD be absent; if present, receivers MUST ignore it. Existing seller agents that do not read this object are unaffected; the wire shape does not change for them. Sellers that publish a requirement here MUST enforce it on creative submission: a `sync_creatives` request that omits a required field is rejected with the corresponding `PROVENANCE_*` error code (see error-code.json), and a creative whose provenance claim is contradicted by an independent verification (`get_creative_features` against a governance agent the seller operates or has allowlisted via `accepted_verifiers`) is rejected with `PROVENANCE_CLAIM_CONTRADICTED`. This is the structural-rejection surface; the truth-of-claim surface lives in `get_creative_features`. Field-level requirements are seller-enforced — JSON Schema validation does not check them.' ), + NoExplicitNull, ] = None accepted_verifiers: Annotated[ list[AcceptedVerifier] | None, @@ -99,4 +107,5 @@ class CreativePolicy(AdCPBaseModel): description='Governance agents the seller operates, has allowlisted, or otherwise trusts to verify provenance claims via `get_creative_features`. Buyers attaching a `verify_agent` pointer on `embedded_provenance[]` or `watermarks[]` MUST select an `agent_url` that appears in this list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments) - the buyer is *representing* that they used a verifier the seller will recognize, not asserting unilateral routing. Sellers MUST reject `sync_creatives` submissions whose `verify_agent.agent_url` does not match any entry here with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. The seller is the verifier-of-record: it is the seller, not the buyer, that decides which agent it will call. Publishing the list lets buyers pre-flight their creative shape against `get_products` and lets multiple buyers converge on the same verifier without coordinating with each other.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_representation.py b/src/adcp/types/domains/core/creative_representation.py index 9b2021655..c81567243 100644 --- a/src/adcp/types/domains/core/creative_representation.py +++ b/src/adcp/types/domains/core/creative_representation.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_representation.json -# timestamp: 2026-10-05T14:13:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from .creative_manifest import CreativeManifest @@ -26,6 +26,7 @@ class Source(AdCPBaseModel): source_id: Annotated[ str | None, Field(description='Optional immutable source-system identifier for this representation.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_representation_set.py b/src/adcp/types/domains/core/creative_representation_set.py index acb11b8f9..e5e7b515c 100644 --- a/src/adcp/types/domains/core/creative_representation_set.py +++ b/src/adcp/types/domains/core/creative_representation_set.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_representation_set.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import creative_representation, creative_revision_id @@ -50,4 +50,5 @@ class CreativeRepresentationSet(AdCPBaseModel): Field( description='Default provenance for every retained representation and for the selected seller-bound output unless a representation supplies a narrower provenance object.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/creative_variable.py b/src/adcp/types/domains/core/creative_variable.py index 1f53dc90c..f13e1e825 100644 --- a/src/adcp/types/domains/core/creative_variable.py +++ b/src/adcp/types/domains/core/creative_variable.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/creative_variable.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool @@ -40,8 +40,10 @@ class CreativeVariable(AdCPBaseModel): Field( description='Default value used when no dynamic value is provided at serve time. All types are string-encoded: text/image/video/audio/url as literal strings, number as decimal (e.g., "42.99"), boolean as "true"/"false", color as "#RRGGBB", date as ISO 8601 (e.g., "2026-12-25T00:00:00Z").' ), + NoExplicitNull, ] = None required: Annotated[ StrictBool | None, Field(description='Whether this variable must have a value for the creative to serve'), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/creative_variant.py b/src/adcp/types/domains/core/creative_variant.py index 5b0571c9e..fec223414 100644 --- a/src/adcp/types/domains/core/creative_variant.py +++ b/src/adcp/types/domains/core/creative_variant.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/creative_variant.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import creative_manifest, creative_revision_id @@ -34,14 +34,16 @@ class GenerationContext(AdCPBaseModel): Field( description="Type of context that triggered generation (e.g., 'web_page', 'conversational', 'search', 'app', 'dooh')" ), + NoExplicitNull, ] = None artifact: Annotated[ Artifact | None, Field( description='Reference to the content-standards artifact that provided the generation context. Links this variant to the specific piece of content (article, video, podcast segment, etc.) where the ad was placed.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CreativeVariant(DeliveryMetrics): @@ -56,6 +58,7 @@ class CreativeVariant(DeliveryMetrics): Field( description='Buyer-authored input revision from which this served execution was derived. Required when that execution derives from a revision-aware creative. Historical reporting may legitimately return an older revision after list_creatives shows a newer current revision. Delivery optimization, generation, transcoding, or representation selection does not mint a revision.' ), + NoExplicitNull, ] = None locale_variant_id: Annotated[ str | None, @@ -64,16 +67,19 @@ class CreativeVariant(DeliveryMetrics): max_length=255, min_length=1, ), + NoExplicitNull, ] = None manifest: Annotated[ creative_manifest.CreativeManifest | None, Field( description='The rendered creative manifest for this variant — the actual output that was served, not the input assets. In 3.2 it carries canonical `format_kind`, optional `format_option_ref`, and the resolved assets (specific headline, image, video, etc. the platform selected or generated). For Tier 2, shows which asset combination was picked. For Tier 3, contains the generated assets which may differ entirely from the input brand identity. Pass to preview_creative to re-render.' ), + NoExplicitNull, ] = None generation_context: Annotated[ GenerationContext | None, Field( description='Input signals that triggered generation of this variant (Tier 3). Describes why the platform created this specific variant. Platforms should provide summarized or anonymized signals rather than raw user input. For web contexts, may include page topic or URL. For conversational contexts, an anonymized content signal. For search, query category or intent. When the content context is managed through AdCP content standards, reference the artifact directly via the artifact field.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/daast_tracker_constraints.py b/src/adcp/types/domains/core/daast_tracker_constraints.py index e28dfb3b8..3d0a9f0f4 100644 --- a/src/adcp/types/domains/core/daast_tracker_constraints.py +++ b/src/adcp/types/domains/core/daast_tracker_constraints.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/daast_tracker_constraints.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import Field, RootModel @@ -46,7 +46,7 @@ class DaastVersions(RootModel[list[daast_version.DaastVersion]]): class DaastTrackerConstraints(AdCPBaseModel): - daast_event: DaastEvent | None = None - target: DaastTarget | None = DaastTarget.linear - offset: DaastOffset | None = None - daast_versions: DaastVersions | None = None + daast_event: Annotated[DaastEvent | None, NoExplicitNull] = None + target: Annotated[DaastTarget | None, NoExplicitNull] = DaastTarget.linear + offset: Annotated[DaastOffset | None, NoExplicitNull] = None + daast_versions: Annotated[DaastVersions | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/daypart_target.py b/src/adcp/types/domains/core/daypart_target.py index a6734ee28..51635049a 100644 --- a/src/adcp/types/domains/core/daypart_target.py +++ b/src/adcp/types/domains/core/daypart_target.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/daypart_target.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ..enums import day_of_week @@ -46,10 +46,12 @@ class DaypartTarget(AdCPBaseModel): description="Civil-time clock used to evaluate this window. 'inventory_local' evaluates the hours in the seller-assigned local timezone of each inventory unit that can deliver the impression, such as a screen, venue, station, or publisher property; it never means the buyer, account, or server timezone. A concrete IANA timezone identifier (for example, 'America/New_York', 'CET', or 'UTC') evaluates one shared civil-time clock across the targeted inventory. Omission defaults to 'inventory_local'. Buyers that begin with a user or account preference MUST resolve it to a concrete IANA identifier before sending the daypart; 'user_timezone' and 'account_timezone' are not wire values. For each candidate delivery instant, convert the instant into this clock and compare its resulting local day and hour with the half-open window: a skipped DST hour has no matching instants, while both occurrences of a repeated hour match. This delivery clock is independent of reporting_capabilities.timezone.", validate_default=True, ), + NoExplicitNull, ] = 'inventory_local' label: Annotated[ str | None, Field( description="Optional human-readable name for this time window (e.g., 'Morning Drive', 'Prime Time')" ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/deadline_policy.py b/src/adcp/types/domains/core/deadline_policy.py index 1f01c52cd..a153d7bb2 100644 --- a/src/adcp/types/domains/core/deadline_policy.py +++ b/src/adcp/types/domains/core/deadline_policy.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/deadline_policy.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -24,7 +24,9 @@ class MaterialStage(AdCPBaseModel): lead_days: Annotated[ SchemaInt, Field(description='Days before scheduled_at this stage is due', ge=0) ] - label: Annotated[str | None, Field(description='What the seller needs at this stage')] = None + label: Annotated[ + str | None, Field(description='What the seller needs at this stage'), NoExplicitNull + ] = None class DeadlinePolicy(AdCPBaseModel): @@ -34,10 +36,12 @@ class DeadlinePolicy(AdCPBaseModel): booking_lead_days: Annotated[ SchemaInt | None, Field(description='Days before scheduled_at by which the placement must be booked', ge=0), + NoExplicitNull, ] = None cancellation_lead_days: Annotated[ SchemaInt | None, Field(description='Days before scheduled_at by which cancellation is penalty-free', ge=0), + NoExplicitNull, ] = None material_stages: Annotated[ list[MaterialStage] | None, @@ -45,10 +49,12 @@ class DeadlinePolicy(AdCPBaseModel): description='Default material submission stages. Items MUST be in chronological order (earliest due first). Agents compute due_at as: installment.scheduled_at minus lead_days.', min_length=1, ), + NoExplicitNull, ] = None business_days_only: Annotated[ StrictBool | None, Field( description='When true, lead_days counts business days (Mon-Fri) rather than calendar days. Defaults to false.' ), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/delivery_breakdown_controls.py b/src/adcp/types/domains/core/delivery_breakdown_controls.py index 6dc9fb561..0ca46bb2c 100644 --- a/src/adcp/types/domains/core/delivery_breakdown_controls.py +++ b/src/adcp/types/domains/core/delivery_breakdown_controls.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/delivery_breakdown_controls.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ..enums import sort_direction as sort_direction_1 @@ -20,16 +20,19 @@ class DeliveryBreakdownControls(AdCPBaseModel): limit: Annotated[ SchemaInt | None, Field(description='Maximum number of rows to return. Defaults to 25.', ge=1), + NoExplicitNull, ] = 25 sort_by: Annotated[ sort_metric.SortMetric | None, Field( description='Metric used to order rows. Falls back to spend when unavailable at this row grain; on fallback sort_direction resets to desc. Rows lacking a value for the applied sort metric order last regardless of direction.' ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description='Direction for sort_by ordering. Sellers MUST apply the requested direction to the applied sort metric; direction has no availability fallback.' ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc diff --git a/src/adcp/types/domains/core/delivery_forecast.py b/src/adcp/types/domains/core/delivery_forecast.py index 03c967c2f..a5f6eb90c 100644 --- a/src/adcp/types/domains/core/delivery_forecast.py +++ b/src/adcp/types/domains/core/delivery_forecast.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/delivery_forecast.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import demographic_system as demographic_system_1 @@ -33,6 +33,7 @@ class DeliveryForecast(AdCPBaseModel): Field( description="How to interpret the points array. 'spend' (default when omitted): points at ascending budget levels. 'availability': total available inventory, budget omitted. 'reach_freq': points at ascending reach/frequency targets. 'weekly'/'daily': metrics are per-period values. 'clicks'/'conversions': points at ascending outcome targets. 'package': each point is a distinct inventory package." ), + NoExplicitNull, ] = None method: Annotated[ forecast_method.ForecastMethod, Field(description='Method used to produce this forecast') @@ -48,6 +49,7 @@ class DeliveryForecast(AdCPBaseModel): Field( description='Measurement system for the demographic field. Ensures buyer and seller agree on demographic notation.' ), + NoExplicitNull, ] = None demographic: Annotated[ str | None, @@ -55,6 +57,7 @@ class DeliveryForecast(AdCPBaseModel): description='Target demographic code within the specified demographic_system. For Nielsen: P18-49, M25-54, W35+. For BARB: ABC1 Adults, 16-34. For AGF: E 14-49.', examples=['P18-49', 'A25-54', 'W35+', 'M18-34'], ), + NoExplicitNull, ] = None measurement_source: Annotated[ str | None, @@ -77,20 +80,23 @@ class DeliveryForecast(AdCPBaseModel): max_length=64, pattern='^[a-z0-9_]+$', ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description='Unit of measurement for reach and audience_size metrics in this forecast. Required for cross-channel forecast comparison.' ), + NoExplicitNull, ] = None generated_at: Annotated[ - AwareDatetime | None, Field(description='When this forecast was computed') + AwareDatetime | None, Field(description='When this forecast was computed'), NoExplicitNull ] = None valid_until: Annotated[ AwareDatetime | None, Field( description='When this forecast expires. After this time, the forecast should be refreshed. Forecast expiry does not affect proposal executability.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/delivery_metric_aggregate.py b/src/adcp/types/domains/core/delivery_metric_aggregate.py index ac7db5ec2..aea048872 100644 --- a/src/adcp/types/domains/core/delivery_metric_aggregate.py +++ b/src/adcp/types/domains/core/delivery_metric_aggregate.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/delivery_metric_aggregate.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import attribution_methodology as attribution_methodology_1 @@ -26,30 +26,35 @@ class Qualifier(AdCPBaseModel): Field( description='Viewability standard under which this row was measured. MRC and GroupM define materially different thresholds; never sum across standards.' ), + NoExplicitNull, ] = None completion_source: Annotated[ completion_source_1.CompletionSource | None, Field( description='Source of `completion_rate` attestation for this row — `seller_attested` from player/ad server, `vendor_attested` from a third-party measurement vendor. The two paths can yield different rates; never sum across sources.' ), + NoExplicitNull, ] = None attribution_methodology: Annotated[ attribution_methodology_1.AttributionMethodology | None, Field( description='Attribution methodology under which this outcome row was computed (`deterministic_purchase`, `probabilistic`, `panel_based`, `modeled`). Outcome metrics measured under different methodologies represent materially different numbers; never sum across methodologies.' ), + NoExplicitNull, ] = None attribution_window: Annotated[ duration.Duration | None, Field( description="Attribution window for this outcome row. Object-valued duration (`{interval, unit}`), not a shorthand string. Outcome metrics measured over different windows represent the same metric over different time periods; the partition keeps them as separate rows so buyers don't accidentally aggregate." ), + NoExplicitNull, ] = None lift_dimension: Annotated[ lift_dimension_1.LiftDimension | None, Field( description='Brand-lift dimension this row represents (awareness, consideration, favorability, purchase intent, or ad recall). Required when `metric_id` is `brand_lift`. Each dimension is a separate surveyed outcome with its own sample size and confidence interval; never sum across dimensions.' ), + NoExplicitNull, ] = None @@ -72,6 +77,7 @@ class Field0(AdCPBaseModel): Field( description="Qualifier keys disambiguating this row from sibling rows under the same `metric_id`. Symmetric with `committed_metrics.qualifier` today; expected to diverge in future minors as transparency disclosures buyers don't commit to ship delivery-only. Closed (`additionalProperties: false`) — new qualifier keys ship explicitly." ), + NoExplicitNull, ] = None value: Annotated[ StrictFloat, @@ -85,9 +91,12 @@ class Field0(AdCPBaseModel): description='Coverage denominator for verification metrics (e.g., `viewable_rate`). Buyers compute coverage as `measurable_impressions / impressions` from the partition.', ge=0.0, ), + NoExplicitNull, ] = None viewable_impressions: Annotated[ - StrictFloat | None, Field(description='Component for `viewable_rate` (numerator).', ge=0.0) + StrictFloat | None, + Field(description='Component for `viewable_rate` (numerator).', ge=0.0), + NoExplicitNull, ] = None impressions: Annotated[ StrictFloat | None, @@ -95,10 +104,12 @@ class Field0(AdCPBaseModel): description='Component for rate metrics whose denominator is total impressions (e.g., `completion_rate`, `engagement_rate`).', ge=0.0, ), + NoExplicitNull, ] = None completed_views: Annotated[ StrictFloat | None, Field(description='Component for `completion_rate` (numerator).', ge=0.0), + NoExplicitNull, ] = None spend: Annotated[ StrictFloat | None, @@ -106,17 +117,22 @@ class Field0(AdCPBaseModel): description='Component for cost-per metrics (denominator-ish; the cost half of the ratio).', ge=0.0, ), + NoExplicitNull, ] = None conversions: Annotated[ StrictFloat | None, Field(description='Component for `cost_per_acquisition` and ROAS-family metrics.', ge=0.0), + NoExplicitNull, ] = None conversion_value: Annotated[ - StrictFloat | None, Field(description='Component for `roas` (numerator).', ge=0.0) + StrictFloat | None, + Field(description='Component for `roas` (numerator).', ge=0.0), + NoExplicitNull, ] = None clicks: Annotated[ StrictFloat | None, Field(description='Component for `cost_per_click` and click-rate metrics.', ge=0.0), + NoExplicitNull, ] = None @@ -133,30 +149,35 @@ class Qualifier3(AdCPBaseModel): Field( description='Viewability standard under which this row was measured. MRC and GroupM define materially different thresholds; never sum across standards.' ), + NoExplicitNull, ] = None completion_source: Annotated[ completion_source_1.CompletionSource | None, Field( description='Attestation source for a vendor completion-style metric — seller_attested from player/ad server, vendor_attested from an independent measurement path. Applicability is defined by the vendor metric; never sum across sources.' ), + NoExplicitNull, ] = None attribution_methodology: Annotated[ attribution_methodology_1.AttributionMethodology | None, Field( description='Attribution methodology under which this outcome row was computed (`deterministic_purchase`, `probabilistic`, `panel_based`, `modeled`). Outcome metrics measured under different methodologies represent materially different numbers; never sum across methodologies.' ), + NoExplicitNull, ] = None attribution_window: Annotated[ duration.Duration | None, Field( description="Attribution window for this outcome row. Object-valued duration (`{interval, unit}`), not a shorthand string. Outcome metrics measured over different windows represent the same metric over different time periods; the partition keeps them as separate rows so buyers don't accidentally aggregate." ), + NoExplicitNull, ] = None lift_dimension: Annotated[ lift_dimension_1.LiftDimension | None, Field( description='Lift dimension this row represents (awareness, consideration, favorability, purchase intent, or ad recall) for vendor lift-style metrics. Applicability is defined by the vendor metric; each dimension is a separate surveyed outcome and rows under different dimensions must not be summed.' ), + NoExplicitNull, ] = None @@ -183,6 +204,7 @@ class DeliveryMetricAggregate2(AdCPBaseModel): Field( description='Optional qualifier keys disambiguating this vendor-metric row from sibling rows under the same (vendor, metric_id) — e.g., attribution_window on a vendor outcome metric. Same closed key set as the standard branch; new keys ship explicitly.' ), + NoExplicitNull, ] = None value: Annotated[ StrictFloat, @@ -196,6 +218,7 @@ class DeliveryMetricAggregate2(AdCPBaseModel): description="Coverage denominator — vendor measurement is rarely 100% of delivery (only impressions where the vendor's SDK fired or panel matched). Buyers compute coverage as `measurable_impressions / impressions`. Same convention as `vendor_metric_value.measurable_impressions`.", ge=0.0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/delivery_metrics.py b/src/adcp/types/domains/core/delivery_metrics.py index ebd4f2f33..352bb3820 100644 --- a/src/adcp/types/domains/core/delivery_metrics.py +++ b/src/adcp/types/domains/core/delivery_metrics.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/delivery_metrics.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..enums import action_source as action_source_1 @@ -25,18 +25,18 @@ class Kind(StrEnum): class QuartileData(AdCPBaseModel): - q1_views: Annotated[StrictFloat | None, Field(description='25% completion views', ge=0.0)] = ( - None - ) - q2_views: Annotated[StrictFloat | None, Field(description='50% completion views', ge=0.0)] = ( - None - ) - q3_views: Annotated[StrictFloat | None, Field(description='75% completion views', ge=0.0)] = ( - None - ) - q4_views: Annotated[StrictFloat | None, Field(description='100% completion views', ge=0.0)] = ( - None - ) + q1_views: Annotated[ + StrictFloat | None, Field(description='25% completion views', ge=0.0), NoExplicitNull + ] = None + q2_views: Annotated[ + StrictFloat | None, Field(description='50% completion views', ge=0.0), NoExplicitNull + ] = None + q3_views: Annotated[ + StrictFloat | None, Field(description='75% completion views', ge=0.0), NoExplicitNull + ] = None + q4_views: Annotated[ + StrictFloat | None, Field(description='100% completion views', ge=0.0), NoExplicitNull + ] = None class VenueBreakdownItem(AdCPBaseModel): @@ -44,19 +44,24 @@ class VenueBreakdownItem(AdCPBaseModel): extra='allow', ) venue_id: Annotated[str, Field(description='Venue identifier')] - venue_name: Annotated[str | None, Field(description='Human-readable venue name')] = None + venue_name: Annotated[ + str | None, Field(description='Human-readable venue name'), NoExplicitNull + ] = None venue_type: Annotated[ str | None, Field(description="Venue type (e.g., 'airport', 'transit', 'retail', 'billboard')"), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt, Field(description='Impressions delivered at this venue', ge=0) ] - loop_plays: Annotated[SchemaInt | None, Field(description='Loop plays at this venue', ge=0)] = ( - None - ) + loop_plays: Annotated[ + SchemaInt | None, Field(description='Loop plays at this venue', ge=0), NoExplicitNull + ] = None screens_used: Annotated[ - SchemaInt | None, Field(description='Number of screens used at this venue', ge=0) + SchemaInt | None, + Field(description='Number of screens used at this venue', ge=0), + NoExplicitNull, ] = None @@ -65,13 +70,17 @@ class DoohMetrics1(AdCPBaseModel): extra='allow', ) loop_plays: Annotated[ - SchemaInt | None, Field(description='Number of times ad played in rotation', ge=0) + SchemaInt | None, + Field(description='Number of times ad played in rotation', ge=0), + NoExplicitNull, ] = None screens_used: Annotated[ - SchemaInt | None, Field(description='Number of unique screens displaying the ad', ge=0) + SchemaInt | None, + Field(description='Number of unique screens displaying the ad', ge=0), + NoExplicitNull, ] = None screen_time_seconds: Annotated[ - SchemaInt | None, Field(description='Total display time in seconds', ge=0) + SchemaInt | None, Field(description='Total display time in seconds', ge=0), NoExplicitNull ] = None sov_achieved: Annotated[ StrictFloat | None, @@ -80,15 +89,19 @@ class DoohMetrics1(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None calculation_notes: Annotated[ str | None, Field( description="Per-row supplementary methodology notes for DOOH impression calculation (e.g., 'rotation-based; 6-second slot weighted by 70% audience overlap'). Free-form prose for context that doesn't fit the structured measurement-vendor surface. Canonical methodology declarations belong on the measurement vendor's `get_adcp_capabilities.measurement.metrics[]` block where they're discoverable once and inherited across delivery rows; this field is for row-specific context (a particular daypart's calculation, a venue-mix exception) rather than the seller's general methodology." ), + NoExplicitNull, ] = None venue_breakdown: Annotated[ - list[VenueBreakdownItem] | None, Field(description='Per-venue performance breakdown') + list[VenueBreakdownItem] | None, + Field(description='Per-venue performance breakdown'), + NoExplicitNull, ] = None @@ -125,6 +138,7 @@ class Panel(AdCPBaseModel): Field( description="Human-readable location description (e.g., 'I-95 N of Exit 12, right-hand read')" ), + NoExplicitNull, ] = None @@ -176,6 +190,7 @@ class ViewedSecondsHistogramItem(AdCPBaseModel): description='Exclusive upper bound of this duration bucket, in seconds. MUST be greater than `lower_bound_seconds`. Omit only on the final bucket to represent an unbounded upper range.', ge=0.0, ), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt, @@ -198,6 +213,7 @@ class ByActionSourceItem(AdCPBaseModel): Field( description='Event source that produced these conversions (for disambiguation when multiple event sources are configured)' ), + NoExplicitNull, ] = None count: Annotated[ StrictFloat, Field(description='Number of conversions from this action source', ge=0.0) @@ -205,6 +221,7 @@ class ByActionSourceItem(AdCPBaseModel): value: Annotated[ StrictFloat | None, Field(description='Total monetary value of conversions from this action source', ge=0.0), + NoExplicitNull, ] = None @@ -222,10 +239,13 @@ class ByEventTypeItem(AdCPBaseModel): Field( description='Event source that produced these conversions (for disambiguation when multiple event sources are configured)' ), + NoExplicitNull, ] = None count: Annotated[StrictFloat, Field(description='Number of events of this type', ge=0.0)] value: Annotated[ - StrictFloat | None, Field(description='Total monetary value of events of this type', ge=0.0) + StrictFloat | None, + Field(description='Total monetary value of events of this type', ge=0.0), + NoExplicitNull, ] = None @@ -244,6 +264,7 @@ class ReachWindow(AdCPBaseModel): Field( description='Duration of the measurement window. REQUIRED when `kind` is `period` or `rolling` — declares the snapshot length (e.g., `{"interval": 1, "unit": "days"}` for a daily snapshot) or the trailing-window length (e.g., `{"interval": 7, "unit": "days"}` for trailing-7-day rolling reach). When `kind` is `cumulative`, this field is implicit (campaign-to-date) and SHOULD be omitted.' ), + NoExplicitNull, ] = None @@ -273,6 +294,7 @@ class TimeBasedView(AdCPBaseModel): Field( description="Viewability standard governing the in-view clock for this entry. RECOMMENDED when basis is 'in_view' (MRC and GroupM thresholds differ); not applicable to play_time entries." ), + NoExplicitNull, ] = None @@ -288,6 +310,7 @@ class Posting(AdCPBaseModel): Field( description='What the record attests: initial posting, rotary rotation to a new face, repair/reposting after damage, or removal at flight end' ), + NoExplicitNull, ] = EventType.posted occurred_at: Annotated[ date, @@ -300,6 +323,7 @@ class Posting(AdCPBaseModel): Field( description='Evidence artifact for this event (completion photograph); capture time and location ride the artifact' ), + NoExplicitNull, ] = None @@ -312,24 +336,31 @@ class OohMetrics(AdCPBaseModel): Field( description="Panels (faces) covered by this row. A panel commonly carries multiple identifiers at once — OOH contracts key line items on the measurement-currency panel number AND the operator's own panel number together." ), + NoExplicitNull, ] = None posting_period_start: Annotated[ - date | None, Field(description='First in-charge date of the posting period this row covers') + date | None, + Field(description='First in-charge date of the posting period this row covers'), + NoExplicitNull, ] = None posting_period_end: Annotated[ - date | None, Field(description='Last date of the posting period this row covers') + date | None, + Field(description='Last date of the posting period this row covers'), + NoExplicitNull, ] = None average_posted_date: Annotated[ date | None, Field( description="Average actual posting date across the row's units — the date the display term runs from under OAAA Bulletin §2.2 / Poster §3.2(a) when materials were timely. Read with materials_timely to know whether that rule was in force." ), + NoExplicitNull, ] = None materials_timely: Annotated[ StrictBool | None, Field( description="Seller's assertion that the buyer delivered acceptable materials by the contractual deadline, which determines whether the §2.2 posting-completion rule (term runs from average posting date) applied. An assertion about the buyer's delivery, not a derived value — it is not recomputable from postings[] evidence, and is the one field in this block that isn't checkable against it." ), + NoExplicitNull, ] = None share_of_voice_contracted: Annotated[ StrictFloat | None, @@ -338,6 +369,7 @@ class OohMetrics(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None illuminated_hours: Annotated[ StrictFloat | None, @@ -346,6 +378,7 @@ class OohMetrics(AdCPBaseModel): ge=0.0, le=24.0, ), + NoExplicitNull, ] = None estimated_impressions: Annotated[ SchemaInt | None, @@ -353,24 +386,28 @@ class OohMetrics(AdCPBaseModel): description="Modeled audience impressions for the panels and period in this row. This is the channel's delivery number — there is no event-counted alternative. The methodology tier MUST be declared in estimation_basis; provider identity is declared in the row-level measurement_source; the billing vendor is declared in measurement_terms.billing_measurement. The row's top-level impressions SHOULD carry the same value so cross-channel aggregation works without channel-specific logic.", ge=0, ), + NoExplicitNull, ] = None estimation_basis: Annotated[ EstimationBasis | None, Field( description="Methodology tier of estimated_impressions: currency_measured (an audience measurement currency's estimate for the panel and period — Geopath, Route, MOVE, COMMB — with the provider named in the row-level measurement_source) or seller_modeled (the seller's own model — the honest fallback for markets without a measurement currency). Tier, not provider: currencies change over time, so provider identity is data on measurement_source, not values in this enum." ), + NoExplicitNull, ] = None postings: Annotated[ list[Posting] | None, Field( description='Posting records — the seller-attested settlement artifact proving each panel was posted for the period. Industry convention (OAAA model contracts, Proof of Performance §3.5): photo evidence per bulletin within five calendar days of posting and again after each rotary rotation; one representative close-up photograph per creative design variation for poster showings (no deadline attached). Distinct from the §2.2 posting-completion obligation (posting within five business days of the scheduled date), which governs average_posted_date, not evidence.' ), + NoExplicitNull, ] = None calculation_notes: Annotated[ str | None, Field( description="Row-specific methodology context that doesn't fit the structured fields (e.g., a partial-period proration or a market-specific estimate adjustment). Same role as dooh_metrics.calculation_notes — canonical methodology declarations belong on the measurement vendor's discoverable surfaces, not here." ), + NoExplicitNull, ] = None @@ -383,6 +420,7 @@ class Viewability1(AdCPBaseModel): Field( description="Vendor that produced these viewability values. Optional but RECOMMENDED so the row is self-describing — buyer agents reading delivery in isolation can attribute the numbers without joining back to `package.committed_metrics` or `package.performance_standards`. The vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; the metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Same shape as `vendor_metric_value.vendor` for symmetry across vendor-attested surfaces." ), + NoExplicitNull, ] = None measurable_impressions: Annotated[ StrictFloat | None, @@ -390,6 +428,7 @@ class Viewability1(AdCPBaseModel): description='Impressions where viewability could be measured. Excludes environments without measurement capability (e.g., non-Intersection Observer browsers, certain app environments). Coverage denominator for `viewable_rate`, `viewed_seconds`, and both viewed-seconds distributions — every duration statistic is computed over this same measurable population.', ge=0.0, ), + NoExplicitNull, ] = None viewable_impressions: Annotated[ StrictFloat | None, @@ -397,6 +436,7 @@ class Viewability1(AdCPBaseModel): description='Impressions that met the viewability threshold defined by the measurement standard.', ge=0.0, ), + NoExplicitNull, ] = None viewable_rate: Annotated[ StrictFloat | None, @@ -405,6 +445,7 @@ class Viewability1(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None viewed_seconds: Annotated[ StrictFloat | None, @@ -412,12 +453,14 @@ class Viewability1(AdCPBaseModel): description="Average in-view duration per measurable impression, in seconds. Reporting-side counterpart to the `viewed_seconds` optimization metric in `optimization-goal.json`. Computed over `measurable_impressions`, not total impressions — the same denominator as `viewable_rate`. The viewability `standard` governs the threshold (e.g., MRC's 50% pixels for 1s display / 2s video) that defines when an impression is in view and therefore when the clock is running. Sellers reporting against a `viewed_seconds` optimization goal MUST populate this field.", ge=0.0, ), + NoExplicitNull, ] = None viewed_seconds_percentiles: Annotated[ ViewedSecondsPercentiles | None, Field( description='Percentile summary of the per-impression in-view durations whose arithmetic mean is reported in `viewed_seconds`. This object MUST use the same reporting row, measurement vendor, viewability `standard`, and `measurable_impressions` population as `viewed_seconds`; sellers MUST omit it when `measurable_impressions` is zero. Percentiles use the nearest-rank definition: sort the N observed durations in ascending order and select rank `ceil(p × N)` (one-based) for percentile p. Values MUST be non-decreasing from p25 through p95. The structured metric identity `viewed_seconds_percentiles` makes this optional surface discoverable and requestable; it is not sortable and the nested object remains the canonical carrier.' ), + NoExplicitNull, ] = None viewed_seconds_histogram: Annotated[ list[ViewedSecondsHistogramItem] | None, @@ -425,12 +468,14 @@ class Viewability1(AdCPBaseModel): description='Bucketed counts of the per-impression in-view durations whose arithmetic mean is reported in `viewed_seconds`. Buckets MUST be ordered by ascending lower bound, MUST NOT overlap, and MUST partition every impression in the same `measurable_impressions` population exactly once; therefore the sum of `impressions` MUST equal `measurable_impressions`. Buckets need not be contiguous: a gap between consecutive bucket boundaries is permitted when no impressions fall within that range — the sum constraint enforces this implicitly, and validators MUST NOT independently require contiguity. Each bucket is half-open `[lower_bound_seconds, upper_bound_seconds)`; only the final bucket MAY omit `upper_bound_seconds`, representing an unbounded upper range. Sellers choose boundaries, but buyers MUST combine histograms only when the complete boundary sequence, measurement vendor, and viewability `standard` match. The structured metric identity `viewed_seconds_histogram` makes this optional surface discoverable and requestable; it is not sortable and this nested array remains the canonical carrier.', min_length=1, ), + NoExplicitNull, ] = None standard: Annotated[ viewability_standard.ViewabilityStandard | None, Field( description='Viewability measurement standard applied to these metrics. Governs the in-view threshold for `viewable_rate`, `viewed_seconds`, and both viewed-seconds distributions.' ), + NoExplicitNull, ] = None @@ -443,13 +488,18 @@ class DeliveryMetrics(AdCPBaseModel): extra='allow', ) impressions: Annotated[ - StrictFloat | None, Field(description='Impressions delivered', ge=0.0) + StrictFloat | None, Field(description='Impressions delivered', ge=0.0), NoExplicitNull + ] = None + spend: Annotated[ + StrictFloat | None, Field(description='Amount spent', ge=0.0), NoExplicitNull + ] = None + clicks: Annotated[ + StrictFloat | None, Field(description='Total clicks', ge=0.0), NoExplicitNull ] = None - spend: Annotated[StrictFloat | None, Field(description='Amount spent', ge=0.0)] = None - clicks: Annotated[StrictFloat | None, Field(description='Total clicks', ge=0.0)] = None ctr: Annotated[ StrictFloat | None, Field(description='Click-through rate (clicks/impressions)', ge=0.0, le=1.0), + NoExplicitNull, ] = None views: Annotated[ StrictFloat | None, @@ -457,6 +507,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Content engagements counted toward the billable view threshold. For video this is a platform-defined view event (e.g., 30 seconds or video midpoint); for audio/podcast it is a stream start; for other formats it follows the pricing model's view definition. When the package uses CPV pricing, spend = views × rate.", ge=0.0, ), + NoExplicitNull, ] = None completed_views: Annotated[ StrictFloat | None, @@ -464,6 +515,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Video/audio completions. When the package has a completed_views optimization goal with view_duration_seconds, completions are counted at that threshold rather than 100% completion.', ge=0.0, ), + NoExplicitNull, ] = None completion_rate: Annotated[ StrictFloat | None, @@ -479,6 +531,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Total conversions attributed to this delivery. When by_event_type is present, this equals the sum of all by_event_type[].count entries.', ge=0.0, ), + NoExplicitNull, ] = None conversion_value: Annotated[ StrictFloat | None, @@ -486,6 +539,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Total monetary value of attributed conversions (in the reporting currency)', ge=0.0, ), + NoExplicitNull, ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -493,13 +547,17 @@ class DeliveryMetrics(AdCPBaseModel): description='Settled portion of attributed conversion value eligible for revenue-share commission, in the reporting currency. For revenue_share pricing, spend = round_currency(commissionable_value × commission_rate). This is distinct from conversion_value because taxes, shipping, discounts, returns, cancellations, or ineligible items may be excluded under the agreed commission basis.', ge=0.0, ), + NoExplicitNull, ] = None roas: Annotated[ StrictFloat | None, Field(description='Return on ad spend (conversion_value / spend)', ge=0.0), + NoExplicitNull, ] = None cost_per_acquisition: Annotated[ - StrictFloat | None, Field(description='Cost per conversion (spend / conversions)', ge=0.0) + StrictFloat | None, + Field(description='Cost per conversion (spend / conversions)', ge=0.0), + NoExplicitNull, ] = None new_to_brand_rate: Annotated[ StrictFloat | None, @@ -508,6 +566,7 @@ class DeliveryMetrics(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None leads: Annotated[ StrictFloat | None, @@ -515,6 +574,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Leads generated (convenience alias for by_event_type where event_type='lead')", ge=0.0, ), + NoExplicitNull, ] = None incremental_sales_lift: Annotated[ StrictFloat | None, @@ -522,6 +582,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Incremental sales lift attributed to the campaign — sales above the control/holdout baseline. Reported as a fraction (0.15 = 15% lift) or as an absolute value depending on seller convention. The seller's `attribution_methodology` qualifier (typically `deterministic_purchase` or `modeled`) and `attribution_window` qualifier on the matching `committed_metrics` entry disambiguate the methodology and window.", ge=0.0, ), + NoExplicitNull, ] = None brand_lift: Annotated[ StrictFloat | None, @@ -529,6 +590,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Brand lift — measured change in a brand metric (awareness, consideration, favorability, purchase intent, or ad recall) attributed to the campaign. Typically panel-based or survey-based. Reported as a fraction (0.05 = 5% lift). **Multidimensional in production** — Kantar, Upwave, Cint, DV all report each dimension separately with its own sample size and confidence interval. The dimension flows through `qualifier.lift_dimension` on `committed_metrics` / `by_package[].metric_values` (`awareness` | `consideration` | `favorability` | `purchase_intent` | `ad_recall`); rows under different dimensions are different surveyed outcomes and must not be combined. Use `attribution_methodology: 'panel_based'` qualifier when the underlying methodology is a panel.", ge=0.0, ), + NoExplicitNull, ] = None foot_traffic: Annotated[ StrictFloat | None, @@ -536,6 +598,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Store visits attributed to ad exposure. Count of incremental visits over baseline. Typically uses location-data panel methodology (`attribution_methodology: 'panel_based'`) or deterministic loyalty-card match (`attribution_methodology: 'deterministic_purchase'`).", ge=0.0, ), + NoExplicitNull, ] = None conversion_lift: Annotated[ StrictFloat | None, @@ -543,6 +606,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Incremental conversions attributed to the campaign — conversions above the control/holdout baseline. Reported as a fraction (0.10 = 10% lift) or as an absolute count depending on seller convention. Distinct from `conversions` (raw count of attributed conversions); conversion_lift requires a control group and an incrementality methodology.', ge=0.0, ), + NoExplicitNull, ] = None brand_search_lift: Annotated[ StrictFloat | None, @@ -550,6 +614,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Lift in brand search query volume attributed to the campaign — measured via search-data partnerships (Google, Microsoft) or survey methodology. Reported as a fraction (0.20 = 20% lift in branded search).', ge=0.0, ), + NoExplicitNull, ] = None plays: Annotated[ StrictFloat | None, @@ -557,6 +622,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Number of times the ad creative was displayed or played on DOOH or broadcast inventory. Raw play count before any impression multiplier is applied. Mirrors `forecastable-metric.json`'s `plays` token for forecast↔delivery reconciliation. Distinct from `dooh_metrics.loop_plays` (scheduled-rotation count) and from `impressions` (multiplied audience figure).", ge=0.0, ), + NoExplicitNull, ] = None measurement_source: Annotated[ str | None, @@ -580,15 +646,19 @@ class DeliveryMetrics(AdCPBaseModel): max_length=64, pattern='^[a-z0-9_]+$', ), + NoExplicitNull, ] = None by_event_type: Annotated[ list[ByEventTypeItem] | None, Field( description='Conversion metrics broken down by event type. Spend-derived metrics (ROAS, CPA) are only available at the package/totals level since spend cannot be attributed to individual event types.' ), + NoExplicitNull, ] = None grps: Annotated[ - StrictFloat | None, Field(description='Gross Rating Points delivered (for CPP)', ge=0.0) + StrictFloat | None, + Field(description='Gross Rating Points delivered (for CPP)', ge=0.0), + NoExplicitNull, ] = None reach: Annotated[ StrictFloat | None, @@ -596,18 +666,21 @@ class DeliveryMetrics(AdCPBaseModel): description='Unique reach in the units specified by reach_unit. When reach_unit is omitted, units are unspecified — do not compare reach values across packages or media buys without a common reach_unit. The measurement window for this value is declared in `reach_window`; when `reach_window` is omitted, the window is unspecified and buyers MUST NOT sum reach across reports (the value MAY be a daily snapshot, a cumulative total, or something else).', ge=0.0, ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description='Unit of measurement for the reach field. Aligns with the reach_unit declared on optimization goals and delivery forecasts. Required when reach is present to enable cross-platform comparison.' ), + NoExplicitNull, ] = None reach_window: Annotated[ ReachWindow | None, Field( description='Measurement window for the reported `reach` and `frequency` values in this row. Declares whether the values are a per-period snapshot, a trailing rolling window, or cumulative-to-date — without this declaration, buyers summing `reach` across rows (e.g., daily delivery reports) can silently double-count audiences. Sellers SHOULD populate this whenever `reach` is present.' ), + NoExplicitNull, ] = None frequency: Annotated[ StrictFloat | None, @@ -615,6 +688,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Average frequency per reach unit, measured over the window declared in `reach_window`. When `reach_unit` is 'households', this is average exposures per household; when 'accounts', per logged-in account; etc. When `reach_window` is omitted, the window is unspecified — buyers MUST NOT compare or average frequency values across rows.", ge=0.0, ), + NoExplicitNull, ] = None quartile_data: Annotated[ QuartileData | None, @@ -627,22 +701,26 @@ class DeliveryMetrics(AdCPBaseModel): Field( description="Time-threshold video view counts. Each entry reports views that met a continuous duration threshold under a stated basis, rather than a completion percentage (percentage-based completion is quartile_data). Thresholds of 2 and 6 seconds are RECOMMENDED cross-platform reporting points; any seller-defined threshold is permitted. One entry per (threshold_seconds, basis) pair per reporting period — sellers MUST de-duplicate before emission and MUST NOT emit the same pair twice; buyers MAY treat duplicate pairs as a seller-side conformance bug. Entries under different bases are different metrics and MUST NOT be summed (see view-threshold-basis). Primarily an autoplay/skippable-video metric (social, olv, in-feed video); completion metrics remain the currency for lean-back CTV/cinema inventory. Distinct from `views` (the single billable-threshold scalar) and from `viewability.viewed_seconds` (average in-view duration, not a threshold count). Array entries are not individually sortable in breakdown sort_by. Disclosure-grade surface: (threshold_seconds, basis) is not part of the committed-metric qualifier vocabulary, so a `committed_metrics` entry for `time_based_views` contracts the array's presence, not specific thresholds." ), + NoExplicitNull, ] = None dooh_metrics: Annotated[ DoohMetrics1 | None, Field(description='DOOH-specific metrics (only included for DOOH campaigns)'), + NoExplicitNull, ] = None ooh_metrics: Annotated[ OohMetrics | None, Field( description='Classic (static) OOH metrics — printed bulletins, posters, transit, and street furniture (only included for ooh campaigns). Experimental in AdCP 3.2. Static units have no play event: the delivery number is a period-level modeled audience estimate whose methodology tier is declared in estimation_basis (provider identity rides the row-level measurement_source), and the settlement artifact is the posting record — it proves the posting period, not an airing.' ), + NoExplicitNull, ] = None viewability: Annotated[ Viewability1 | None, Field( description="Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — plus optional percentile and histogram distributions over that duration; all three use the same `measurable_impressions` population and are governed by the same viewability threshold (`standard`). Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. The numeric leaves are addressable via the leaf metric identities `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` for declaration, commitments, aggregates, and breakdown sorting. The structured distribution carriers require explicit `viewed_seconds_percentiles` and `viewed_seconds_histogram` identities for declaration, commitment, and selection; they are not numeric aggregate rows or sort keys. This object remains the canonical carrier of every value. When a buy reports under more than one standard, contract a specific standard via the `viewability_standard` qualifier on `committed_metrics`; when the package's `committed_metrics` carry a `viewability_standard` qualifier, sellers MUST populate `standard` on reported viewability objects so reconciliation can match the qualifier." ), + NoExplicitNull, ] = None engagements: Annotated[ StrictFloat | None, @@ -650,6 +728,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Total engagements — direct interactions with the ad beyond viewing. Includes social reactions/comments/shares, story/unit opens, interactive overlay taps on CTV, companion banner interactions on audio. Platform-specific; corresponds to the 'engagements' optimization metric. Maps to DBCFM KPI_INTERACTIONS (Interaktionen) in the Reporting/Performance block.", ge=0.0, ), + NoExplicitNull, ] = None follows: Annotated[ StrictFloat | None, @@ -657,12 +736,14 @@ class DeliveryMetrics(AdCPBaseModel): description='New followers, page likes, artist/podcast/channel follows, or free channel/feed subscribes attributed to this delivery. Paid subscriptions are conversion events with `event_type: subscribe`, not `follows`.', ge=0.0, ), + NoExplicitNull, ] = None saves: Annotated[ StrictFloat | None, Field( description='Saves, bookmarks, playlist adds, pins attributed to this delivery.', ge=0.0 ), + NoExplicitNull, ] = None profile_visits: Annotated[ StrictFloat | None, @@ -670,6 +751,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Visits to the brand's in-platform page (profile, artist page, channel, or storefront) attributed to this delivery. Does not include external website clicks.", ge=0.0, ), + NoExplicitNull, ] = None engagement_rate: Annotated[ StrictFloat | None, @@ -678,9 +760,12 @@ class DeliveryMetrics(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None cost_per_click: Annotated[ - StrictFloat | None, Field(description='Cost per click (spend / clicks)', ge=0.0) + StrictFloat | None, + Field(description='Cost per click (spend / clicks)', ge=0.0), + NoExplicitNull, ] = None cost_per_completed_view: Annotated[ StrictFloat | None, @@ -688,6 +773,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Cost per completed view (spend / completed_views). Primary CPCV pricing scalar for video/audio inventory; the package's `pricing_model` is `cpcv` when this field is the billing basis.", ge=0.0, ), + NoExplicitNull, ] = None cpm: Annotated[ StrictFloat | None, @@ -695,6 +781,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Cost per thousand impressions, computed as (spend / impressions) × 1000. Universal pricing scalar across CTV, display, mobile/web video, native, audio, and DOOH inventory; the package's `pricing_model` is `cpm` when this field is the billing basis. Field name aligns with the canonical `cpm` token in `enums/pricing-model.json` and `pricing-options/cpm-option.json` so buyers cross-walk pricing model → reported scalar without a translation table.", ge=0.0, ), + NoExplicitNull, ] = None downloads: Annotated[ StrictFloat | None, @@ -702,6 +789,7 @@ class DeliveryMetrics(AdCPBaseModel): description="Audio/podcast downloads (IAB Podcast Measurement Technical Guidelines 2.x methodology). Distinct from `views` — for podcast inventory this is the count of podcast episode downloads; for streaming audio it is the count of stream starts that meet the platform's download threshold. Prefer this over `views` for audio inventory.", ge=0.0, ), + NoExplicitNull, ] = None units_sold: Annotated[ StrictFloat | None, @@ -709,6 +797,7 @@ class DeliveryMetrics(AdCPBaseModel): description='Items sold attributed to this delivery. Retail-media scalar distinct from `conversions` — a single conversion (transaction) may carry multiple `units_sold`. Used by retail media platforms where the buyer optimizes against unit movement, not transaction count. Attribution lookback windows are platform-specific (commonly 7/14/30 days, view-through and click-through variants); sellers SHOULD declare the window via `reporting_capabilities.measurement_windows` or `measurement_terms` rather than encoding it in this scalar.', ge=0.0, ), + NoExplicitNull, ] = None new_to_brand_units: Annotated[ StrictFloat | None, @@ -716,16 +805,19 @@ class DeliveryMetrics(AdCPBaseModel): description='Units sold to first-time brand buyers (count, not rate). Retail-media scalar — the unit-volume parallel to the conversion-fraction `new_to_brand_rate`. Used by retail media platforms where new-customer acquisition unit volume is a primary KPI. Same attribution-window note as `units_sold` applies.', ge=0.0, ), + NoExplicitNull, ] = None by_action_source: Annotated[ list[ByActionSourceItem] | None, Field( description='Conversion metrics broken down by action source (website, app, in_store, etc.). Useful for omnichannel sellers where conversions occur across digital and physical channels.' ), + NoExplicitNull, ] = None vendor_metric_values: Annotated[ list[vendor_metric_value.VendorMetricValue] | None, Field( description="Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared. Each entry carries the vendor (BrandRef), the metric identifier within the vendor's vocabulary, the value, optional unit, and `measurable_impressions` as the coverage denominator — vendor measurement is rarely 100% of delivered impressions, since vendors only score impressions where their SDK fires or their panel matches. When a declared vendor metric is omitted from this array, buyers infer no measurement happened (no integration). One row per `(vendor.domain, vendor.brand_id, metric_id, qualifier)` per reporting period — the same vendor metric MAY appear in multiple rows only when each carries a distinct qualifier (e.g., 7-day and 30-day attribution windows); sellers MUST de-duplicate before emission and MUST NOT emit two rows with the same tuple; buyers MAY treat duplicate rows as a seller-side conformance bug. The structured `vendor_metric_values` array is the recommended path for vendor metrics; `additionalProperties: true` on this parent object is preserved so existing free-form vendor emissions remain conformant during migration." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/delivery_recipient.py b/src/adcp/types/domains/core/delivery_recipient.py index 4eadba187..214ff7a15 100644 --- a/src/adcp/types/domains/core/delivery_recipient.py +++ b/src/adcp/types/domains/core/delivery_recipient.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/delivery_recipient.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import delivery_recipient_cloud @@ -17,5 +17,5 @@ class DeliveryRecipient(AdCPBaseModel): extra='forbid', ) identity: Annotated[str, Field(max_length=512, min_length=1)] - cloud: delivery_recipient_cloud.DeliveryRecipientCloud | None = None - region: Annotated[str | None, Field(max_length=128, min_length=1)] = None + cloud: Annotated[delivery_recipient_cloud.DeliveryRecipientCloud | None, NoExplicitNull] = None + region: Annotated[str | None, Field(max_length=128, min_length=1), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/demographic_age_range.py b/src/adcp/types/domains/core/demographic_age_range.py index 80b37faaa..bd4964024 100644 --- a/src/adcp/types/domains/core/demographic_age_range.py +++ b/src/adcp/types/domains/core/demographic_age_range.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/demographic_age_range.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool, model_validator @@ -24,6 +24,7 @@ class DemographicAgeRange(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None max: Annotated[ SchemaInt | None, @@ -32,6 +33,7 @@ class DemographicAgeRange(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None include_unknown: Annotated[ StrictBool, diff --git a/src/adcp/types/domains/core/demographic_reporting_capability.py b/src/adcp/types/domains/core/demographic_reporting_capability.py index 259f9c477..12db9e008 100644 --- a/src/adcp/types/domains/core/demographic_reporting_capability.py +++ b/src/adcp/types/domains/core/demographic_reporting_capability.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/demographic_reporting_capability.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool, model_validator from ..enums import demographic_system as demographic_system_1 @@ -34,8 +34,11 @@ class Interval(AdCPBaseModel): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None - demographic_system: demographic_system_1.DemographicSystem | None = None + demographic_system: Annotated[demographic_system_1.DemographicSystem | None, NoExplicitNull] = ( + None + ) class Age(AdCPBaseModel): @@ -56,6 +59,7 @@ class Age(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None max_reportable_age: Annotated[ SchemaInt | None, @@ -64,18 +68,21 @@ class Age(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None supports_unbounded_min: Annotated[ StrictBool | None, Field( description='Whether exact_predicates accepts a canonical age predicate with min omitted. Required when that mode is declared.' ), + NoExplicitNull, ] = None supports_unbounded_max: Annotated[ StrictBool | None, Field( description='Whether exact_predicates accepts a canonical age predicate with max omitted. Required when that mode is declared.' ), + NoExplicitNull, ] = None intervals: Annotated[ list[Interval] | None, @@ -83,6 +90,7 @@ class Age(AdCPBaseModel): description='Fixed reportable age intervals for enumerated_intervals. Bounds are authoritative capability data, not a protocol-wide bucket taxonomy. Optional native labels identify the measurement-system code returned with the interval.', min_length=1, ), + NoExplicitNull, ] = None @@ -91,7 +99,9 @@ class DemographicReportingCapability(AdCPBaseModel): extra='forbid', ) age: Annotated[ - Age | None, Field(description='Machine-comparable age ranges this product can report.') + Age | None, + Field(description='Machine-comparable age ranges this product can report.'), + NoExplicitNull, ] = None demographic_systems: Annotated[ list[demographic_system_1.DemographicSystem] | None, @@ -99,6 +109,7 @@ class DemographicReportingCapability(AdCPBaseModel): description='Measurement-system notations this product may return. A code remains opaque unless its capability interval and response row also carry a canonical age predicate.', min_length=1, ), + NoExplicitNull, ] = None may_suppress_small_cells: Annotated[ StrictBool, diff --git a/src/adcp/types/domains/core/demographic_targeting_capability.py b/src/adcp/types/domains/core/demographic_targeting_capability.py index ff4cd3a67..a1e0ad695 100644 --- a/src/adcp/types/domains/core/demographic_targeting_capability.py +++ b/src/adcp/types/domains/core/demographic_targeting_capability.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/demographic_targeting_capability.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ..enums import age_determination_basis, age_verification_method @@ -58,6 +58,7 @@ class Age(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None max_supported_age: Annotated[ SchemaInt | None, @@ -66,18 +67,21 @@ class Age(AdCPBaseModel): ge=0, le=150, ), + NoExplicitNull, ] = None supports_unbounded_min: Annotated[ StrictBool | None, Field( description='Whether continuous_bounds accepts a predicate with min omitted. When true, the omitted min means no lower age restriction and MUST NOT be replaced by min_supported_age. When false, the seller MUST reject open-lower predicates. Required when continuous_bounds is declared.' ), + NoExplicitNull, ] = None supports_unbounded_max: Annotated[ StrictBool | None, Field( description='Whether continuous_bounds accepts a predicate with max omitted. When true, the omitted max means no upper age restriction and MUST NOT be replaced by max_supported_age; for example, min 65 remains 65+. When false, the seller MUST reject open-upper predicates. Required when continuous_bounds is declared.' ), + NoExplicitNull, ] = None unknown_handling: Annotated[ UnknownHandling, @@ -91,6 +95,7 @@ class Age(AdCPBaseModel): description='User-level age determination bases this product can use for exact execution. Sellers MUST publish only bases whose provenance they can preserve and audit; they MUST NOT infer a basis from an opaque platform age bucket. When omitted, the product makes no basis-aware targeting commitment and MUST reject requests that specify accepted_bases.', min_length=1, ), + NoExplicitNull, ] = None supported_verification_methods: Annotated[ list[age_verification_method.AgeVerificationMethod] | None, @@ -98,6 +103,7 @@ class Age(AdCPBaseModel): description='Age verification methods available when supported_bases contains verified. This is an availability declaration only: a method must still establish the entire requested age predicate. Threshold-only methods such as World ID cannot establish an arbitrary bounded range.', min_length=1, ), + NoExplicitNull, ] = None intervals: Annotated[ list[Interval] | None, @@ -105,6 +111,7 @@ class Age(AdCPBaseModel): description='Seller-owned age intervals available to enumerated_intervals execution. Bounds are authoritative capability data, not a protocol-wide bucket taxonomy. Exact compilation may union adjacent intervals but MUST NOT introduce gaps or extra ages.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/demographic_targeting_intent.py b/src/adcp/types/domains/core/demographic_targeting_intent.py index cc75e5c63..fc8b6eebb 100644 --- a/src/adcp/types/domains/core/demographic_targeting_intent.py +++ b/src/adcp/types/domains/core/demographic_targeting_intent.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/demographic_targeting_intent.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import age_determination_basis, age_verification_method @@ -20,6 +20,7 @@ class Age(DemographicAgeRange): description='User-level age determination bases the buyer permits for this predicate. Sellers MUST use only these bases or reject the request; they MUST NOT silently fall back to another basis. When omitted, the buyer imposes no determination-basis constraint beyond product capability and any age_restriction. population_estimate is structurally unavailable because aggregate evidence cannot identify an eligible user.', min_length=1, ), + NoExplicitNull, ] = None accepted_verification_methods: Annotated[ list[age_verification_method.AgeVerificationMethod] | None, @@ -27,6 +28,7 @@ class Age(DemographicAgeRange): description='Verification methods acceptable when verified is an accepted basis. This field narrows verified demographic execution; age_restriction.accepted_methods independently governs legal eligibility and the two sets are intersected when both are present.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/demographic_targeting_resolution.py b/src/adcp/types/domains/core/demographic_targeting_resolution.py index 20bc8cbdb..a853c0e7a 100644 --- a/src/adcp/types/domains/core/demographic_targeting_resolution.py +++ b/src/adcp/types/domains/core/demographic_targeting_resolution.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/demographic_targeting_resolution.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -26,7 +26,7 @@ class Execution(AdCPBaseModel): extra='allow', ) type: Literal['continuous_bounds'] = 'continuous_bounds' - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Execution1(AdCPBaseModel): @@ -41,7 +41,7 @@ class Execution1(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Execution2(AdCPBaseModel): @@ -56,7 +56,7 @@ class Execution2(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class DemographicTargetingResolution(AdCPBaseModel): @@ -86,6 +86,7 @@ class DemographicTargetingResolution(AdCPBaseModel): description='Effective user-level determination bases configured for this package after intersecting buyer accepted_bases, product supported_bases, and age_restriction. This is an auditable configuration readback, not proof that every impression used every listed basis.', min_length=1, ), + NoExplicitNull, ] = None applied_verification_methods: Annotated[ list[age_verification_method.AgeVerificationMethod] | None, @@ -93,5 +94,6 @@ class DemographicTargetingResolution(AdCPBaseModel): description='Effective verification methods when applied_bases contains verified. A method name does not establish that a particular proof or claim was valid; runtime verification and claim entailment remain required.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/deployment.py b/src/adcp/types/domains/core/deployment.py index 533d40d74..602f26c6d 100644 --- a/src/adcp/types/domains/core/deployment.py +++ b/src/adcp/types/domains/core/deployment.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/deployment.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from . import activation_key as activation_key_1 @@ -21,7 +21,9 @@ class Deployment1(AdCPBaseModel): Field(description='Discriminator indicating this is a platform-based deployment'), ] = 'platform' platform: Annotated[str, Field(description='Platform identifier for DSPs')] - account: Annotated[str | None, Field(description='Account identifier if applicable')] = None + account: Annotated[ + str | None, Field(description='Account identifier if applicable'), NoExplicitNull + ] = None is_live: Annotated[ StrictBool, Field(description='Whether signal is currently active on this deployment') ] @@ -30,6 +32,7 @@ class Deployment1(AdCPBaseModel): Field( description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.' ), + NoExplicitNull, ] = None estimated_activation_duration_minutes: Annotated[ StrictFloat | None, @@ -37,10 +40,12 @@ class Deployment1(AdCPBaseModel): description='Estimated time to activate if not live, or to complete activation if in progress', ge=0.0, ), + NoExplicitNull, ] = None deployed_at: Annotated[ AwareDatetime | None, Field(description='Timestamp when activation completed (if is_live=true)'), + NoExplicitNull, ] = None @@ -53,7 +58,9 @@ class Deployment2(AdCPBaseModel): Field(description='Discriminator indicating this is an agent URL-based deployment'), ] = 'agent' agent_url: Annotated[AnyUrl, Field(description='URL identifying the deployment agent')] - account: Annotated[str | None, Field(description='Account identifier if applicable')] = None + account: Annotated[ + str | None, Field(description='Account identifier if applicable'), NoExplicitNull + ] = None is_live: Annotated[ StrictBool, Field(description='Whether signal is currently active on this deployment') ] @@ -62,6 +69,7 @@ class Deployment2(AdCPBaseModel): Field( description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.' ), + NoExplicitNull, ] = None estimated_activation_duration_minutes: Annotated[ StrictFloat | None, @@ -69,10 +77,12 @@ class Deployment2(AdCPBaseModel): description='Estimated time to activate if not live, or to complete activation if in progress', ge=0.0, ), + NoExplicitNull, ] = None deployed_at: Annotated[ AwareDatetime | None, Field(description='Timestamp when activation completed (if is_live=true)'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/destination.py b/src/adcp/types/domains/core/destination.py index cbb8703fe..cfe638cf1 100644 --- a/src/adcp/types/domains/core/destination.py +++ b/src/adcp/types/domains/core/destination.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/destination.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -23,7 +23,7 @@ class Destination1(AdCPBaseModel): Field(description="Platform identifier for DSPs (e.g., 'the-trade-desk', 'amazon-dsp')"), ] account: Annotated[ - str | None, Field(description='Optional account identifier on the platform') + str | None, Field(description='Optional account identifier on the platform'), NoExplicitNull ] = None @@ -39,7 +39,7 @@ class Destination2(AdCPBaseModel): AnyUrl, Field(description='URL identifying the deployment agent (for sales agents, etc.)') ] account: Annotated[ - str | None, Field(description='Optional account identifier on the agent') + str | None, Field(description='Optional account identifier on the agent'), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/core/destination_item.py b/src/adcp/types/domains/core/destination_item.py index d7d8aa0eb..02919603a 100644 --- a/src/adcp/types/domains/core/destination_item.py +++ b/src/adcp/types/domains/core/destination_item.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/destination_item.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -49,27 +49,42 @@ class DestinationItem(AdCPBaseModel): description: Annotated[ str | None, Field(description='Destination description highlighting attractions and appeal.'), + NoExplicitNull, + ] = None + city: Annotated[str | None, Field(description='City name, if applicable.'), NoExplicitNull] = ( + None + ) + region: Annotated[ + str | None, Field(description='State, province, or region name.'), NoExplicitNull ] = None - city: Annotated[str | None, Field(description='City name, if applicable.')] = None - region: Annotated[str | None, Field(description='State, province, or region name.')] = None country: Annotated[ - str | None, Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$') + str | None, + Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$'), + NoExplicitNull, ] = None location: Annotated[ - Location | None, Field(description='Geographic coordinates of the destination.') + Location | None, + Field(description='Geographic coordinates of the destination.'), + NoExplicitNull, ] = None destination_type: Annotated[ - DestinationType | None, Field(description='Destination category.') + DestinationType | None, Field(description='Destination category.'), NoExplicitNull ] = None price: Annotated[ - price_1.Price | None, Field(description='Starting price for a trip to this destination.') + price_1.Price | None, + Field(description='Starting price for a trip to this destination.'), + NoExplicitNull, + ] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Destination hero image URL.'), NoExplicitNull + ] = None + url: Annotated[ + AnyUrl | None, Field(description='Destination landing page or booking URL.'), NoExplicitNull ] = None - image_url: Annotated[AnyUrl | None, Field(description='Destination hero image URL.')] = None - url: Annotated[AnyUrl | None, Field(description='Destination landing page or booking URL.')] = ( - None - ) rating: Annotated[ - StrictFloat | None, Field(description='Destination rating (1–5).', ge=1.0, le=5.0) + StrictFloat | None, + Field(description='Destination rating (1–5).', ge=1.0, le=5.0), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -77,6 +92,7 @@ class DestinationItem(AdCPBaseModel): description="Tags for filtering (e.g., 'family', 'romantic', 'solo', 'winter-sun').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -84,5 +100,6 @@ class DestinationItem(AdCPBaseModel): description="Typed creative asset pools for this destination. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (destination hero), 'images_vertical' (9:16 for Snap, Stories), 'images_square' (1:1). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/downstream_connection_requirement.py b/src/adcp/types/domains/core/downstream_connection_requirement.py index 8477377df..c3b37a119 100644 --- a/src/adcp/types/domains/core/downstream_connection_requirement.py +++ b/src/adcp/types/domains/core/downstream_connection_requirement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/downstream_connection_requirement.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -52,28 +52,36 @@ class ResourceRef(AdCPBaseModel): Field( description='Provider-native advertiser or business account id, when safe to disclose.' ), + NoExplicitNull, ] = None identity_id: Annotated[ str | None, Field( description='Provider-native creator, page, channel, organization, or profile id, when safe to disclose.' ), + NoExplicitNull, ] = None handle: Annotated[ str | None, Field(description='Provider-native public handle for the owning identity, when available.'), + NoExplicitNull, ] = None profile_url: Annotated[ - AnyUrl | None, Field(description='Public URL for the owning identity, when available.') + AnyUrl | None, + Field(description='Public URL for the owning identity, when available.'), + NoExplicitNull, ] = None post_id: Annotated[ str | None, Field( description='Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.' ), + NoExplicitNull, ] = None post_url: Annotated[ - AnyUrl | None, Field(description='Public URL for the referenced post, when available.') + AnyUrl | None, + Field(description='Public URL for the referenced post, when available.'), + NoExplicitNull, ] = None @@ -86,6 +94,7 @@ class DownstreamConnectionRequirement(AdCPBaseModel): Field( description='Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.' ), + NoExplicitNull, ] = None connection_type: Annotated[ ConnectionType, @@ -98,39 +107,48 @@ class DownstreamConnectionRequirement(AdCPBaseModel): Field( description='Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.' ), + NoExplicitNull, + ] = None + scope: Annotated[ + Scope | None, Field(description='Granularity of the downstream grant.'), NoExplicitNull ] = None - scope: Annotated[Scope | None, Field(description='Granularity of the downstream grant.')] = None status: Annotated[ Status | None, Field( description='Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.' ), + NoExplicitNull, ] = None connection_id: Annotated[ str | None, Field( description='Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.' ), + NoExplicitNull, ] = None resource_ref: Annotated[ ResourceRef | None, Field( description='Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.' ), + NoExplicitNull, ] = None authorization_url: Annotated[ AnyUrl | None, Field( description='Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.' ), + NoExplicitNull, ] = None authorization_instructions: Annotated[ str | None, Field( description='Human-readable instructions for completing or restoring this downstream connection.' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field(description='Expiration time for the downstream grant, when known.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/education_item.py b/src/adcp/types/domains/core/education_item.py index 8871d79a5..dcabeba08 100644 --- a/src/adcp/types/domains/core/education_item.py +++ b/src/adcp/types/domains/core/education_item.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/education_item.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from . import ext as ext_1 @@ -53,49 +53,62 @@ class EducationItem(AdCPBaseModel): description: Annotated[ str | None, Field(description='Program description including curriculum highlights and outcomes.'), + NoExplicitNull, ] = None subject: Annotated[ str | None, Field( description="Subject area or field of study (e.g., 'computer-science', 'business', 'healthcare')." ), + NoExplicitNull, + ] = None + degree_type: Annotated[ + DegreeType | None, Field(description='Type of credential awarded.'), NoExplicitNull + ] = None + level: Annotated[ + Level | None, Field(description='Difficulty or prerequisite level.'), NoExplicitNull + ] = None + price: Annotated[ + price_1.Price | None, Field(description='Tuition or course fee.'), NoExplicitNull ] = None - degree_type: Annotated[DegreeType | None, Field(description='Type of credential awarded.')] = ( - None - ) - level: Annotated[Level | None, Field(description='Difficulty or prerequisite level.')] = None - price: Annotated[price_1.Price | None, Field(description='Tuition or course fee.')] = None duration: Annotated[ str | None, Field( description="Program duration as a human-readable string (e.g., '4 weeks', '2 years', '6 months')." ), + NoExplicitNull, ] = None start_date: Annotated[ - date | None, Field(description='Next available start date (ISO 8601 date).') + date | None, Field(description='Next available start date (ISO 8601 date).'), NoExplicitNull ] = None language: Annotated[ - str | None, Field(description="Language of instruction (e.g., 'en', 'nl', 'es').") + str | None, + Field(description="Language of instruction (e.g., 'en', 'nl', 'es')."), + NoExplicitNull, ] = None - modality: Annotated[Modality | None, Field(description='Delivery format.')] = None + modality: Annotated[Modality | None, Field(description='Delivery format.'), NoExplicitNull] = ( + None + ) location: Annotated[ str | None, Field( description="Campus or instruction location (e.g., 'Amsterdam, NL'). Omit for fully online programs." ), + NoExplicitNull, + ] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Program or institution image URL.'), NoExplicitNull + ] = None + url: Annotated[ + AnyUrl | None, Field(description='Program landing page or enrollment URL.'), NoExplicitNull ] = None - image_url: Annotated[AnyUrl | None, Field(description='Program or institution image URL.')] = ( - None - ) - url: Annotated[AnyUrl | None, Field(description='Program landing page or enrollment URL.')] = ( - None - ) tags: Annotated[ list[str] | None, Field( description="Tags for filtering (e.g., 'stem', 'scholarship-available', 'evening-classes').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -103,5 +116,6 @@ class EducationItem(AdCPBaseModel): description="Typed creative asset pools for this program. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (campus/program hero), 'images_vertical' (9:16 for Stories), 'logo' (institution logo). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/error.py b/src/adcp/types/domains/core/error.py index 755bed423..273e1f98c 100644 --- a/src/adcp/types/domains/core/error.py +++ b/src/adcp/types/domains/core/error.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/error.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -75,18 +75,21 @@ class Issue(AdCPBaseModel): Field( description="Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." ), + NoExplicitNull, ] = None schema_id: Annotated[ str | None, Field( description="Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` — either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element — the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." ), + NoExplicitNull, ] = None discriminator: Annotated[ list[DiscriminatorItem] | None, Field( description="Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload — emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx." ), + NoExplicitNull, ] = None @@ -119,14 +122,18 @@ class Error(AdCPBaseModel): Field( description='Optional buyer-actionable classification of the failure. Use this when the enclosing error code or message is too coarse or contains producer-internal context that must not cross the buyer trust boundary. `code` reuses the comprehensive standard vocabulary and recovery classifications published in `enums/error-code.json`, including policy, governance, account, commercial, inventory, and creative failures. The wire field remains open for forward compatibility. `message` MUST be safe to show to the buyer and MUST NOT contain vendor identifiers, ad-server type names, internal object names, internal IDs, stack traces, or other producer-private implementation details. When present, the enclosing `error.recovery` MUST classify the buyer-actionable reason. If both the enclosing code and buyer reason are registered, their standard recovery classifications MUST agree with each other and with `error.recovery`; sellers MUST choose the closest compatible enclosing code rather than retain a conflicting upstream transport wrapper. One buyer_reason classifies one error object. If a rejection contains independently actionable failures from different classes, sellers MUST emit separate error objects or omit buyer_reason rather than select a misleading single class.' ), + NoExplicitNull, ] = None field: Annotated[ str | None, Field( description="Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' → 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." ), + NoExplicitNull, + ] = None + suggestion: Annotated[ + str | None, Field(description='Suggested fix for the error'), NoExplicitNull ] = None - suggestion: Annotated[str | None, Field(description='Suggested fix for the error')] = None retry_after: Annotated[ StrictFloat | None, Field( @@ -134,34 +141,40 @@ class Error(AdCPBaseModel): ge=1.0, le=3600.0, ), + NoExplicitNull, ] = None issues: Annotated[ list[Issue] | None, Field( description='Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only — translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` → `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers — the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.' ), + NoExplicitNull, ] = None details: Annotated[ dict[str, Any] | None, Field( description='Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n "code": "INVALID_PRICING_OPTION",\n "message": "Pricing option not found: po_prism_abandoner_cpm",\n "field": "pricing_option_id",\n "details": {\n "rejected_value": "po_prism_abandoner_cpm",\n "accepted_values": ["po_prism_cart_cpm", "po_prism_view_cpm"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what\'s accepted for *this caller in this context* (account, brand, scope) — leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.' ), + NoExplicitNull, ] = None recovery: Annotated[ Recovery | None, Field( description='Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). AdCP 3.2 producers MUST populate `recovery` on every error; 3.1 producers SHOULD populate it. The shared 3.x schema intentionally does not add `recovery` to `required` so retained and live errors from earlier 3.x producers remain decodable. When `buyer_reason` is present, `recovery` is required and MUST classify that buyer-actionable reason. If the enclosing code and buyer reason are both registered, their standard recovery classifications MUST agree with each other and with this field. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. When a legacy error omits `recovery`, receivers use the registered classification for a known code and fall back to `transient` for an unknown code, subject to the bounded retry budget. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known top-level and buyer-reason codes; `error.recovery` on the wire is authoritative when present.' ), + NoExplicitNull, ] = None source: Annotated[ Source | None, Field( description='Who emitted this error entry. `producer` (default when absent): emitted by the response\'s authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn\'t project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network — responses commonly traverse multiple SDKs (e.g., sales agent → interchange → DSP → buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry\'s `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: "sdk"`) are authoritative for what the response\'s authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can\'t tell whether a code came from the seller or an intermediate SDK, which corrupts attribution.' ), + NoExplicitNull, ] = None sdk_id: Annotated[ str | None, Field( description='Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: "sdk"`; MUST be absent when `source: "producer"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK\'s registry version doesn\'t).' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/evaluator_spec.py b/src/adcp/types/domains/core/evaluator_spec.py index 7ad721736..e99bc95af 100644 --- a/src/adcp/types/domains/core/evaluator_spec.py +++ b/src/adcp/types/domains/core/evaluator_spec.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/evaluator_spec.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from ..content_standards import artifact @@ -35,6 +35,7 @@ class RankByItem(AdCPBaseModel): Field( description='Sort direction for this feature: `maximize` ranks higher feature values first (e.g. creative_quality_score), `minimize` ranks lower values first (e.g. predicted_cpa).' ), + NoExplicitNull, ] = Direction.maximize @@ -53,6 +54,7 @@ class FeatureAgent(AdCPBaseModel): Field( description="Optional canonical feature_id the producing agent SHOULD request against this agent. When present it SHOULD match the agent's `accepted_verifiers[].feature_id` or be omitted; when absent the seller selects a feature at evaluation time. Resolves selector ambiguity exactly as the provenance verify_agent.feature_id does." ), + NoExplicitNull, ] = None @@ -63,10 +65,12 @@ class EvalBudget(AdCPBaseModel): max_calls: Annotated[ SchemaInt | None, Field(description='Soft cap on the number of judge calls the evaluator should make.', ge=1), + NoExplicitNull, ] = None max_seconds: Annotated[ StrictFloat | None, Field(description='Soft cap on wall-clock seconds the evaluation should consume.', ge=0.0), + NoExplicitNull, ] = None @@ -88,6 +92,7 @@ class EvaluatorSpec2(AdCPBaseModel): description='Optional hard GATE over creative-feature values — the predicates a leaf MUST satisfy for the producing agent to recommend/return it. Reuses the feature-requirement shape (min_value/max_value for quantitative features like creative_quality_score, allowed_values for binary/categorical) — the same predicate vocabulary that gates property/audience filters, which its own schema names as an intended creative-gate reuse. A leaf that fails any predicate is DROPPED from the agent\'s best_of_n survivors before ranking — this is internal pruning of which leaves the agent recommends, not an AdCP-layer block of an already-produced billable leaf (what is produced/billed is governed by max_variants/max_creatives/max_spend). Distinct from `rank_by`: the gate is a pass/fail predicate (drop on fail), `rank_by` is an ordering over survivors. Each predicate\'s `if_not_covered` (exclude|include, default exclude) is the fail-open knob when the source cannot measure that feature. A pass/warn/fail verdict is expressed as a categorical string feature value gated via `allowed_values` (e.g. ["pass"] or ["pass","warn"]) — the buyer\'s predicate decides whether warn passes; the verdict is derived, never stored on creative-feature-result. Omit to leave evaluation advisory (no leaf is dropped).', min_length=1, ), + NoExplicitNull, ] = None rank_by: Annotated[ list[RankByItem1] | None, @@ -95,20 +100,23 @@ class EvaluatorSpec2(AdCPBaseModel): description='Optional RANK ordering over creative-feature values — an ordered list (most significant first) the agent uses to order the gate survivors into recommended/rank. An explicit {feature_id, direction} ordering rather than the feature-requirement predicate (which has no sort direction): the gate decides pass/fail, rank_by decides better/worse. Soft preference, never a gate: leaves are not dropped by rank_by, they are only ordered. Omit to let the evaluator/seller choose the ordering.', min_length=1, ), + NoExplicitNull, ] = None feature_agent: Annotated[ FeatureAgent | None, Field( description="Optional buyer-attached pointer to a get_creative_features-capable creative-feature / governance agent the producing agent calls to evaluate each leaf (the gate's SOURCE of feature values). This is the buyer-represents → seller-calls pattern #5280 established for provenance, generalized to the evaluator gate: the buyer REPRESENTS which agent it used, but the seller is the verifier-of-record and decides which agent it actually calls. `agent_url` MUST appear (canonicalized per /docs/reference/url-canonicalization) in the seller's `creative_policy.accepted_verifiers[].agent_url`; an off-list agent is rejected with `EVALUATOR_AGENT_NOT_ACCEPTED` (mirrors PROVENANCE_VERIFIER_NOT_ACCEPTED) before any outbound call. The outbound evaluator call authenticates on the transport; this pointer MUST NOT carry API keys, bearer tokens, client secrets, authorization values, JWKs, JWKS documents, or JWKS URIs. Reuses the same allowlist mechanism — no new allowlist is introduced. Distinct from the `agent_url` oneOf form, which names the evaluator's source directly; `feature_agent` attaches the gate's measurement source alongside any of the three forms." ), + NoExplicitNull, ] = None eval_budget: Annotated[ EvalBudget | None, Field( description='Optional soft ceiling on evaluation effort. Advisory in v1 with no billing coupling. Well-known soft fields max_calls / max_seconds; open for evaluator-specific knobs.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None evaluator_id: Annotated[ str, Field( @@ -127,6 +135,7 @@ class EvaluatorSpec3(AdCPBaseModel): description='Optional hard GATE over creative-feature values — the predicates a leaf MUST satisfy for the producing agent to recommend/return it. Reuses the feature-requirement shape (min_value/max_value for quantitative features like creative_quality_score, allowed_values for binary/categorical) — the same predicate vocabulary that gates property/audience filters, which its own schema names as an intended creative-gate reuse. A leaf that fails any predicate is DROPPED from the agent\'s best_of_n survivors before ranking — this is internal pruning of which leaves the agent recommends, not an AdCP-layer block of an already-produced billable leaf (what is produced/billed is governed by max_variants/max_creatives/max_spend). Distinct from `rank_by`: the gate is a pass/fail predicate (drop on fail), `rank_by` is an ordering over survivors. Each predicate\'s `if_not_covered` (exclude|include, default exclude) is the fail-open knob when the source cannot measure that feature. A pass/warn/fail verdict is expressed as a categorical string feature value gated via `allowed_values` (e.g. ["pass"] or ["pass","warn"]) — the buyer\'s predicate decides whether warn passes; the verdict is derived, never stored on creative-feature-result. Omit to leave evaluation advisory (no leaf is dropped).', min_length=1, ), + NoExplicitNull, ] = None rank_by: Annotated[ list[RankByItem2] | None, @@ -134,20 +143,23 @@ class EvaluatorSpec3(AdCPBaseModel): description='Optional RANK ordering over creative-feature values — an ordered list (most significant first) the agent uses to order the gate survivors into recommended/rank. An explicit {feature_id, direction} ordering rather than the feature-requirement predicate (which has no sort direction): the gate decides pass/fail, rank_by decides better/worse. Soft preference, never a gate: leaves are not dropped by rank_by, they are only ordered. Omit to let the evaluator/seller choose the ordering.', min_length=1, ), + NoExplicitNull, ] = None feature_agent: Annotated[ FeatureAgent | None, Field( description="Optional buyer-attached pointer to a get_creative_features-capable creative-feature / governance agent the producing agent calls to evaluate each leaf (the gate's SOURCE of feature values). This is the buyer-represents → seller-calls pattern #5280 established for provenance, generalized to the evaluator gate: the buyer REPRESENTS which agent it used, but the seller is the verifier-of-record and decides which agent it actually calls. `agent_url` MUST appear (canonicalized per /docs/reference/url-canonicalization) in the seller's `creative_policy.accepted_verifiers[].agent_url`; an off-list agent is rejected with `EVALUATOR_AGENT_NOT_ACCEPTED` (mirrors PROVENANCE_VERIFIER_NOT_ACCEPTED) before any outbound call. The outbound evaluator call authenticates on the transport; this pointer MUST NOT carry API keys, bearer tokens, client secrets, authorization values, JWKs, JWKS documents, or JWKS URIs. Reuses the same allowlist mechanism — no new allowlist is introduced. Distinct from the `agent_url` oneOf form, which names the evaluator's source directly; `feature_agent` attaches the gate's measurement source alongside any of the three forms." ), + NoExplicitNull, ] = None eval_budget: Annotated[ EvalBudget | None, Field( description='Optional soft ceiling on evaluation effort. Advisory in v1 with no billing coupling. Well-known soft fields max_calls / max_seconds; open for evaluator-specific knobs.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None agent_url: Annotated[ AnyUrl, Field( @@ -163,12 +175,14 @@ class Exemplars(AdCPBaseModel): alias='pass', description='Artifacts exemplifying variants the buyer considers good — the high end (≈1) of the calibrated prediction feature.', ), + NoExplicitNull, ] = None fail: Annotated[ list[artifact.Artifact] | None, Field( description='Artifacts exemplifying variants the buyer considers bad — the low end (≈0) of the calibrated prediction feature.' ), + NoExplicitNull, ] = None @@ -182,6 +196,7 @@ class EvaluatorSpec1(AdCPBaseModel): description='Optional hard GATE over creative-feature values — the predicates a leaf MUST satisfy for the producing agent to recommend/return it. Reuses the feature-requirement shape (min_value/max_value for quantitative features like creative_quality_score, allowed_values for binary/categorical) — the same predicate vocabulary that gates property/audience filters, which its own schema names as an intended creative-gate reuse. A leaf that fails any predicate is DROPPED from the agent\'s best_of_n survivors before ranking — this is internal pruning of which leaves the agent recommends, not an AdCP-layer block of an already-produced billable leaf (what is produced/billed is governed by max_variants/max_creatives/max_spend). Distinct from `rank_by`: the gate is a pass/fail predicate (drop on fail), `rank_by` is an ordering over survivors. Each predicate\'s `if_not_covered` (exclude|include, default exclude) is the fail-open knob when the source cannot measure that feature. A pass/warn/fail verdict is expressed as a categorical string feature value gated via `allowed_values` (e.g. ["pass"] or ["pass","warn"]) — the buyer\'s predicate decides whether warn passes; the verdict is derived, never stored on creative-feature-result. Omit to leave evaluation advisory (no leaf is dropped).', min_length=1, ), + NoExplicitNull, ] = None rank_by: Annotated[ list[RankByItem] | None, @@ -189,20 +204,23 @@ class EvaluatorSpec1(AdCPBaseModel): description='Optional RANK ordering over creative-feature values — an ordered list (most significant first) the agent uses to order the gate survivors into recommended/rank. An explicit {feature_id, direction} ordering rather than the feature-requirement predicate (which has no sort direction): the gate decides pass/fail, rank_by decides better/worse. Soft preference, never a gate: leaves are not dropped by rank_by, they are only ordered. Omit to let the evaluator/seller choose the ordering.', min_length=1, ), + NoExplicitNull, ] = None feature_agent: Annotated[ FeatureAgent | None, Field( description="Optional buyer-attached pointer to a get_creative_features-capable creative-feature / governance agent the producing agent calls to evaluate each leaf (the gate's SOURCE of feature values). This is the buyer-represents → seller-calls pattern #5280 established for provenance, generalized to the evaluator gate: the buyer REPRESENTS which agent it used, but the seller is the verifier-of-record and decides which agent it actually calls. `agent_url` MUST appear (canonicalized per /docs/reference/url-canonicalization) in the seller's `creative_policy.accepted_verifiers[].agent_url`; an off-list agent is rejected with `EVALUATOR_AGENT_NOT_ACCEPTED` (mirrors PROVENANCE_VERIFIER_NOT_ACCEPTED) before any outbound call. The outbound evaluator call authenticates on the transport; this pointer MUST NOT carry API keys, bearer tokens, client secrets, authorization values, JWKs, JWKS documents, or JWKS URIs. Reuses the same allowlist mechanism — no new allowlist is introduced. Distinct from the `agent_url` oneOf form, which names the evaluator's source directly; `feature_agent` attaches the gate's measurement source alongside any of the three forms." ), + NoExplicitNull, ] = None eval_budget: Annotated[ EvalBudget | None, Field( description='Optional soft ceiling on evaluation effort. Advisory in v1 with no billing coupling. Well-known soft fields max_calls / max_seconds; open for evaluator-specific knobs.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None exemplars: Annotated[ Exemplars, Field( diff --git a/src/adcp/types/domains/core/event.py b/src/adcp/types/domains/core/event.py index 96c9afd97..d0350947d 100644 --- a/src/adcp/types/domains/core/event.py +++ b/src/adcp/types/domains/core/event.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/event.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..enums import action_source as action_source_1 @@ -35,27 +35,35 @@ class Event(AdCPBaseModel): user_match: Annotated[ user_match_1.UserMatch | None, Field(description='User identifiers for attribution matching'), + NoExplicitNull, ] = None custom_data: Annotated[ event_custom_data.EventCustomData | None, Field(description='Event-specific data (value, currency, items, etc.)'), + NoExplicitNull, ] = None action_source: Annotated[ - action_source_1.ActionSource | None, Field(description='Where the event originated') + action_source_1.ActionSource | None, + Field(description='Where the event originated'), + NoExplicitNull, ] = None surface: Annotated[ event_surface.EventSurface | None, Field( description='Optional structured surface context for the event, such as an owned channel, profile, feed, podcast, newsletter list, website, app, or store. Complements `action_source`; use it when optimization-relevant meaning would otherwise live only in platform-specific `ext` metadata.' ), + NoExplicitNull, ] = None event_source_url: Annotated[ AnyUrl | None, Field( description="URL where the event occurred (required when action_source is 'website')" ), + NoExplicitNull, ] = None custom_event_name: Annotated[ - str | None, Field(description="Name for custom events (used when event_type is 'custom')") + str | None, + Field(description="Name for custom events (used when event_type is 'custom')"), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/event_custom_data.py b/src/adcp/types/domains/core/event_custom_data.py index 222cf806f..6096df326 100644 --- a/src/adcp/types/domains/core/event_custom_data.py +++ b/src/adcp/types/domains/core/event_custom_data.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/event_custom_data.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -17,11 +17,15 @@ class Content(AdCPBaseModel): extra='allow', ) id: Annotated[str, Field(description='Product or content identifier')] - quantity: Annotated[SchemaInt | None, Field(description='Quantity of this item', ge=1)] = None + quantity: Annotated[ + SchemaInt | None, Field(description='Quantity of this item', ge=1), NoExplicitNull + ] = None price: Annotated[ - StrictFloat | None, Field(description='Price per unit of this item', ge=0.0) + StrictFloat | None, Field(description='Price per unit of this item', ge=0.0), NoExplicitNull ] = None - brand: Annotated[str | None, Field(description='Brand name of this item')] = None + brand: Annotated[str | None, Field(description='Brand name of this item'), NoExplicitNull] = ( + None + ) class EventCustomData(AdCPBaseModel): @@ -34,6 +38,7 @@ class EventCustomData(AdCPBaseModel): description='Monetary value of the event. For an event source that declares value_currencies, currency is required and MUST appear in that list. Legacy sources without the new contract remain schema-compatible but are ineligible for canonical ROAS.', ge=0.0, ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -41,30 +46,37 @@ class EventCustomData(AdCPBaseModel): description='ISO 4217 currency code. For a source declaring value_currencies, monetary records MUST carry a currency from that list.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, + ] = None + order_id: Annotated[ + str | None, Field(description='Unique order or transaction identifier'), NoExplicitNull ] = None - order_id: Annotated[str | None, Field(description='Unique order or transaction identifier')] = ( - None - ) content_ids: Annotated[ list[str] | None, Field( description="Item identifiers for catalog attribution. Values are matched against catalog items using the identifier type declared by the catalog's content_id_type field (e.g., SKUs, GTINs, or vertical-specific IDs like job_id)." ), + NoExplicitNull, ] = None content_type: Annotated[ str | None, Field( description="Category of content associated with the event (e.g., 'product', 'job', 'hotel'). Corresponds to the catalog type when used for catalog attribution." ), + NoExplicitNull, + ] = None + content_name: Annotated[ + str | None, Field(description='Name of the product or content'), NoExplicitNull ] = None - content_name: Annotated[str | None, Field(description='Name of the product or content')] = None content_category: Annotated[ - str | None, Field(description='Category of the product or content') + str | None, Field(description='Category of the product or content'), NoExplicitNull ] = None num_items: Annotated[ - SchemaInt | None, Field(description='Number of items in the event', ge=0) + SchemaInt | None, Field(description='Number of items in the event', ge=0), NoExplicitNull + ] = None + search_string: Annotated[ + str | None, Field(description='Search query for search events'), NoExplicitNull ] = None - search_string: Annotated[str | None, Field(description='Search query for search events')] = None progress_percent: Annotated[ StrictFloat | None, Field( @@ -72,6 +84,7 @@ class EventCustomData(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None progress_seconds: Annotated[ StrictFloat | None, @@ -79,8 +92,11 @@ class EventCustomData(AdCPBaseModel): description='Content progress duration reached in seconds, primarily for `watch_milestone` events. Use when the milestone is time-based rather than percentage-based.', ge=0.0, ), + NoExplicitNull, ] = None contents: Annotated[ - list[Content] | None, Field(description='Per-item details for e-commerce events') + list[Content] | None, + Field(description='Per-item details for e-commerce events'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/event_source_health.py b/src/adcp/types/domains/core/event_source_health.py index 01c4f5c1f..d8e91c2c8 100644 --- a/src/adcp/types/domains/core/event_source_health.py +++ b/src/adcp/types/domains/core/event_source_health.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/event_source_health.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import assessment_status @@ -32,6 +32,7 @@ class Detail(AdCPBaseModel): Field( description="Seller's name for this score (e.g., 'Event Quality Score', 'Event Match Quality')." ), + NoExplicitNull, ] = None @@ -50,6 +51,7 @@ class EventSourceHealth(AdCPBaseModel): Field( description='Seller-specific scoring detail. Only present when the seller has a native quality score to relay. Buyer agents should use status (not detail) for cross-seller decisions. Detail is supplementary context for human review or advanced diagnostics.' ), + NoExplicitNull, ] = None match_rate: Annotated[ StrictFloat | None, @@ -58,18 +60,21 @@ class EventSourceHealth(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None last_event_at: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp of the most recent event received from this source. Absent when no events have been received.' ), + NoExplicitNull, ] = None evaluated_at: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp of when this health assessment was computed. When health is derived from reporting data, this may lag real-time. Buyer agents can use this to decide whether to trust stale assessments or re-request.' ), + NoExplicitNull, ] = None events_received_24h: Annotated[ SchemaInt | None, @@ -77,10 +82,12 @@ class EventSourceHealth(AdCPBaseModel): description='Number of events received from this source in the last 24 hours. Zero indicates the source is configured but not firing.', ge=0, ), + NoExplicitNull, ] = None issues: Annotated[ list[diagnostic_issue.DiagnosticIssue] | None, Field( description='Actionable issues detected with this event source. Sellers should limit to the top 3-5 most actionable items. Buyer agents should sort by severity rather than relying on array position.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/event_surface.py b/src/adcp/types/domains/core/event_surface.py index 1255d8a3c..f20f1b5e6 100644 --- a/src/adcp/types/domains/core/event_surface.py +++ b/src/adcp/types/domains/core/event_surface.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/event_surface.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -43,6 +43,7 @@ class EventSurface(AdCPBaseModel): max_length=128, min_length=1, ), + NoExplicitNull, ] = None namespace: Annotated[ str | None, @@ -51,6 +52,7 @@ class EventSurface(AdCPBaseModel): max_length=128, min_length=1, ), + NoExplicitNull, ] = None property_id: Annotated[ str | None, @@ -59,5 +61,6 @@ class EventSurface(AdCPBaseModel): max_length=256, min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/feature_requirement.py b/src/adcp/types/domains/core/feature_requirement.py index 101e00bf0..bdfafb329 100644 --- a/src/adcp/types/domains/core/feature_requirement.py +++ b/src/adcp/types/domains/core/feature_requirement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/feature_requirement.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -26,10 +26,12 @@ class FeatureRequirement(AdCPBaseModel): min_value: Annotated[ StrictFloat | None, Field(description='Minimum numeric value required (for quantitative features)'), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field(description='Maximum numeric value allowed (for quantitative features)'), + NoExplicitNull, ] = None allowed_values: Annotated[ list[Any] | None, @@ -37,16 +39,19 @@ class FeatureRequirement(AdCPBaseModel): description='Values that pass the requirement (for binary/categorical features)', min_length=1, ), + NoExplicitNull, ] = None if_not_covered: Annotated[ IfNotCovered | None, Field( description="How to handle properties where this feature is not covered. 'exclude' (default): property is removed from the list. 'include': property passes this requirement (fail-open)." ), + NoExplicitNull, ] = IfNotCovered.exclude policy_id: Annotated[ str | None, Field( description='Optional attribution — when this requirement encodes a specific buyer-chosen threshold authorized by a policy, policy_id references the authorizing PolicyEntry. Producers populate when the mechanism exists because of a specific policy (e.g., max_value: 15 on audience_children_composition because of uk_hfss); do NOT populate when the requirement is a general filter unrelated to any policy. Governance findings echo this policy_id when emitting denials traced to the requirement. See /docs/governance/policy-attribution.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/flight_item.py b/src/adcp/types/domains/core/flight_item.py index eacf6d9c7..4f065dd4c 100644 --- a/src/adcp/types/domains/core/flight_item.py +++ b/src/adcp/types/domains/core/flight_item.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/flight_item.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from . import ext as ext_1 @@ -22,9 +22,9 @@ class Origin(AdCPBaseModel): str, Field(description="IATA airport code (e.g., 'AMS', 'JFK', 'LHR').", pattern='^[A-Z]{3}$'), ] - city: Annotated[str | None, Field(description="City name (e.g., 'Amsterdam', 'New York').")] = ( - None - ) + city: Annotated[ + str | None, Field(description="City name (e.g., 'Amsterdam', 'New York')."), NoExplicitNull + ] = None class Destination(AdCPBaseModel): @@ -32,7 +32,7 @@ class Destination(AdCPBaseModel): extra='forbid', ) airport_code: Annotated[str, Field(description='IATA airport code.', pattern='^[A-Z]{3}$')] - city: Annotated[str | None, Field(description='City name.')] = None + city: Annotated[str | None, Field(description='City name.'), NoExplicitNull] = None class FlightItem(AdCPBaseModel): @@ -44,29 +44,38 @@ class FlightItem(AdCPBaseModel): ] origin: Annotated[Origin, Field(description='Departure airport or city.')] destination: Annotated[Destination, Field(description='Arrival airport or city.')] - airline: Annotated[str | None, Field(description='Airline name or IATA airline code.')] = None - price: Annotated[price_1.Price | None, Field(description='Ticket price or starting fare.')] = ( - None - ) + airline: Annotated[ + str | None, Field(description='Airline name or IATA airline code.'), NoExplicitNull + ] = None + price: Annotated[ + price_1.Price | None, Field(description='Ticket price or starting fare.'), NoExplicitNull + ] = None description: Annotated[ - str | None, Field(description='Route description or promotional text.') + str | None, Field(description='Route description or promotional text.'), NoExplicitNull ] = None departure_time: Annotated[ - AwareDatetime | None, Field(description='Departure date and time (ISO 8601).') + AwareDatetime | None, + Field(description='Departure date and time (ISO 8601).'), + NoExplicitNull, ] = None arrival_time: Annotated[ - AwareDatetime | None, Field(description='Arrival date and time (ISO 8601).') + AwareDatetime | None, Field(description='Arrival date and time (ISO 8601).'), NoExplicitNull ] = None image_url: Annotated[ - AnyUrl | None, Field(description='Promotional image URL (typically a destination photo).') + AnyUrl | None, + Field(description='Promotional image URL (typically a destination photo).'), + NoExplicitNull, + ] = None + url: Annotated[ + AnyUrl | None, Field(description='Booking page URL for this route.'), NoExplicitNull ] = None - url: Annotated[AnyUrl | None, Field(description='Booking page URL for this route.')] = None tags: Annotated[ list[str] | None, Field( description="Tags for filtering (e.g., 'direct', 'red-eye', 'business-class').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -74,5 +83,6 @@ class FlightItem(AdCPBaseModel): description="Typed creative asset pools for this flight. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (destination hero), 'images_vertical' (9:16 for Stories), 'images_square' (1:1). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/forecast_dimension_audience.py b/src/adcp/types/domains/core/forecast_dimension_audience.py index d523f7699..eff799c62 100644 --- a/src/adcp/types/domains/core/forecast_dimension_audience.py +++ b/src/adcp/types/domains/core/forecast_dimension_audience.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_dimension_audience.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import audience_source as audience_source_1 @@ -24,5 +24,5 @@ class AudienceForecastDimension(AdCPBaseModel): audience_source_1.AudienceSource, Field(description='Origin of the audience segment.') ] audience_name: Annotated[ - str | None, Field(description='Human-readable audience segment name.') + str | None, Field(description='Human-readable audience segment name.'), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/core/forecast_dimension_geo.py b/src/adcp/types/domains/core/forecast_dimension_geo.py index c53d9c428..6f3b1432c 100644 --- a/src/adcp/types/domains/core/forecast_dimension_geo.py +++ b/src/adcp/types/domains/core/forecast_dimension_geo.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_dimension_geo.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import geo_level as geo_level_1 @@ -26,6 +26,7 @@ class GeoForecastDimension(AdCPBaseModel): Field( description="Classification system for metro or postal_area levels. Required when geo_level is 'metro' or 'postal_area'. Metro rows use metro-system enum values such as 'nielsen_dma'; native postal rows use country-local postal-system enum values such as 'zip' with country 'US'; deprecated legacy postal rows may use legacy-postal-system enum values such as 'us_zip'. Omit for country and region rows." ), + NoExplicitNull, ] = None country: Annotated[ str | None, @@ -33,6 +34,7 @@ class GeoForecastDimension(AdCPBaseModel): description='ISO 3166-1 alpha-2 country code. Required for native postal_area rows and omitted for legacy postal rows, metro rows, country rows, and region rows.', pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None geo_code: Annotated[ str, @@ -45,4 +47,5 @@ class GeoForecastDimension(AdCPBaseModel): Field( description="Human-readable geographic name (e.g., 'United States', 'California', 'New York DMA')." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/forecast_dimension_placement.py b/src/adcp/types/domains/core/forecast_dimension_placement.py index d0baa3c53..80537f788 100644 --- a/src/adcp/types/domains/core/forecast_dimension_placement.py +++ b/src/adcp/types/domains/core/forecast_dimension_placement.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_dimension_placement.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import placement_ref as placement_ref_1 @@ -28,4 +28,5 @@ class PlacementForecastDimension(AdCPBaseModel): Field( description='Human-readable placement name, useful when the buyer has not resolved the placement catalog.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/forecast_dimension_signal.py b/src/adcp/types/domains/core/forecast_dimension_signal.py index 2105ae6cb..e53511c3a 100644 --- a/src/adcp/types/domains/core/forecast_dimension_signal.py +++ b/src/adcp/types/domains/core/forecast_dimension_signal.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/forecast_dimension_signal.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat, model_validator from . import signal_ref as signal_ref_1 @@ -31,6 +31,7 @@ class SignalForecastDimension(AdCPBaseModel): Field( description='Canonical signal reference for this forecast row. Required when the row needs to disambiguate product-local, data-provider, or signal-source identity. Product-relative forecasts SHOULD use signal_ref.' ), + NoExplicitNull, ] = None signal_id: Annotated[ str | None, @@ -38,6 +39,7 @@ class SignalForecastDimension(AdCPBaseModel): description='Signal identifier shorthand for this forecast row. Use only when the enclosing context already identifies the signal unambiguously, such as a coverage_forecast nested directly under one get_signals signal item. Otherwise use signal_ref.', pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None signal_value: Annotated[ str | StrictFloat | StrictBool | None, @@ -56,9 +58,12 @@ class SignalForecastDimension(AdCPBaseModel): Field( description='Human-readable signal name, useful when the buyer has not resolved the signal definition.' ), + NoExplicitNull, ] = None signal_value_name: Annotated[ - str | None, Field(description='Human-readable label for the signal value bucket.') + str | None, + Field(description='Human-readable label for the signal value bucket.'), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/forecast_point.py b/src/adcp/types/domains/core/forecast_point.py index 33eaa000d..04a7b12c9 100644 --- a/src/adcp/types/domains/core/forecast_point.py +++ b/src/adcp/types/domains/core/forecast_point.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_point.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Dict -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import availability_status as availability_status_1 @@ -19,14 +19,17 @@ class CoverageRate(ForecastRange): low: Annotated[ StrictFloat | None, Field(description='Conservative (low-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None mid: Annotated[ StrictFloat | None, Field(description='Expected (most likely) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None high: Annotated[ StrictFloat | None, Field(description='Optimistic (high-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None @@ -35,27 +38,28 @@ class Metrics(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - audience_size: forecast_range.ForecastRange | None = None - reach: forecast_range.ForecastRange | None = None - frequency: forecast_range.ForecastRange | None = None - impressions: forecast_range.ForecastRange | None = None - clicks: forecast_range.ForecastRange | None = None - spend: forecast_range.ForecastRange | None = None - views: forecast_range.ForecastRange | None = None - completed_views: forecast_range.ForecastRange | None = None - grps: forecast_range.ForecastRange | None = None - engagements: forecast_range.ForecastRange | None = None - follows: forecast_range.ForecastRange | None = None - saves: forecast_range.ForecastRange | None = None - profile_visits: forecast_range.ForecastRange | None = None - measured_impressions: forecast_range.ForecastRange | None = None - downloads: forecast_range.ForecastRange | None = None - plays: forecast_range.ForecastRange | None = None + audience_size: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + reach: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + frequency: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + clicks: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + spend: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + views: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + completed_views: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + grps: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + engagements: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + follows: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + saves: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + profile_visits: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + measured_impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + downloads: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + plays: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None coverage_rate: Annotated[ CoverageRate | None, Field( description="Share of the declared forecast scope represented by this point. For signal coverage forecasts, this is the point's count divided by the coverage_forecast.scope denominator. Range 0.0 to 1.0." ), + NoExplicitNull, ] = None @@ -72,34 +76,40 @@ class Viewability(AdCPBaseModel): vendor: Annotated[ brand_ref.BrandReference | None, Field(description='Vendor expected to produce or verify these viewability values.'), + NoExplicitNull, ] = None measurable_impressions: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted impressions where viewability can be measured. Coverage denominator for viewable_rate and viewed_seconds.' ), + NoExplicitNull, ] = None viewable_impressions: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted impressions expected to meet the viewability threshold defined by standard.' ), + NoExplicitNull, ] = None viewable_rate: Annotated[ ViewableRate | None, Field( description='Forecasted viewable impression rate (viewable_impressions / measurable_impressions). Range 0.0 to 1.0.' ), + NoExplicitNull, ] = None viewed_seconds: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted average in-view duration per measurable impression, in seconds.' ), + NoExplicitNull, ] = None standard: Annotated[ viewability_standard.ViewabilityStandard | None, Field(description='Viewability measurement standard applied to these forecasted values.'), + NoExplicitNull, ] = None @@ -114,6 +124,7 @@ class ForecastPoint(AdCPBaseModel): examples=['Primetime', 'Morning Drive', 'Large Format Transit'], max_length=128, ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -121,24 +132,28 @@ class ForecastPoint(AdCPBaseModel): description='Budget amount for this forecast point. Required for spend curves; omit for availability forecasts where the metrics represent total available inventory. For allocation-level forecasts, this is the absolute budget for that allocation (not the percentage). For proposal-level forecasts, this is the total proposal budget. When omitted, use metrics.spend to express the estimated cost of the available inventory.', ge=0.0, ), + NoExplicitNull, ] = None product_id: Annotated[ str | None, Field( description='Optional product context for this forecast row. Usually omitted on product-level and allocation-level forecasts where the product is already implied. On proposal-level forecasts, populate when a dimensional row, especially a placement row, maps to a specific product allocation so buyers can turn the row into an executable package choice. Omit for true aggregate proposal rows spanning multiple products.' ), + NoExplicitNull, ] = None dimensions: Annotated[ forecast_point_dimensions.ForecastPointDimensions | None, Field( description='Dimension constraints represented by this forecast point, such as country, region, placement, device type, platform, audience, signal value, time window, or intersections such as placement x country or product x signal. Each item declares one dimension family; when multiple items are present, the point represents their intersection. Sellers MUST NOT emit more than one item for each `kind` on a point; consumers MUST NOT treat repeated kinds as OR semantics. Use multiple points with dimensions to expose country/placement/signal availability within one product, proposal, or signal coverage forecast without creating separate products solely for each dimension. Dimensions describe the forecast row and are independent of pricing_options.' ), + NoExplicitNull, ] = None availability_status: Annotated[ availability_status_1.AvailabilityStatus | None, Field( description="Bookability of the inventory this row describes, as of the forecast's generated_at. Most meaningful on rows with a time dimension in an availability forecast (forecast_range_unit 'availability'). This is a snapshot, not a hold: valid_until bounds freshness, and proposal finalization or purchase remains the commitment boundary. When omitted, the row makes no bookability claim. 'unavailable' rows may carry empty metrics." ), + NoExplicitNull, ] = None metrics: Annotated[ Metrics, @@ -151,10 +166,12 @@ class ForecastPoint(AdCPBaseModel): Field( description='Forecasted viewability metrics. Mirrors delivery-metrics.viewability, but numeric values are ForecastRange objects because forecast rows may provide low/mid/high bounds. Use this for pre-buy viewability expectations by forecast point without folding measurement metrics into pricing_options.' ), + NoExplicitNull, ] = None vendor_metric_values: Annotated[ list[forecast_vendor_metric_value.ForecastVendorMetricValue] | None, Field( description="Forecasted values for vendor-defined metrics that the product's reporting_capabilities.vendor_metrics declared. Mirrors delivery-metrics.vendor_metric_values, but value and measurable_impressions use ForecastRange. These forecasted measurement values are independent of pricing_options." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/forecast_range.py b/src/adcp/types/domains/core/forecast_range.py index fd237d1c2..606390671 100644 --- a/src/adcp/types/domains/core/forecast_range.py +++ b/src/adcp/types/domains/core/forecast_range.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_range.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat, model_validator @@ -18,13 +18,19 @@ class ForecastRange(AdCPBaseModel): extra='allow', ) low: Annotated[ - StrictFloat | None, Field(description='Conservative (low-end) forecast value', ge=0.0) + StrictFloat | None, + Field(description='Conservative (low-end) forecast value', ge=0.0), + NoExplicitNull, ] = None mid: Annotated[ - StrictFloat | None, Field(description='Expected (most likely) forecast value', ge=0.0) + StrictFloat | None, + Field(description='Expected (most likely) forecast value', ge=0.0), + NoExplicitNull, ] = None high: Annotated[ - StrictFloat | None, Field(description='Optimistic (high-end) forecast value', ge=0.0) + StrictFloat | None, + Field(description='Optimistic (high-end) forecast value', ge=0.0), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/forecast_rate_range.py b/src/adcp/types/domains/core/forecast_rate_range.py index b91ad0e10..8bf39cf1d 100644 --- a/src/adcp/types/domains/core/forecast_rate_range.py +++ b/src/adcp/types/domains/core/forecast_rate_range.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_rate_range.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field, StrictFloat from .forecast_range import ForecastRange @@ -15,12 +16,15 @@ class ForecastRateRange(ForecastRange): low: Annotated[ StrictFloat | None, Field(description='Conservative (low-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None mid: Annotated[ StrictFloat | None, Field(description='Expected (most likely) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None high: Annotated[ StrictFloat | None, Field(description='Optimistic (high-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/forecast_vendor_metric_value.py b/src/adcp/types/domains/core/forecast_vendor_metric_value.py index 9ce6f425b..d9318dc0d 100644 --- a/src/adcp/types/domains/core/forecast_vendor_metric_value.py +++ b/src/adcp/types/domains/core/forecast_vendor_metric_value.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/forecast_vendor_metric_value.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import brand_ref, forecast_range, vendor_metric_id @@ -40,28 +40,33 @@ class ForecastVendorMetricValue(AdCPBaseModel): description="Unit of the value. Free-form to accommodate heterogeneous vendor metrics (e.g., 'score', 'seconds', 'persons', 'gCO2e', 'USD', 'lift_percent', 'index'). When populated inline, SHOULD match the vendor's published unit.", examples=['score', 'seconds', 'persons', 'gCO2e', 'USD', 'lift_percent', 'index'], ), + NoExplicitNull, ] = None measurable_impressions: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted number of impressions the vendor expects to be able to measure. Coverage denominator for the vendor metric; buyers compute estimated coverage as measurable_impressions / impressions when both are present. For play-based channels (DOOH, cinema, place-based) forecast measurable_plays or measurable_play_seconds instead.' ), + NoExplicitNull, ] = None measurable_plays: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted number of plays the vendor expects to be able to measure. Coverage denominator for channels where a play, not an impression, is the atomic observation unit; buyers compute estimated coverage as measurable_plays / plays when both are present.' ), + NoExplicitNull, ] = None measurable_play_seconds: Annotated[ forecast_range.ForecastRange | None, Field( description='Forecasted play-seconds (creative playout seconds summed across endpoints — screens, speakers, players) the vendor expects to be able to measure. Coverage denominator when the vendor meters exposure duration rather than discrete plays.' ), + NoExplicitNull, ] = None breakdown: Annotated[ dict[str, Any] | None, Field( description="Optional structured payload for vendor metrics that do not fit a single scalar. Forecast rows SHOULD use ForecastRange values inside breakdown when sub-values are numeric forecasts. Buyers MUST treat this object as opaque without consulting the vendor's documentation." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/format.py b/src/adcp/types/domains/core/format.py index 67e3670fc..56fa0cba1 100644 --- a/src/adcp/types/domains/core/format.py +++ b/src/adcp/types/domains/core/format.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/format.json -# timestamp: 2026-10-13T11:29:35+00:00 +# timestamp: 2026-10-22T12:24:23+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, RootModel, StrictBool, StrictFloat from ..enums import ( @@ -71,33 +71,46 @@ class Dimensions(AdCPBaseModel): width: Annotated[ StrictFloat | None, Field(description='Fixed width. Interpretation depends on unit (default: pixels).', gt=0.0), + NoExplicitNull, ] = None height: Annotated[ StrictFloat | None, Field( description='Fixed height. Interpretation depends on unit (default: pixels).', gt=0.0 ), + NoExplicitNull, ] = None min_width: Annotated[ - StrictFloat | None, Field(description='Minimum width for responsive renders', gt=0.0) + StrictFloat | None, + Field(description='Minimum width for responsive renders', gt=0.0), + NoExplicitNull, ] = None min_height: Annotated[ - StrictFloat | None, Field(description='Minimum height for responsive renders', gt=0.0) + StrictFloat | None, + Field(description='Minimum height for responsive renders', gt=0.0), + NoExplicitNull, ] = None max_width: Annotated[ - StrictFloat | None, Field(description='Maximum width for responsive renders', gt=0.0) + StrictFloat | None, + Field(description='Maximum width for responsive renders', gt=0.0), + NoExplicitNull, ] = None max_height: Annotated[ - StrictFloat | None, Field(description='Maximum height for responsive renders', gt=0.0) + StrictFloat | None, + Field(description='Maximum height for responsive renders', gt=0.0), + NoExplicitNull, ] = None unit: Annotated[ dimension_unit.DimensionUnit | None, Field( description="Unit of measurement for width/height values. Defaults to 'px' when absent. Print formats use 'inches' or 'cm'." ), + NoExplicitNull, ] = None responsive: Annotated[ - Responsive | None, Field(description='Indicates which dimensions are responsive/fluid') + Responsive | None, + Field(description='Indicates which dimensions are responsive/fluid'), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -105,6 +118,7 @@ class Dimensions(AdCPBaseModel): description="Fixed aspect ratio constraint (e.g., '16:9', '4:3', '1:1', '1.91:1')", pattern='^\\d+(\\.\\d+)?:\\d+(\\.\\d+)?$', ), + NoExplicitNull, ] = None @@ -120,6 +134,7 @@ class Renders(AdCPBaseModel): Field( description='When true, parameters for this render (dimensions and/or duration) are specified in the format_id. Used for template formats that accept parameters. Mutually exclusive with specifying dimensions object explicitly.' ), + NoExplicitNull, ] = None dimensions: Annotated[ Dimensions, @@ -151,6 +166,7 @@ class Renders1(AdCPBaseModel): Field( description='Dimensions for this rendered piece. Defaults to pixels when unit is absent.' ), + NoExplicitNull, ] = None @@ -210,6 +226,7 @@ class Accessibility(AdCPBaseModel): Field( description='When true, all assets with x-accessibility fields must include those fields. For inspectable assets (image, video, audio), this means providing accessibility metadata like alt_text or captions. For opaque assets (HTML, JavaScript), this means providing self-declared accessibility properties.' ), + NoExplicitNull, ] = None @@ -247,6 +264,7 @@ class BaseIndividualAsset(AdCPBaseModel): Field( description="Descriptive label for this asset's purpose (e.g., 'hero_image', 'logo', 'third_party_tracking'). For documentation and UI display only — manifests key assets by asset_id, not asset_role." ), + NoExplicitNull, ] = None required: Annotated[ StrictBool, @@ -259,12 +277,14 @@ class BaseIndividualAsset(AdCPBaseModel): Field( description="Publisher-controlled elements rendered on top of buyer content at this asset's position (e.g., video player controls, publisher logos). Creative agents should avoid placing critical content (CTAs, logos, key copy) within overlay bounds." ), + NoExplicitNull, ] = None asset_group_id: Annotated[ str | None, Field( description="Optional canonical asset_group_id this slot fills, drawn from /schemas/core/asset-group-vocabulary.json. Lets buyers and migration tools resolve v1 author-invented slot names (e.g., `click_url`) to canonical names (e.g., `landing_page_url`). Validators MAY soft-warn when a v1 slot's asset_id is a known alias but no asset_group_id is declared." ), + NoExplicitNull, ] = None @@ -275,12 +295,14 @@ class BaseGroupAsset(AdCPBaseModel): Field( description="Descriptive label for this asset's purpose. For documentation and UI display only — manifests key assets by asset_id, not asset_role." ), + NoExplicitNull, ] = None asset_group_id: Annotated[ str | None, Field( description='Optional canonical asset_group_id this slot fills, drawn from /schemas/core/asset-group-vocabulary.json. Same semantics as on baseIndividualAsset — lets buyers and migration tools resolve v1 author-invented slot names to canonical names.' ), + NoExplicitNull, ] = None required: Annotated[ StrictBool, @@ -291,55 +313,72 @@ class BaseGroupAsset(AdCPBaseModel): Field( description="Publisher-controlled elements rendered on top of buyer content at this asset's position (e.g., carousel navigation arrows, slide indicators). Creative agents should avoid placing critical content within overlay bounds." ), + NoExplicitNull, ] = None class Assets(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['image'] = 'image' - requirements: image_asset_requirements.ImageAssetRequirements | None = None + requirements: Annotated[ + image_asset_requirements.ImageAssetRequirements | None, NoExplicitNull + ] = None class Assets9(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['video'] = 'video' - requirements: video_asset_requirements.VideoAssetRequirements | None = None + requirements: Annotated[ + video_asset_requirements.VideoAssetRequirements | None, NoExplicitNull + ] = None class Assets10(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['audio'] = 'audio' - requirements: audio_asset_requirements.AudioAssetRequirements | None = None + requirements: Annotated[ + audio_asset_requirements.AudioAssetRequirements | None, NoExplicitNull + ] = None class Assets11(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['text'] = 'text' - requirements: text_asset_requirements.TextAssetRequirements | None = None + requirements: Annotated[ + text_asset_requirements.TextAssetRequirements | None, NoExplicitNull + ] = None class Assets12(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['markdown'] = 'markdown' - requirements: markdown_asset_requirements.MarkdownAssetRequirements | None = None + requirements: Annotated[ + markdown_asset_requirements.MarkdownAssetRequirements | None, NoExplicitNull + ] = None class Assets13(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['html'] = 'html' - requirements: html_asset_requirements.HtmlAssetRequirements | None = None + requirements: Annotated[ + html_asset_requirements.HtmlAssetRequirements | None, NoExplicitNull + ] = None class Assets14(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['css'] = 'css' - requirements: css_asset_requirements.CssAssetRequirements | None = None + requirements: Annotated[css_asset_requirements.CssAssetRequirements | None, NoExplicitNull] = ( + None + ) class Assets15(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['javascript'] = 'javascript' - requirements: javascript_asset_requirements.JavascriptAssetRequirements | None = None + requirements: Annotated[ + javascript_asset_requirements.JavascriptAssetRequirements | None, NoExplicitNull + ] = None class Assets16(BaseIndividualAsset): @@ -350,25 +389,33 @@ class Assets16(BaseIndividualAsset): class Assets17(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['vast'] = 'vast' - requirements: vast_asset_requirements.VastAssetRequirements | None = None + requirements: Annotated[ + vast_asset_requirements.VastAssetRequirements | None, NoExplicitNull + ] = None class Assets18(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['daast'] = 'daast' - requirements: daast_asset_requirements.DaastAssetRequirements | None = None + requirements: Annotated[ + daast_asset_requirements.DaastAssetRequirements | None, NoExplicitNull + ] = None class Assets19(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['url'] = 'url' - requirements: url_asset_requirements.UrlAssetRequirements | None = None + requirements: Annotated[url_asset_requirements.UrlAssetRequirements | None, NoExplicitNull] = ( + None + ) class Assets20(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['webhook'] = 'webhook' - requirements: webhook_asset_requirements.WebhookAssetRequirements | None = None + requirements: Annotated[ + webhook_asset_requirements.WebhookAssetRequirements | None, NoExplicitNull + ] = None class Assets21(BaseIndividualAsset): @@ -408,42 +455,58 @@ class Assets28(BaseIndividualAsset): class Assets31(BaseGroupAsset): asset_type: Literal['image'] = 'image' - requirements: image_asset_requirements.ImageAssetRequirements | None = None + requirements: Annotated[ + image_asset_requirements.ImageAssetRequirements | None, NoExplicitNull + ] = None class Assets32(BaseGroupAsset): asset_type: Literal['video'] = 'video' - requirements: video_asset_requirements.VideoAssetRequirements | None = None + requirements: Annotated[ + video_asset_requirements.VideoAssetRequirements | None, NoExplicitNull + ] = None class Assets33(BaseGroupAsset): asset_type: Literal['audio'] = 'audio' - requirements: audio_asset_requirements.AudioAssetRequirements | None = None + requirements: Annotated[ + audio_asset_requirements.AudioAssetRequirements | None, NoExplicitNull + ] = None class Assets34(BaseGroupAsset): asset_type: Literal['text'] = 'text' - requirements: text_asset_requirements.TextAssetRequirements | None = None + requirements: Annotated[ + text_asset_requirements.TextAssetRequirements | None, NoExplicitNull + ] = None class Assets35(BaseGroupAsset): asset_type: Literal['markdown'] = 'markdown' - requirements: markdown_asset_requirements.MarkdownAssetRequirements | None = None + requirements: Annotated[ + markdown_asset_requirements.MarkdownAssetRequirements | None, NoExplicitNull + ] = None class Assets36(BaseGroupAsset): asset_type: Literal['html'] = 'html' - requirements: html_asset_requirements.HtmlAssetRequirements | None = None + requirements: Annotated[ + html_asset_requirements.HtmlAssetRequirements | None, NoExplicitNull + ] = None class Assets37(BaseGroupAsset): asset_type: Literal['css'] = 'css' - requirements: css_asset_requirements.CssAssetRequirements | None = None + requirements: Annotated[css_asset_requirements.CssAssetRequirements | None, NoExplicitNull] = ( + None + ) class Assets38(BaseGroupAsset): asset_type: Literal['javascript'] = 'javascript' - requirements: javascript_asset_requirements.JavascriptAssetRequirements | None = None + requirements: Annotated[ + javascript_asset_requirements.JavascriptAssetRequirements | None, NoExplicitNull + ] = None class Assets39(BaseGroupAsset): @@ -452,22 +515,30 @@ class Assets39(BaseGroupAsset): class Assets40(BaseGroupAsset): asset_type: Literal['vast'] = 'vast' - requirements: vast_asset_requirements.VastAssetRequirements | None = None + requirements: Annotated[ + vast_asset_requirements.VastAssetRequirements | None, NoExplicitNull + ] = None class Assets41(BaseGroupAsset): asset_type: Literal['daast'] = 'daast' - requirements: daast_asset_requirements.DaastAssetRequirements | None = None + requirements: Annotated[ + daast_asset_requirements.DaastAssetRequirements | None, NoExplicitNull + ] = None class Assets42(BaseGroupAsset): asset_type: Literal['url'] = 'url' - requirements: url_asset_requirements.UrlAssetRequirements | None = None + requirements: Annotated[url_asset_requirements.UrlAssetRequirements | None, NoExplicitNull] = ( + None + ) class Assets43(BaseGroupAsset): asset_type: Literal['webhook'] = 'webhook' - requirements: webhook_asset_requirements.WebhookAssetRequirements | None = None + requirements: Annotated[ + webhook_asset_requirements.WebhookAssetRequirements | None, NoExplicitNull + ] = None class Assets44(BaseGroupAsset): @@ -501,12 +572,12 @@ class Assets51(BaseGroupAsset): class Assets22(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['catalog'] = 'catalog' - requirements: catalog_requirements.CatalogRequirements | None = None + requirements: Annotated[catalog_requirements.CatalogRequirements | None, NoExplicitNull] = None class Assets45(BaseGroupAsset): asset_type: Literal['catalog'] = 'catalog' - requirements: catalog_requirements.CatalogRequirements | None = None + requirements: Annotated[catalog_requirements.CatalogRequirements | None, NoExplicitNull] = None Assets30 = Annotated[ @@ -656,6 +727,7 @@ class Assets29(AdCPBaseModel): Field( description="How the platform uses repetitions of this group. 'sequential' means all items display in order (carousels, playlists). 'optimize' means the platform selects the best-performing combination from alternatives (asset group optimization like Meta Advantage+ or Google Pmax)." ), + NoExplicitNull, ] = SelectionMode.sequential assets: Annotated[ list[GroupFormatAssetUnion], Field(description='Assets within each repetition of this group') @@ -668,6 +740,7 @@ class CanonicalParameters1(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -675,12 +748,14 @@ class CanonicalParameters1(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -688,60 +763,70 @@ class CanonicalParameters1(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -749,12 +834,14 @@ class CanonicalParameters1(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: image.CanonicalFormatImage @@ -766,6 +853,7 @@ class CanonicalParameters2(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -773,12 +861,14 @@ class CanonicalParameters2(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -786,60 +876,70 @@ class CanonicalParameters2(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -847,12 +947,14 @@ class CanonicalParameters2(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: html5.CanonicalFormatHtml5Banner @@ -864,6 +966,7 @@ class CanonicalParameters3(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -871,12 +974,14 @@ class CanonicalParameters3(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -884,60 +989,70 @@ class CanonicalParameters3(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -945,12 +1060,14 @@ class CanonicalParameters3(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: display_tag.CanonicalFormatDisplayTag @@ -962,6 +1079,7 @@ class CanonicalParameters4(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -969,12 +1087,14 @@ class CanonicalParameters4(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -982,60 +1102,70 @@ class CanonicalParameters4(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1043,12 +1173,14 @@ class CanonicalParameters4(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: image_carousel.CanonicalFormatImageCarousel @@ -1060,6 +1192,7 @@ class CanonicalParameters5(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1067,12 +1200,14 @@ class CanonicalParameters5(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1080,60 +1215,70 @@ class CanonicalParameters5(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1141,12 +1286,14 @@ class CanonicalParameters5(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: video_hosted.CanonicalFormatHostedVideo @@ -1158,6 +1305,7 @@ class CanonicalParameters6(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1165,12 +1313,14 @@ class CanonicalParameters6(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1178,60 +1328,70 @@ class CanonicalParameters6(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1239,12 +1399,14 @@ class CanonicalParameters6(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: video_vast.CanonicalFormatVastVideo @@ -1256,6 +1418,7 @@ class CanonicalParameters7(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1263,12 +1426,14 @@ class CanonicalParameters7(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1276,60 +1441,70 @@ class CanonicalParameters7(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1337,12 +1512,14 @@ class CanonicalParameters7(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: audio_hosted.CanonicalFormatHostedAudio @@ -1354,6 +1531,7 @@ class CanonicalParameters8(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1361,12 +1539,14 @@ class CanonicalParameters8(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1374,60 +1554,70 @@ class CanonicalParameters8(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1435,12 +1625,14 @@ class CanonicalParameters8(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_vast'] = 'audio_vast' params: audio_vast.CanonicalFormatVastAudio @@ -1452,6 +1644,7 @@ class CanonicalParameters9(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1459,12 +1652,14 @@ class CanonicalParameters9(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1472,60 +1667,70 @@ class CanonicalParameters9(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1533,12 +1738,14 @@ class CanonicalParameters9(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: audio_daast.CanonicalFormatDaastAudio @@ -1550,6 +1757,7 @@ class CanonicalParameters10(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1557,12 +1765,14 @@ class CanonicalParameters10(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1570,60 +1780,70 @@ class CanonicalParameters10(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1631,12 +1851,14 @@ class CanonicalParameters10(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven @@ -1648,6 +1870,7 @@ class CanonicalParameters11(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1655,12 +1878,14 @@ class CanonicalParameters11(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1668,60 +1893,70 @@ class CanonicalParameters11(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1729,12 +1964,14 @@ class CanonicalParameters11(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: native_in_feed.CanonicalFormatNativeInFeed @@ -1746,6 +1983,7 @@ class CanonicalParameters12(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1753,12 +1991,14 @@ class CanonicalParameters12(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1766,60 +2006,70 @@ class CanonicalParameters12(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1827,12 +2077,14 @@ class CanonicalParameters12(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: responsive_creative.CanonicalFormatResponsiveCreative @@ -1844,6 +2096,7 @@ class CanonicalParameters13(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1851,12 +2104,14 @@ class CanonicalParameters13(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1864,60 +2119,70 @@ class CanonicalParameters13(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -1925,12 +2190,14 @@ class CanonicalParameters13(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement @@ -1942,6 +2209,7 @@ class CanonicalParameters14(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1949,12 +2217,14 @@ class CanonicalParameters14(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1962,60 +2232,70 @@ class CanonicalParameters14(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -2023,12 +2303,14 @@ class CanonicalParameters14(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay @@ -2040,6 +2322,7 @@ class CanonicalParameters15(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -2047,12 +2330,14 @@ class CanonicalParameters15(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -2060,60 +2345,70 @@ class CanonicalParameters15(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -2121,12 +2416,14 @@ class CanonicalParameters15(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['coordinated_placements'] = 'coordinated_placements' params: coordinated_placements.CanonicalFormatCoordinatedPlacements @@ -2138,6 +2435,7 @@ class CanonicalParameters16(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -2145,12 +2443,14 @@ class CanonicalParameters16(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -2158,60 +2458,70 @@ class CanonicalParameters16(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -2219,12 +2529,14 @@ class CanonicalParameters16(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['custom'] = 'custom' params: Annotated[ @@ -2465,18 +2777,21 @@ class Format(AdCPBaseModel): Field( description='Plain text explanation of what this format does and what assets it requires' ), + NoExplicitNull, ] = None example_url: Annotated[ AnyUrl | None, Field( description='Optional URL to showcase page with examples and interactive demos of this format' ), + NoExplicitNull, ] = None accepts_parameters: Annotated[ list[format_id_parameter.FormatIdParameter] | None, Field( description='List of parameters this format accepts in format_id. Template formats define which parameters (dimensions, duration, etc.) can be specified when instantiating the format. Empty or omitted means this is a concrete format with fixed parameters.' ), + NoExplicitNull, ] = None renders: Annotated[ list[Renders | Renders1] | None, @@ -2484,6 +2799,7 @@ class Format(AdCPBaseModel): description='Specification of rendered pieces for this format. Most formats produce a single render. Companion ad formats (video + banner), adaptive formats, and multi-placement formats produce multiple renders. Each render specifies its role and dimensions.', min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[FormatAssetUnion] @@ -2491,16 +2807,19 @@ class Format(AdCPBaseModel): Field( description="Array of all assets supported for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Use the 'required' boolean on each asset to indicate whether it's mandatory." ), + NoExplicitNull, ] = None delivery: Annotated[ dict[str, Any] | None, Field(description='Delivery method specifications (e.g., hosted, VAST, third-party tags)'), + NoExplicitNull, ] = None supported_macros: Annotated[ list[universal_macro.UniversalMacro | str] | None, Field( description='List of universal macros supported by this format (e.g., MEDIA_BUY_ID, CACHEBUSTER, DEVICE_ID). Used for validation and developer tooling. See docs/creative/universal-macros.mdx for full documentation.' ), + NoExplicitNull, ] = None input_format_ids: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -2508,6 +2827,7 @@ class Format(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.1. Removed at 4.0.** Use `list_transformers` instead — a transformer declares its own `input_format_ids`/`output_format_ids`, so build capability is a property of the transformer (the unit you select and that carries pricing), not a relationship hung on a format. Discover build capability via `list_transformers` (optionally filtered by `input_format_ids`/`output_format_ids`).\n\nMigration: sellers that expressed transform capability by hanging `input_format_ids` on a format SHOULD declare a transformer via `list_transformers` instead. Buyers SHOULD discover build capability via `list_transformers` rather than filtering formats.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* array of format IDs this format accepts as input creative manifests; when present, indicates this format can take existing creatives in these formats as input. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.', ), + NoExplicitNull, ] = None output_format_ids: Annotated[ list[format_id_1.FormatReferenceStructuredObject] | None, @@ -2515,18 +2835,21 @@ class Format(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.1. Removed at 4.0.** Use `list_transformers` instead — a transformer declares its own `output_format_ids`, so what a builder can produce is a property of the transformer, not a relationship hung on a format. Discover via `list_transformers`.\n\nMigration: sellers that expressed multi-output build capability (e.g. a multi-publisher template) by hanging `output_format_ids` on a format SHOULD declare a transformer via `list_transformers` instead.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* array of format IDs this format can produce as output; when present, indicates this format can build creatives in these output formats. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.', ), + NoExplicitNull, ] = None format_card: Annotated[ FormatCard | None, Field( description='Optional standard visual card (300x400px) for displaying this format in user interfaces. Can be rendered via preview_creative or pre-generated.' ), + NoExplicitNull, ] = None accessibility: Annotated[ Accessibility | None, Field( description='Accessibility posture of this format. Declares the WCAG conformance level that creatives produced by this format will meet.' ), + NoExplicitNull, ] = None supported_disclosure_positions: Annotated[ list[disclosure_position.DisclosurePosition] | None, @@ -2534,6 +2857,7 @@ class Format(AdCPBaseModel): description='Disclosure positions this format can render. Buyers use this to determine whether a format can satisfy their compliance requirements before submitting a creative. When omitted, the format makes no disclosure rendering guarantees — creative agents SHOULD treat this as incompatible with briefs that require specific disclosure positions. Values correspond to positions on creative-brief.json required_disclosures.', min_length=1, ), + NoExplicitNull, ] = None disclosure_capabilities: Annotated[ list[DisclosureCapability] | None, @@ -2541,12 +2865,14 @@ class Format(AdCPBaseModel): description='Structured disclosure capabilities per position with persistence modes. Declares which persistence behaviors each disclosure position supports, enabling persistence-aware matching against provenance render guidance and brief requirements. When present, supersedes supported_disclosure_positions for persistence-aware queries. The flat supported_disclosure_positions field is retained for backward compatibility. Each position MUST appear at most once; validators and agents SHOULD reject duplicates.', min_length=1, ), + NoExplicitNull, ] = None format_card_detailed: Annotated[ FormatCardDetailed | None, Field( description='Optional detailed card with carousel and full specifications. Provides rich format documentation similar to ad spec pages.' ), + NoExplicitNull, ] = None reported_metrics: Annotated[ list[available_metric.AvailableMetric] | None, @@ -2554,6 +2880,7 @@ class Format(AdCPBaseModel): description='Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. Under `enums/available-metric.json`, a container such as `viewability` subsumes numeric leaves such as `viewable_rate`; structured distributions match only their explicit `viewed_seconds_percentiles` or `viewed_seconds_histogram` identities. If omitted, the format defers entirely to product-level metric declarations.', min_length=1, ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -2562,12 +2889,14 @@ class Format(AdCPBaseModel): description='**DEPRECATED in 3.1. Removed at 4.0.** Use `transformer.pricing_options` (via `list_transformers`) instead — pricing belongs on the transformer (the unit selected and billed), exactly as it belongs on a media-buy product. Once formats only describe output shape, format-level pricing is vestigial.\n\nMigration: transformation/generation agents that charged via `format.pricing_options` SHOULD move the same `vendor-pricing-option` entries onto the corresponding transformer. The applied option is echoed per-leaf on the build_creative response and reconciled via report_usage, unchanged.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* pricing options for this format, used by transformation/generation agents that charge per format adapted, per image generated, or per unit of work; present when the request included include_pricing=true and account. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.', min_length=1, ), + NoExplicitNull, ] = None canonical: Annotated[ canonical_projection_ref.CanonicalProjectionReference | None, Field( description='Optional v2 canonical-projection annotation. Always an object — bare-string shorthand (`canonical: "image"`) is not supported; the minimal form is `canonical: { "kind": "image" }`. Carries `kind` (which canonical the v1 format projects to) plus optional `asset_source` and `slots_override` for cases where the v1 format\'s shape doesn\'t follow the canonical\'s defaults (e.g., generative entries whose input is `generation_prompt: text` instead of `image_main: image`).\n\nWhen set, SDKs use this annotation as the authoritative v1 → v2 mapping for this format, bypassing the [v1 canonical mapping registry](/schemas/registries/v1-canonical-mapping.json) lookup. Combined with the slot-level `asset_group_id` declarations on each `assets[i]` entry, a v1 format declaration with `canonical` set is fully self-describing for v1↔v2 translation.\n\nResolution order for SDK projection from v1 wire shape to v2 (per RFC #3305 amendment #3767):\n1. If this `canonical` field is set, use it (seller-declared, highest priority). Apply `asset_source` and `slots_override` from the projection ref when present; otherwise inherit the canonical\'s defaults.\n2. Else, look up `format_id` in the canonical mapping registry\'s `format_id_glob` entries.\n3. Else, attempt structural match against the registry\'s `structural` entries (asset types, slot shape, vast_versions, etc.).\n4. Else, fail closed: SDK MUST NOT emit `format_options` for products carrying this format. Surface `FORMAT_PROJECTION_FAILED` on the response `errors[]` suggesting the seller add an explicit `canonical` annotation or file a registry entry.\n\nWhen `canonical.kind` is `custom`, the seller MUST also declare `canonical_format_shape` and `canonical_format_schema` (parallel to ProductFormatDeclaration\'s `format_shape` and `format_schema`) so buyer SDKs can fetch the seller\'s custom format schema.\n\nSee `canonical-projection-ref.json` for full projection semantics and examples (default-slot case, generative case, brief-driven case).' ), + NoExplicitNull, ] = None canonical_parameters: Annotated[ CanonicalParameters | None, @@ -2575,6 +2904,7 @@ class Format(AdCPBaseModel): deprecated=True, description="**DEPRECATED in 3.1. Removed at 4.0.** Use `v1_format_ref` on the v2 `ProductFormatDeclaration` instead — the seller authors a v2 declaration (in `Product.format_options` or `creative.supported_formats`) and links it back to this v1 format via `v1_format_ref: { agent_url, id }`. The directional link from v2 → v1 is the same fact as `canonical_parameters` without the parallel-shape drift surface (v1 file and `canonical_parameters` were two declarations of the same thing; hand-authored, drifting silently).\n\nMigration: every seller currently authoring `canonical_parameters` SHOULD migrate to authoring a v2 declaration on the corresponding product (or capability) with `v1_format_ref` pointing back at this v1 format. v1 files become pure v1 again — no v2-shape mirroring.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* When `canonical` is set, this field carries the full ProductFormatDeclaration that the SDK projects this v1 format into. The `format_kind` MUST equal the `canonical` field value (validators enforce). When set, this is the authoritative source for SDK v1→v2 projection — the registry's structural-match parameter inference is bypassed. SDKs reading 3.1 catalogs MUST continue to honor `canonical_parameters` when present; 4.0+ SDKs MAY reject the field. New code SHOULD NOT emit this field. Seller execution authority is never projected through this deprecated field, so tracker_execution_contract and tracker_execution_contract_digest are forbidden.\n\n**Drift contract (still normative while supported).** Hand-authored `canonical_parameters` MUST satisfy the *narrows* relation against this v1 format's `requirements` and `assets[*]` shape (see canonical-formats.mdx 'Narrows — formal definition'). SDKs that read this v1 file SHOULD lint-time check the equivalence at build/load and emit `FORMAT_PROJECTION_FAILED` if the two disagree.", ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/format_id.py b/src/adcp/types/domains/core/format_id.py index ef25372f8..28f8eedea 100644 --- a/src/adcp/types/domains/core/format_id.py +++ b/src/adcp/types/domains/core/format_id.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/format_id.json -# timestamp: 2026-10-05T03:58:57+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt, WireUrl +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt, WireUrl from pydantic import ConfigDict, Field, StrictFloat @@ -33,6 +33,7 @@ class FormatReferenceStructuredObject(AdCPBaseModel): description='Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -40,6 +41,7 @@ class FormatReferenceStructuredObject(AdCPBaseModel): description='Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.', ge=1, ), + NoExplicitNull, ] = None duration_ms: Annotated[ StrictFloat | None, @@ -47,6 +49,7 @@ class FormatReferenceStructuredObject(AdCPBaseModel): description='Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.', ge=1.0, ), + NoExplicitNull, ] = None pixel_ratio: Annotated[ StrictFloat | None, @@ -54,4 +57,5 @@ class FormatReferenceStructuredObject(AdCPBaseModel): description='Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: "display_image", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.', gt=0.0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/frequency_cap.py b/src/adcp/types/domains/core/frequency_cap.py index 6c05043c0..09ccd6bcd 100644 --- a/src/adcp/types/domains/core/frequency_cap.py +++ b/src/adcp/types/domains/core/frequency_cap.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/frequency_cap.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat, model_validator from ..enums import reach_unit @@ -25,6 +25,7 @@ class FrequencyCap(AdCPBaseModel): Field( description='Cooldown period between consecutive exposures to the same entity. Prevents back-to-back ad delivery (e.g. {"interval": 60, "unit": "minutes"} for a 1-hour cooldown). Preferred over suppress_minutes.' ), + NoExplicitNull, ] = None suppress_minutes: Annotated[ StrictFloat | None, @@ -33,6 +34,7 @@ class FrequencyCap(AdCPBaseModel): description='Deprecated — use suppress instead. Cooldown period in minutes between consecutive exposures to the same entity (e.g. 60 for a 1-hour cooldown).', ge=0.0, ), + NoExplicitNull, ] = None max_impressions: Annotated[ SchemaInt | None, @@ -40,18 +42,21 @@ class FrequencyCap(AdCPBaseModel): description="Maximum number of impressions per entity per window. For duration windows, implementations typically use a rolling window. campaign applies across the owning field's full flight: the package flight for a targeting overlay, or the MediaBuy flight for a root cap.", ge=1, ), + NoExplicitNull, ] = None per: Annotated[ reach_unit.ReachUnit | None, Field( description='Entity granularity for impression counting. Required when max_impressions is set.' ), + NoExplicitNull, ] = None window: Annotated[ duration.Duration | None, Field( description='Time window for the max_impressions cap (e.g. {"interval": 7, "unit": "days"} or {"interval": 1, "unit": "campaign"} for the full flight). Required when max_impressions is set.' ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/frequency_cap_constraints.py b/src/adcp/types/domains/core/frequency_cap_constraints.py index f2b42c145..3f0e6d035 100644 --- a/src/adcp/types/domains/core/frequency_cap_constraints.py +++ b/src/adcp/types/domains/core/frequency_cap_constraints.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/frequency_cap_constraints.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..enums import frequency_cap_control_mode, frequency_cap_mutable_field, reach_unit @@ -26,6 +26,7 @@ class FrequencyCapConstraints(AdCPBaseModel): Field( description='Logical cap fields that can change after creation. An empty array means create-only. For example, [max_impressions] permits changing the count while keeping per and window fixed. Omission means update support is undeclared and the legacy broad meaning applies.' ), + NoExplicitNull, ] = None supported_control_modes: Annotated[ list[frequency_cap_control_mode.FrequencyCapControlMode] | None, @@ -33,14 +34,17 @@ class FrequencyCapConstraints(AdCPBaseModel): description='FrequencyCap shapes accepted by the product. max_impressions_and_suppress means both controls may appear together and are enforced with AND semantics.', min_length=1, ), + NoExplicitNull, ] = None supported_per_units: Annotated[ list[reach_unit.ReachUnit] | None, Field(description='Entity granularities accepted by max_impressions caps.', min_length=1), + NoExplicitNull, ] = None max_impressions_constraints: Annotated[ frequency_cap_impression_constraints.FrequencyCapImpressionConstraints | None, Field(description='Exact supported max_impressions presets or range.'), + NoExplicitNull, ] = None window_constraints: Annotated[ list[frequency_cap_interval_constraints.FrequencyCapIntervalConstraints] | None, @@ -48,6 +52,7 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Exact supported max_impressions.window intervals, one entry per duration unit.', min_length=1, ), + NoExplicitNull, ] = None suppression_constraints: Annotated[ list[frequency_cap_interval_constraints.FrequencyCapIntervalConstraints] | None, @@ -55,8 +60,9 @@ class FrequencyCapConstraints(AdCPBaseModel): description='Exact supported suppress cooldown intervals, one entry per duration unit.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> FrequencyCapConstraints: diff --git a/src/adcp/types/domains/core/frequency_cap_impression_constraints.py b/src/adcp/types/domains/core/frequency_cap_impression_constraints.py index 1adf67658..d464d7ffd 100644 --- a/src/adcp/types/domains/core/frequency_cap_impression_constraints.py +++ b/src/adcp/types/domains/core/frequency_cap_impression_constraints.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/frequency_cap_impression_constraints.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, model_validator @@ -25,10 +25,10 @@ class FrequencyCapImpressionConstraints(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - minimum: Annotated[SchemaInt | None, Field(ge=1)] = None - maximum: Annotated[SchemaInt | None, Field(ge=1)] = None - allowed_values: Annotated[list[AllowedValue] | None, Field(min_length=1)] = None - ext: ext_1.ExtensionObject | None = None + minimum: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + maximum: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + allowed_values: Annotated[list[AllowedValue] | None, Field(min_length=1), NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> FrequencyCapImpressionConstraints: diff --git a/src/adcp/types/domains/core/frequency_cap_interval_constraints.py b/src/adcp/types/domains/core/frequency_cap_interval_constraints.py index d4e37447c..5be1b9f2e 100644 --- a/src/adcp/types/domains/core/frequency_cap_interval_constraints.py +++ b/src/adcp/types/domains/core/frequency_cap_interval_constraints.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/frequency_cap_interval_constraints.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, model_validator @@ -27,10 +27,12 @@ class FrequencyCapIntervalConstraints(AdCPBaseModel): extra='forbid', ) unit: frequency_cap_duration_unit.FrequencyCapDurationUnit - minimum_interval: Annotated[SchemaInt | None, Field(ge=1)] = None - maximum_interval: Annotated[SchemaInt | None, Field(ge=1)] = None - allowed_intervals: Annotated[list[AllowedInterval] | None, Field(min_length=1)] = None - ext: ext_1.ExtensionObject | None = None + minimum_interval: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + maximum_interval: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + allowed_intervals: Annotated[ + list[AllowedInterval] | None, Field(min_length=1), NoExplicitNull + ] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> FrequencyCapIntervalConstraints: diff --git a/src/adcp/types/domains/core/frequency_cap_requirements.py b/src/adcp/types/domains/core/frequency_cap_requirements.py index 0a034bfd0..3f8555582 100644 --- a/src/adcp/types/domains/core/frequency_cap_requirements.py +++ b/src/adcp/types/domains/core/frequency_cap_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/frequency_cap_requirements.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import frequency_cap_control_mode, frequency_cap_mutable_field, reach_unit @@ -23,17 +23,23 @@ class FrequencyCapRequirements(AdCPBaseModel): description='Require each listed logical field to be mutable after creation. Matches a product whose mutable_fields contains every listed value, or a product that omits mutable_fields, or legacy frequency_cap: true. A product declaring mutable_fields: [] is create-only and never matches a non-empty list.', min_length=1, ), + NoExplicitNull, ] = None supported_control_modes: Annotated[ - list[frequency_cap_control_mode.FrequencyCapControlMode] | None, Field(min_length=1) + list[frequency_cap_control_mode.FrequencyCapControlMode] | None, + Field(min_length=1), + NoExplicitNull, + ] = None + supported_per_units: Annotated[ + list[reach_unit.ReachUnit] | None, Field(min_length=1), NoExplicitNull ] = None - supported_per_units: Annotated[list[reach_unit.ReachUnit] | None, Field(min_length=1)] = None supported_window_units: Annotated[ list[frequency_cap_duration_unit.FrequencyCapDurationUnit] | None, Field( description='Require each listed unit to be a supported max_impressions.window unit after seller-wide inheritance.', min_length=1, ), + NoExplicitNull, ] = None supported_suppression_units: Annotated[ list[frequency_cap_duration_unit.FrequencyCapDurationUnit] | None, @@ -41,4 +47,5 @@ class FrequencyCapRequirements(AdCPBaseModel): description='Require each listed unit to be a supported suppress unit after seller-wide inheritance.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/generation_credential.py b/src/adcp/types/domains/core/generation_credential.py index 88054752f..ec992dabd 100644 --- a/src/adcp/types/domains/core/generation_credential.py +++ b/src/adcp/types/domains/core/generation_credential.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/generation_credential.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..enums import right_use @@ -38,11 +38,13 @@ class GenerationCredential(AdCPBaseModel): Field( description='When this credential expires. Key lifetime is determined by the provider.' ), + NoExplicitNull, ] = None endpoint: Annotated[ AnyUrl | None, Field( description="Provider API endpoint to use with this credential, if different from the provider's default" ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/geo_breakdown_support.py b/src/adcp/types/domains/core/geo_breakdown_support.py index c69e3f8a1..e7b331baf 100644 --- a/src/adcp/types/domains/core/geo_breakdown_support.py +++ b/src/adcp/types/domains/core/geo_breakdown_support.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/geo_breakdown_support.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..enums import metro_system @@ -20,20 +20,24 @@ class GeographicBreakdownSupport(AdCPBaseModel): country: Annotated[ StrictBool | None, Field(description='Supports country-level geo breakdown (ISO 3166-1 alpha-2)'), + NoExplicitNull, ] = None region: Annotated[ StrictBool | None, Field(description='Supports region/state-level geo breakdown (ISO 3166-2)'), + NoExplicitNull, ] = None metro: Annotated[ dict[metro_system.MetroAreaSystem, StrictBool] | None, Field( description='Metro area breakdown support. Keys are metro-system enum values; true means supported.' ), + NoExplicitNull, ] = None postal_area: Annotated[ postal_area_support.PostalAreaSupport | None, Field( description='Postal area breakdown support. Prefer the native country-keyed map where each ISO 3166-1 alpha-2 country lists supported country-local postal systems. Deprecated legacy country-fused postal-system boolean maps remain accepted for compatibility.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/geo_delivery_metrics.py b/src/adcp/types/domains/core/geo_delivery_metrics.py index 05560e37c..e025bb924 100644 --- a/src/adcp/types/domains/core/geo_delivery_metrics.py +++ b/src/adcp/types/domains/core/geo_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/geo_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from ..enums import geo_level as geo_level_1 @@ -22,6 +23,7 @@ class GeoDeliveryMetrics(DeliveryMetrics): Field( description='Classification system for metro or postal_area levels. Metro rows use metro-system values. Native postal rows use country-local postal-system values with country; deprecated legacy postal rows may use legacy-postal-system values.' ), + NoExplicitNull, ] = None country: Annotated[ str | None, @@ -29,6 +31,7 @@ class GeoDeliveryMetrics(DeliveryMetrics): description='ISO 3166-1 alpha-2 country code for native postal_area rows.', pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None geo_code: Annotated[ str, @@ -41,6 +44,7 @@ class GeoDeliveryMetrics(DeliveryMetrics): Field( description="Human-readable geographic name (e.g., 'United States', 'California', 'New York DMA')" ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/geo_place_area.py b/src/adcp/types/domains/core/geo_place_area.py index 5268362c4..ae301513e 100644 --- a/src/adcp/types/domains/core/geo_place_area.py +++ b/src/adcp/types/domains/core/geo_place_area.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/geo_place_area.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -37,6 +37,7 @@ class GeographicPlaceArea(AdCPBaseModel): description="Optional exact catalog version from the seller's declared supported_versions. When omitted, the seller applies catalog.current_version and MUST echo that version on persisted package state.", min_length=1, ), + NoExplicitNull, ] = None place_type: geo_place_type.GeographicPlaceType values: Annotated[ @@ -51,5 +52,6 @@ class GeographicPlaceArea(AdCPBaseModel): Field( description='Optional human-readable diagnostic labels keyed by identifiers present in values. Extra keys are a conformance error. Labels are non-authoritative and MUST NOT be used to resolve or apply targeting.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/geo_place_catalog_capability.py b/src/adcp/types/domains/core/geo_place_catalog_capability.py index f4be47dc1..65077d279 100644 --- a/src/adcp/types/domains/core/geo_place_catalog_capability.py +++ b/src/adcp/types/domains/core/geo_place_catalog_capability.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/geo_place_catalog_capability.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field @@ -27,6 +27,7 @@ class GeographicPlaceCatalogCapability(AdCPBaseModel): Field( description='Optional catalog dataset or derivative-source identifier. This records provenance/coverage and does not change the identifier namespace in system.' ), + NoExplicitNull, ] = None current_version: Annotated[ str, diff --git a/src/adcp/types/domains/core/geo_place_catalog_entry.py b/src/adcp/types/domains/core/geo_place_catalog_entry.py index 6967d6b1f..0976189b1 100644 --- a/src/adcp/types/domains/core/geo_place_catalog_entry.py +++ b/src/adcp/types/domains/core/geo_place_catalog_entry.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/geo_place_catalog_entry.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -47,6 +47,7 @@ class GeographicPlaceCatalogEntry(AdCPBaseModel): description='ISO 3166-2 subdivision containing the place when the catalog has a subdivision mapping. Required by resolver semantics when the request constrained subdivision.', pattern='^[A-Z]{2}-[A-Z0-9]{1,3}$', ), + NoExplicitNull, ] = None place_type: geo_place_type.GeographicPlaceType label: Annotated[ @@ -77,11 +78,13 @@ class GeographicPlaceCatalogEntry(AdCPBaseModel): description='Replacement identifiers in the same system and response system_version.', min_length=1, ), + NoExplicitNull, ] = None valid_until: Annotated[ AwareDatetime | None, Field( description='Known time after which the identifier must no longer be accepted for new targeting.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/geo_place_requirement.py b/src/adcp/types/domains/core/geo_place_requirement.py index 5af677c53..734dc3901 100644 --- a/src/adcp/types/domains/core/geo_place_requirement.py +++ b/src/adcp/types/domains/core/geo_place_requirement.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/geo_place_requirement.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey +from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StringConstraints @@ -32,6 +32,7 @@ class CatalogRequirement(AdCPBaseModel): description='Optional exact catalog versions that must remain selectable. Versions are opaque strings, not ordered ranges.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/geo_region_support.py b/src/adcp/types/domains/core/geo_region_support.py index e43e5c62c..876683659 100644 --- a/src/adcp/types/domains/core/geo_region_support.py +++ b/src/adcp/types/domains/core/geo_region_support.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/geo_region_support.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from datetime import date from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StringConstraints @@ -61,18 +61,21 @@ class GeographicRegionSupport(AdCPBaseModel): description='Optional opaque ISO subdivision catalog or seller-mapping version that identifies the support snapshot. It is provenance, not part of subdivision identity.', min_length=1, ), + NoExplicitNull, ] = None as_of: Annotated[ date | None, Field( description='Optional date on which the seller last validated this support declaration.' ), + NoExplicitNull, ] = None max_values_per_package: Annotated[ SchemaInt | None, Field( description='Optional maximum number of region values selectable on one package.', ge=1 ), + NoExplicitNull, ] = None max_packages: Annotated[ SchemaInt | None, @@ -80,5 +83,6 @@ class GeographicRegionSupport(AdCPBaseModel): description='Optional maximum number of independently region-targeted packages created from this configured product.', ge=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/get_geo_place_resolution_request.py b/src/adcp/types/domains/core/get_geo_place_resolution_request.py index 9aaefa7ff..e579c7bb1 100644 --- a/src/adcp/types/domains/core/get_geo_place_resolution_request.py +++ b/src/adcp/types/domains/core/get_geo_place_resolution_request.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/get_geo_place_resolution_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, model_validator from . import geo_place_type, locale_tag @@ -22,6 +22,7 @@ class GetGeographicPlaceResolutionRequest(AdCPBaseModel): q: Annotated[ str | None, Field(description='Unresolved place name or alias supplied by the user.', min_length=1), + NoExplicitNull, ] = None value: Annotated[ str | None, @@ -29,6 +30,7 @@ class GetGeographicPlaceResolutionRequest(AdCPBaseModel): description='Existing identifier to look up in the requested/current catalog version for lifecycle refresh or replacement discovery. Mutually exclusive with q.', min_length=1, ), + NoExplicitNull, ] = None country: Annotated[ str, @@ -43,18 +45,21 @@ class GetGeographicPlaceResolutionRequest(AdCPBaseModel): description='Optional ISO 3166-2 subdivision constraint.', pattern='^[A-Z]{2}-[A-Z0-9]{1,3}$', ), + NoExplicitNull, ] = None - place_type: geo_place_type.GeographicPlaceType | None = None + place_type: Annotated[geo_place_type.GeographicPlaceType | None, NoExplicitNull] = None system_version: Annotated[ str | None, Field(description='Optional exact supported catalog version to search.', min_length=1), + NoExplicitNull, ] = None locale: Annotated[ locale_tag.LanguageTag | None, Field(description='Optional BCP 47 language tag for returned labels.'), + NoExplicitNull, ] = None - cursor: Annotated[str | None, Field(min_length=1)] = None - limit: Annotated[SchemaInt | None, Field(ge=1, le=100)] = 20 + cursor: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + limit: Annotated[SchemaInt | None, Field(ge=1, le=100), NoExplicitNull] = 20 @model_validator(mode='after') def _require_schema_required_group(self) -> GetGeographicPlaceResolutionRequest: diff --git a/src/adcp/types/domains/core/get_geo_place_resolution_response.py b/src/adcp/types/domains/core/get_geo_place_resolution_response.py index b1cef6059..02357922e 100644 --- a/src/adcp/types/domains/core/get_geo_place_resolution_response.py +++ b/src/adcp/types/domains/core/get_geo_place_resolution_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/get_geo_place_resolution_response.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import geo_place_catalog_entry, geo_place_system, get_geo_place_resolution_request @@ -25,4 +25,4 @@ class GetGeographicPlaceResolutionResponse(AdCPBaseModel): system: geo_place_system.GeographicPlaceIdentifierSystem system_version: Annotated[str, Field(min_length=1)] matches: list[geo_place_catalog_entry.GeographicPlaceCatalogEntry] - next_cursor: Annotated[str | None, Field(min_length=1)] = None + next_cursor: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/hotel_item.py b/src/adcp/types/domains/core/hotel_item.py index 30f7a7e63..431733ca9 100644 --- a/src/adcp/types/domains/core/hotel_item.py +++ b/src/adcp/types/domains/core/hotel_item.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/hotel_item.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from datetime import date from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -32,12 +32,18 @@ class Address(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - street: Annotated[str | None, Field(description='Street address.')] = None - city: Annotated[str | None, Field(description='City name.')] = None - region: Annotated[str | None, Field(description='State, province, or region.')] = None - postal_code: Annotated[str | None, Field(description='Postal or ZIP code.')] = None + street: Annotated[str | None, Field(description='Street address.'), NoExplicitNull] = None + city: Annotated[str | None, Field(description='City name.'), NoExplicitNull] = None + region: Annotated[ + str | None, Field(description='State, province, or region.'), NoExplicitNull + ] = None + postal_code: Annotated[str | None, Field(description='Postal or ZIP code.'), NoExplicitNull] = ( + None + ) country: Annotated[ - str | None, Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$') + str | None, + Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$'), + NoExplicitNull, ] = None @@ -56,28 +62,42 @@ class HotelItem(AdCPBaseModel): Field(description="Property name (e.g., 'Grand Hotel Amsterdam', 'Seaside Resort & Spa')."), ] description: Annotated[ - str | None, Field(description='Property description highlighting features and location.') + str | None, + Field(description='Property description highlighting features and location.'), + NoExplicitNull, ] = None location: Annotated[Location, Field(description='Geographic coordinates of the property.')] address: Annotated[ - Address | None, Field(description='Structured address for display and geocoding.') + Address | None, + Field(description='Structured address for display and geocoding.'), + NoExplicitNull, ] = None star_rating: Annotated[ - SchemaInt | None, Field(description='Official star rating (1–5).', ge=1, le=5) + SchemaInt | None, + Field(description='Official star rating (1–5).', ge=1, le=5), + NoExplicitNull, ] = None price: Annotated[ price_1.Price | None, Field(description="Nightly rate or starting price. Use period 'night' for nightly rates."), + NoExplicitNull, + ] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Primary property image URL.'), NoExplicitNull + ] = None + url: Annotated[ + AnyUrl | None, Field(description='Property landing page or booking URL.'), NoExplicitNull + ] = None + phone: Annotated[ + str | None, Field(description='Property phone number in E.164 format.'), NoExplicitNull ] = None - image_url: Annotated[AnyUrl | None, Field(description='Primary property image URL.')] = None - url: Annotated[AnyUrl | None, Field(description='Property landing page or booking URL.')] = None - phone: Annotated[str | None, Field(description='Property phone number in E.164 format.')] = None amenities: Annotated[ list[str] | None, Field( description="Property amenities (e.g., 'pool', 'wifi', 'spa', 'parking', 'restaurant').", min_length=1, ), + NoExplicitNull, ] = None check_in_time: Annotated[ str | None, @@ -85,6 +105,7 @@ class HotelItem(AdCPBaseModel): description="Standard check-in time in HH:MM format (e.g., '15:00').", pattern='^[0-2][0-9]:[0-5][0-9]$', ), + NoExplicitNull, ] = None check_out_time: Annotated[ str | None, @@ -92,6 +113,7 @@ class HotelItem(AdCPBaseModel): description="Standard check-out time in HH:MM format (e.g., '11:00').", pattern='^[0-2][0-9]:[0-5][0-9]$', ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -99,18 +121,21 @@ class HotelItem(AdCPBaseModel): description="Tags for filtering and targeting (e.g., 'boutique', 'family', 'business', 'luxury').", min_length=1, ), + NoExplicitNull, ] = None valid_from: Annotated[ date | None, Field( description="Date from which this item is available or this rate applies (ISO 8601, e.g., '2025-03-01'). Used for seasonal availability windows in feed imports." ), + NoExplicitNull, ] = None valid_to: Annotated[ date | None, Field( description="Date until which this item is available or this rate applies (ISO 8601, e.g., '2025-09-30'). Used for seasonal availability windows in feed imports." ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -118,5 +143,6 @@ class HotelItem(AdCPBaseModel): description="Typed creative asset pools for this hotel. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (16:9 hero images), 'images_vertical' (9:16 for Snap, Stories), 'images_square' (1:1), 'logo'. Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/impairment.py b/src/adcp/types/domains/core/impairment.py index 14d2e4a71..fd4ca21be 100644 --- a/src/adcp/types/domains/core/impairment.py +++ b/src/adcp/types/domains/core/impairment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/impairment.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import impairment_offline_state, impairment_reason_code @@ -32,6 +32,7 @@ class Transition(AdCPBaseModel): description="Prior status of the resource (e.g., 'ready', 'approved', 'good'). Optional — sellers SHOULD include when known, MAY omit when the resource was discovered already in an offline state (e.g., a property depublished via brand.json crawl with no prior snapshot). Open string at the schema layer because each resource_type has its own serviceable-state vocabulary; the pattern constraint blocks free-form garbage, and the impairment.coherence assertion validates that 'from' is a known serviceable value for the resource_type.", pattern='^[a-z][a-z0-9_]*$', ), + NoExplicitNull, ] = None to: Annotated[ impairment_offline_state.ImpairmentOfflineState, @@ -86,6 +87,7 @@ class Impairment(AdCPBaseModel): description='Human-readable explanation. Supplements reason_code with seller-specific detail.', max_length=500, ), + NoExplicitNull, ] = None observed_at: Annotated[ AwareDatetime, @@ -99,4 +101,5 @@ class Impairment(AdCPBaseModel): description='Action the buyer can take to clear the impairment, if any. Free text. Absent when no buyer-side remediation is possible (e.g., seller-initiated withdrawal pending re-publication).', max_length=500, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/indicator.py b/src/adcp/types/domains/core/indicator.py index 03c30f6a0..e292654f2 100644 --- a/src/adcp/types/domains/core/indicator.py +++ b/src/adcp/types/domains/core/indicator.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/indicator.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import indicator_type @@ -24,6 +24,7 @@ class Indicator(AdCPBaseModel): Field( description='When the seller first detected the current uninterrupted occurrence of this indicator. Keep this value stable while the condition remains present. If the condition clears and is later detected again, use the new detection time. Optional because some upstream platforms expose current assessments without an original detection timestamp.' ), + NoExplicitNull, ] = None scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -31,10 +32,12 @@ class Indicator(AdCPBaseModel): description='Optional narrower publisher or placement scope within the enclosing media buy, package, or package–creative assignment. Omit only when the seller evaluated and asserts the indicator across the whole enclosing resource/relationship. When partial indicators_evaluated_scope is declared, every returned indicator MUST include scope and every entry MUST fall within that coverage. This is scope, not source: the responding seller remains the source.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Seller- or provider-specific detail such as scores, thresholds, evaluation windows, methodology identifiers, or upstream attribution. Core consumers must not need ext to understand the broad meaning of type.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/indicator_bearing.py b/src/adcp/types/domains/core/indicator_bearing.py index 3d5b17295..0ac4b8057 100644 --- a/src/adcp/types/domains/core/indicator_bearing.py +++ b/src/adcp/types/domains/core/indicator_bearing.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/indicator_bearing.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import indicator_type @@ -22,6 +22,7 @@ class IndicatorBearingResourceState(AdCPBaseModel): Field( description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' ), + NoExplicitNull, ] = None indicator_types_evaluated: Annotated[ list[indicator_type.IndicatorType] | None, @@ -29,12 +30,14 @@ class IndicatorBearingResourceState(AdCPBaseModel): description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', min_length=1, ), + NoExplicitNull, ] = None indicators_as_of: Annotated[ AwareDatetime | None, Field( description='When the seller completed this evaluation. Required whenever indicators is present, including an empty array. State changes require a strictly newer timestamp; equal-timestamp conflicts are invalid and buyers retain stored state.' ), + NoExplicitNull, ] = None indicators_evaluated_scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -42,4 +45,5 @@ class IndicatorBearingResourceState(AdCPBaseModel): description='Optional publisher or placement coverage for this evaluation. Omit when the named indicator types were evaluated across the whole enclosing resource or relationship. Unlisted scopes remain unknown. When present, every returned indicator MUST include scope and every scope entry MUST be contained by this coverage.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/indicator_scope.py b/src/adcp/types/domains/core/indicator_scope.py index 4d13d271b..7e57675a0 100644 --- a/src/adcp/types/domains/core/indicator_scope.py +++ b/src/adcp/types/domains/core/indicator_scope.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/indicator_scope.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -26,4 +26,5 @@ class IndicatorScope(AdCPBaseModel): Field( description='Optional placement ID within publisher_domain. Omit to scope the assertion or evaluation to all delivery for the publisher in the enclosing object.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/indicators_changed_webhook.py b/src/adcp/types/domains/core/indicators_changed_webhook.py index 9f5a27f27..c66092ae9 100644 --- a/src/adcp/types/domains/core/indicators_changed_webhook.py +++ b/src/adcp/types/domains/core/indicators_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/indicators_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import indicator_type @@ -61,8 +61,8 @@ class IndicatorsChangedWebhook(AdCPBaseModel): RelationshipKind, Field(description='Identifies which indicator-bearing snapshot changed.') ] media_buy_id: str - package_id: str | None = None - creative_id: str | None = None + package_id: Annotated[str | None, NoExplicitNull] = None + creative_id: Annotated[str | None, NoExplicitNull] = None change_kind: Annotated[ ChangeKind, Field( @@ -79,4 +79,4 @@ class IndicatorsChangedWebhook(AdCPBaseModel): observed_at: Annotated[ AwareDatetime, Field(description='When the seller observed the semantic snapshot change.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/insertion_order.py b/src/adcp/types/domains/core/insertion_order.py index 3829890d7..452405186 100644 --- a/src/adcp/types/domains/core/insertion_order.py +++ b/src/adcp/types/domains/core/insertion_order.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/insertion_order.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat @@ -31,15 +31,27 @@ class Terms(AdCPBaseModel): extra='allow', ) advertiser: Annotated[ - str | None, Field(description='Advertiser name or identifier', max_length=500) + str | None, + Field(description='Advertiser name or identifier', max_length=500), + NoExplicitNull, ] = None publisher: Annotated[ - str | None, Field(description='Publisher name or identifier', max_length=500) + str | None, + Field(description='Publisher name or identifier', max_length=500), + NoExplicitNull, + ] = None + total_budget: Annotated[ + TotalBudget | None, Field(description='Total committed budget'), NoExplicitNull + ] = None + flight_start: Annotated[ + AwareDatetime | None, Field(description='Campaign start date'), NoExplicitNull + ] = None + flight_end: Annotated[ + AwareDatetime | None, Field(description='Campaign end date'), NoExplicitNull + ] = None + payment_terms: Annotated[ + PaymentTerms | None, Field(description='Payment terms'), NoExplicitNull ] = None - total_budget: Annotated[TotalBudget | None, Field(description='Total committed budget')] = None - flight_start: Annotated[AwareDatetime | None, Field(description='Campaign start date')] = None - flight_end: Annotated[AwareDatetime | None, Field(description='Campaign end date')] = None - payment_terms: Annotated[PaymentTerms | None, Field(description='Payment terms')] = None class InsertionOrder(AdCPBaseModel): @@ -58,18 +70,21 @@ class InsertionOrder(AdCPBaseModel): Field( description='Summary fields echoed from the committed proposal for agent verification. Buyer agents use these to confirm the IO matches what was negotiated before a human signs. These are read-only summaries, not negotiation surfaces — deal terms live on products and packages.' ), + NoExplicitNull, ] = None terms_url: Annotated[ AnyUrl | None, Field( description='URL to a human-readable document containing the full insertion order terms' ), + NoExplicitNull, ] = None signing_url: Annotated[ AnyUrl | None, Field( description='URL to an electronic signing service (e.g., DocuSign) for human signature workflows. When present, a human must sign before the buyer agent can proceed with create_media_buy.' ), + NoExplicitNull, ] = None requires_signature: Annotated[ StrictBool, diff --git a/src/adcp/types/domains/core/installment.py b/src/adcp/types/domains/core/installment.py index 6a5ccfe09..86ac0dfec 100644 --- a/src/adcp/types/domains/core/installment.py +++ b/src/adcp/types/domains/core/installment.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/installment.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import derivative_type, installment_status @@ -43,6 +43,7 @@ class Installment(AdCPBaseModel): Field( description='Canonical parent collection identity. Products spanning multiple collections or publisher namespaces MUST use this field.' ), + NoExplicitNull, ] = None collection_id: Annotated[ str | None, @@ -50,74 +51,93 @@ class Installment(AdCPBaseModel): deprecated=True, description='Deprecated publisher-domain-free parent collection shorthand. Use collection_ref. It is unambiguous only when the enclosing product addresses one publisher namespace.', ), + NoExplicitNull, ] = None - name: Annotated[str | None, Field(description='Installment title')] = None + name: Annotated[str | None, Field(description='Installment title'), NoExplicitNull] = None season: Annotated[ - str | None, Field(description="Season identifier (e.g., '1', '2024', 'spring_2026')") + str | None, + Field(description="Season identifier (e.g., '1', '2024', 'spring_2026')"), + NoExplicitNull, ] = None installment_number: Annotated[ - str | None, Field(description="Installment number within the season (e.g., '3', '47')") + str | None, + Field(description="Installment number within the season (e.g., '3', '47')"), + NoExplicitNull, ] = None scheduled_at: Annotated[ - AwareDatetime | None, Field(description='When the installment airs or publishes (ISO 8601)') + AwareDatetime | None, + Field(description='When the installment airs or publishes (ISO 8601)'), + NoExplicitNull, ] = None status: Annotated[ installment_status.InstallmentStatus | None, Field(description='Lifecycle status of the installment'), + NoExplicitNull, ] = None duration_seconds: Annotated[ - SchemaInt | None, Field(description='Expected duration of the installment in seconds', ge=0) + SchemaInt | None, + Field(description='Expected duration of the installment in seconds', ge=0), + NoExplicitNull, ] = None flexible_end: Annotated[ StrictBool | None, Field(description='Whether the end time is approximate (live events, sports)'), + NoExplicitNull, ] = None valid_until: Annotated[ AwareDatetime | None, Field( description='When this installment data expires and should be re-queried. Agents should re-query before committing budget to products with tentative installments.' ), + NoExplicitNull, ] = None content_rating: Annotated[ content_rating_1.ContentRating | None, Field( description="Installment-specific content rating. Overrides the collection's baseline content_rating when present." ), + NoExplicitNull, ] = None topics: Annotated[ list[str] | None, Field( description="Content topics for this installment. Uses the same taxonomy as the collection's genre_taxonomy when present. Enables installment-level brand safety evaluation beyond content_rating." ), + NoExplicitNull, ] = None special: Annotated[ special_1.Special | None, Field( description='Installment-specific event context. When present, this installment is anchored to a real-world event. Overrides the collection-level special when present.' ), + NoExplicitNull, ] = None guest_talent: Annotated[ list[talent.Talent] | None, Field( description="Installment-specific guests and talent. Additive to the collection's recurring talent." ), + NoExplicitNull, ] = None ad_inventory: Annotated[ ad_inventory_config.AdInventoryConfiguration | None, Field( description='Break-based ad inventory for this installment. For non-break formats (host reads, integrations), use product placements.' ), + NoExplicitNull, ] = None deadlines: Annotated[ installment_deadlines.InstallmentDeadlines | None, Field( description='Booking, cancellation, and material submission deadlines for this installment. Present when the installment has time-sensitive inventory that requires advance commitment or material delivery.' ), + NoExplicitNull, ] = None derivative_of: Annotated[ DerivativeOf | None, Field( description='When this installment is a clip, highlight, or recap derived from a full installment. The source installment_id must reference an installment within the same response.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/installment_deadlines.py b/src/adcp/types/domains/core/installment_deadlines.py index 98619ea9a..6d9b4dbed 100644 --- a/src/adcp/types/domains/core/installment_deadlines.py +++ b/src/adcp/types/domains/core/installment_deadlines.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/installment_deadlines.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import material_deadline @@ -21,12 +21,14 @@ class InstallmentDeadlines(AdCPBaseModel): Field( description='Last date/time to book a placement in this installment (ISO 8601). After this point, the seller will not accept new bookings.' ), + NoExplicitNull, ] = None cancellation_deadline: Annotated[ AwareDatetime | None, Field( description="Last date/time to cancel without penalty (ISO 8601). Cancellations after this point may incur fees per the seller's terms." ), + NoExplicitNull, ] = None material_deadlines: Annotated[ list[material_deadline.MaterialDeadline] | None, @@ -34,4 +36,5 @@ class InstallmentDeadlines(AdCPBaseModel): description="Stages for creative material submission. Items MUST be in chronological order by due_at (earliest first). Typical pattern: 'draft' for raw materials the seller will process, 'final' for production-ready assets. Print example: draft artwork then press-ready PDF. Influencer example: talking points then approved script.", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/installment_delivery_metrics.py b/src/adcp/types/domains/core/installment_delivery_metrics.py index f8a824060..26ae391b1 100644 --- a/src/adcp/types/domains/core/installment_delivery_metrics.py +++ b/src/adcp/types/domains/core/installment_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/installment_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import AwareDatetime, Field from . import installment_ref as installment_ref_1 @@ -19,12 +20,14 @@ class InstallmentDeliveryMetrics(DeliveryMetrics): Field( description='Current human-readable installment name. Convenience metadata only; installment_ref is stable identity.' ), + NoExplicitNull, ] = None scheduled_at: Annotated[ AwareDatetime | None, Field( description="Convenience echo of the installment's scheduled publication or air time." ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/installment_property_delivery_metrics.py b/src/adcp/types/domains/core/installment_property_delivery_metrics.py index 483577736..5819c2ff6 100644 --- a/src/adcp/types/domains/core/installment_property_delivery_metrics.py +++ b/src/adcp/types/domains/core/installment_property_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/installment_property_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import AwareDatetime, Field from . import identifier as identifier_1 @@ -19,12 +20,14 @@ class InstallmentPropertyDeliveryMetrics(DeliveryMetrics): installment_name: Annotated[ str | None, Field(description='Current human-readable installment name. Convenience metadata only.'), + NoExplicitNull, ] = None scheduled_at: Annotated[ AwareDatetime | None, Field( description="Convenience echo of the installment's scheduled publication or air time." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str, @@ -42,10 +45,12 @@ class InstallmentPropertyDeliveryMetrics(DeliveryMetrics): Field( description="Canonical publisher-scoped catalog identity when available. Its publisher_domain MUST equal the row's publisher_domain." ), + NoExplicitNull, ] = None property_name: Annotated[ str | None, Field(description='Current human-readable property name. Convenience metadata only.'), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/job_item.py b/src/adcp/types/domains/core/job_item.py index 26139ed2f..6833d3895 100644 --- a/src/adcp/types/domains/core/job_item.py +++ b/src/adcp/types/domains/core/job_item.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/job_item.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -42,8 +42,12 @@ class Salary(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[StrictFloat | None, Field(description='Minimum salary.', ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(description='Maximum salary.', ge=0.0)] = None + min: Annotated[ + StrictFloat | None, Field(description='Minimum salary.', ge=0.0), NoExplicitNull + ] = None + max: Annotated[ + StrictFloat | None, Field(description='Maximum salary.', ge=0.0), NoExplicitNull + ] = None currency: Annotated[str, Field(description='ISO 4217 currency code.', pattern='^[A-Z]{3}$')] period: Annotated[Period, Field(description='Pay period.')] @@ -66,30 +70,35 @@ class JobItem(AdCPBaseModel): Field( description="Job location as a display string (e.g., 'Amsterdam, NL', 'Remote', 'New York, NY'). Use 'Remote' for fully remote positions." ), + NoExplicitNull, + ] = None + employment_type: Annotated[ + EmploymentType | None, Field(description='Type of employment.'), NoExplicitNull ] = None - employment_type: Annotated[EmploymentType | None, Field(description='Type of employment.')] = ( - None - ) experience_level: Annotated[ - ExperienceLevel | None, Field(description='Required experience level.') + ExperienceLevel | None, Field(description='Required experience level.'), NoExplicitNull ] = None salary: Annotated[ Salary | None, Field(description='Salary range. Specify min and/or max with currency and period.'), + NoExplicitNull, ] = None date_posted: Annotated[ - date | None, Field(description='Date the job was posted (ISO 8601 date).') + date | None, Field(description='Date the job was posted (ISO 8601 date).'), NoExplicitNull ] = None valid_through: Annotated[ - date | None, Field(description='Application deadline (ISO 8601 date).') + date | None, Field(description='Application deadline (ISO 8601 date).'), NoExplicitNull + ] = None + apply_url: Annotated[ + AnyUrl | None, Field(description='Direct application URL.'), NoExplicitNull ] = None - apply_url: Annotated[AnyUrl | None, Field(description='Direct application URL.')] = None job_functions: Annotated[ list[str] | None, Field( description="Job function categories (e.g., 'engineering', 'marketing', 'sales', 'finance').", min_length=1, ), + NoExplicitNull, ] = None industries: Annotated[ list[str] | None, @@ -97,6 +106,7 @@ class JobItem(AdCPBaseModel): description="Industry classifications (e.g., 'technology', 'healthcare', 'retail').", min_length=1, ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -104,6 +114,7 @@ class JobItem(AdCPBaseModel): description="Tags for filtering (e.g., 'remote', 'visa-sponsorship', 'equity').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -111,5 +122,6 @@ class JobItem(AdCPBaseModel): description="Typed creative asset pools for this job. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (company/role hero), 'images_vertical' (9:16 for Stories), 'logo' (company logo). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/keyword_target.py b/src/adcp/types/domains/core/keyword_target.py index bd0f95b11..cdd4df8b9 100644 --- a/src/adcp/types/domains/core/keyword_target.py +++ b/src/adcp/types/domains/core/keyword_target.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/keyword_target.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import match_type as match_type_1 @@ -18,4 +18,4 @@ class KeywordTarget(AdCPBaseModel): ) keyword: Annotated[str, Field(min_length=1)] match_type: match_type_1.MatchType - bid_price: Annotated[StrictFloat | None, Field(ge=0.0)] = None + bid_price: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/limited_series.py b/src/adcp/types/domains/core/limited_series.py index b89c41294..d8b12715e 100644 --- a/src/adcp/types/domains/core/limited_series.py +++ b/src/adcp/types/domains/core/limited_series.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/limited_series.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field @@ -18,8 +18,8 @@ class LimitedSeries(AdCPBaseModel): SchemaInt, Field(description='Planned number of installments in the series', ge=1) ] starts: Annotated[ - AwareDatetime | None, Field(description='When the series begins (ISO 8601)') + AwareDatetime | None, Field(description='When the series begins (ISO 8601)'), NoExplicitNull + ] = None + ends: Annotated[ + AwareDatetime | None, Field(description='When the series ends (ISO 8601)'), NoExplicitNull ] = None - ends: Annotated[AwareDatetime | None, Field(description='When the series ends (ISO 8601)')] = ( - None - ) diff --git a/src/adcp/types/domains/core/macro_declaration.py b/src/adcp/types/domains/core/macro_declaration.py index 5d8879a08..ba983f075 100644 --- a/src/adcp/types/domains/core/macro_declaration.py +++ b/src/adcp/types/domains/core/macro_declaration.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/macro_declaration.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictBool from .assets import asset_union @@ -67,6 +67,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Authority-controlled registry or mapping URI. Required for IAB VAST, IAB DAAST, and vendor dialects.' ), + NoExplicitNull, ] = None dialect_revision: Annotated[ str | None, @@ -74,6 +75,7 @@ class MacroDeclaration(AdCPBaseModel): description='Immutable registry/mapping revision, release, commit, or digest. Required for IAB VAST, IAB DAAST, and vendor dialects.', min_length=1, ), + NoExplicitNull, ] = None dialect_semantic: Annotated[ str, @@ -88,6 +90,7 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Verified AdCP universal meaning. Present only when mapping_status is `verified_universal`.' ), + NoExplicitNull, ] = None operation: asset_union.MacroProcessingOperation performed_by: Annotated[ @@ -95,12 +98,14 @@ class MacroDeclaration(AdCPBaseModel): Field( description='Actor authorized to perform the declared translation or value resolution. Omitted for preservation.' ), + NoExplicitNull, ] = None translation_target: Annotated[ asset_union.MacroTranslationTarget | None, Field( description='Required only for `translate_to_native`. The output token contract that replaces this declaration after translation.' ), + NoExplicitNull, ] = None location: Location encoding: Annotated[ diff --git a/src/adcp/types/domains/core/macro_resolution_capability.py b/src/adcp/types/domains/core/macro_resolution_capability.py index de4877035..fbc5d7db6 100644 --- a/src/adcp/types/domains/core/macro_resolution_capability.py +++ b/src/adcp/types/domains/core/macro_resolution_capability.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/macro_resolution_capability.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import macro_dialect, macro_resolver, macro_value_context, universal_macro @@ -29,11 +29,11 @@ class MacroProcessingCapability(AdCPBaseModel): extra='forbid', ) dialect: macro_dialect.MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: MappingStatus - universal_semantic: universal_macro.UniversalMacro | None = None + universal_semantic: Annotated[universal_macro.UniversalMacro | None, NoExplicitNull] = None operation: Operation performed_by: macro_resolver.MacroProcessingActor supported_contexts: Annotated[list[macro_value_context.MacroValueContext], Field(min_length=1)] @@ -43,5 +43,8 @@ class MacroProcessingCapability(AdCPBaseModel): description='Exact supported encoding profiles, not a maximum depth. Required for value resolution.', min_length=1, ), + NoExplicitNull, + ] = None + translation_target: Annotated[ + macro_translation_target.MacroTranslationTarget | None, NoExplicitNull ] = None - translation_target: macro_translation_target.MacroTranslationTarget | None = None diff --git a/src/adcp/types/domains/core/macro_resolution_result.py b/src/adcp/types/domains/core/macro_resolution_result.py index 5529952a4..fd601c31a 100644 --- a/src/adcp/types/domains/core/macro_resolution_result.py +++ b/src/adcp/types/domains/core/macro_resolution_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/macro_resolution_result.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import ( @@ -45,13 +45,13 @@ class MacroResolutionResult(AdCPBaseModel): ] token: Annotated[str, Field(min_length=1)] dialect: macro_dialect.MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: macro_mapping_status.MacroMappingStatus - universal_semantic: universal_macro.UniversalMacro | None = None + universal_semantic: Annotated[universal_macro.UniversalMacro | None, NoExplicitNull] = None operation: macro_processing_operation.MacroProcessingOperation - performed_by: macro_resolver.MacroProcessingActor | None = None + performed_by: Annotated[macro_resolver.MacroProcessingActor | None, NoExplicitNull] = None requested_encoding: macro_encoding.MacroEncoding required: StrictBool unavailable_behavior: UnavailableBehavior @@ -60,5 +60,6 @@ class MacroResolutionResult(AdCPBaseModel): matched_encodings: Annotated[ list[macro_encoding.MacroEncoding] | None, Field(description='Exact advertised profiles considered for this match, when relevant.'), + NoExplicitNull, ] = None - message: Annotated[str | None, Field(min_length=1)] = None + message: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/macro_translation_target.py b/src/adcp/types/domains/core/macro_translation_target.py index e3d4143ed..88893b2f4 100644 --- a/src/adcp/types/domains/core/macro_translation_target.py +++ b/src/adcp/types/domains/core/macro_translation_target.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/macro_translation_target.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import macro_dialect, macro_mapping_status, macro_resolver, universal_macro @@ -19,11 +19,11 @@ class MacroTranslationTarget(AdCPBaseModel): ) token: Annotated[str, Field(min_length=1)] dialect: macro_dialect.MacroDialectFamily - dialect_namespace: AnyUrl | None = None - dialect_revision: Annotated[str | None, Field(min_length=1)] = None + dialect_namespace: Annotated[AnyUrl | None, NoExplicitNull] = None + dialect_revision: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None dialect_semantic: Annotated[str, Field(min_length=1)] mapping_status: macro_mapping_status.MacroMappingStatus - universal_semantic: universal_macro.UniversalMacro | None = None + universal_semantic: Annotated[universal_macro.UniversalMacro | None, NoExplicitNull] = None next_operation: Literal['resolve_value'] = 'resolve_value' performed_by: macro_resolver.MacroProcessingActor encoding: macro_encoding.MacroEncoding diff --git a/src/adcp/types/domains/core/material_deadline.py b/src/adcp/types/domains/core/material_deadline.py index 03a4deab2..d5eca4985 100644 --- a/src/adcp/types/domains/core/material_deadline.py +++ b/src/adcp/types/domains/core/material_deadline.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/material_deadline.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field @@ -29,4 +29,5 @@ class MaterialDeadline(AdCPBaseModel): Field( description="What the seller needs at this stage (e.g., 'Talking points and brand guidelines', 'Press-ready PDF with bleed')" ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/mcp_webhook_payload.py b/src/adcp/types/domains/core/mcp_webhook_payload.py index 9fad1fac0..d05d7489f 100644 --- a/src/adcp/types/domains/core/mcp_webhook_payload.py +++ b/src/adcp/types/domains/core/mcp_webhook_payload.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/mcp_webhook_payload.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import adcp_protocol, task_status @@ -35,6 +35,7 @@ class McpWebhookPayload(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None operation_id: Annotated[ str, @@ -59,6 +60,7 @@ class McpWebhookPayload(AdCPBaseModel): Field( description='AdCP protocol this task belongs to. Helps classify the operation type at a high level.' ), + NoExplicitNull, ] = None status: Annotated[ task_status.TaskStatus, @@ -77,12 +79,14 @@ class McpWebhookPayload(AdCPBaseModel): Field( description='Human-readable summary of the current task state. Provides context about what happened and what action may be needed.' ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, Field( description='Compatibility metadata copied from the originating response when present. This value alone is not continuation authority and MUST NOT be used to resume input-required or auth-required work or to select session state.' ), + NoExplicitNull, ] = None token: Annotated[ str | None, @@ -91,10 +95,12 @@ class McpWebhookPayload(AdCPBaseModel): max_length=4096, min_length=16, ), + NoExplicitNull, ] = None result: Annotated[ async_response_data.AdcpAsyncResponseData | None, Field( description='Task-specific payload matching the status. For completed/failed, contains the full task response. For working/input-required/submitted, contains status-specific data. This is the data layer that AdCP specs - same structure used in A2A status.message.parts[].data.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/measurement_readiness.py b/src/adcp/types/domains/core/measurement_readiness.py index 79db1b455..8ef0f86b5 100644 --- a/src/adcp/types/domains/core/measurement_readiness.py +++ b/src/adcp/types/domains/core/measurement_readiness.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/measurement_readiness.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import assessment_status, event_type @@ -29,22 +29,26 @@ class MeasurementReadiness(AdCPBaseModel): description='Event types this product needs for effective optimization. Buyers should ensure their event sources cover these types.', min_length=1, ), + NoExplicitNull, ] = None missing_event_types: Annotated[ list[event_type.EventType] | None, Field( description='Event types this product requires that the buyer has not configured. Empty or absent when all required types are covered.' ), + NoExplicitNull, ] = None issues: Annotated[ list[diagnostic_issue.DiagnosticIssue] | None, Field( description='Actionable issues preventing full measurement readiness. Sellers should limit to the top 3-5 most actionable items. Buyer agents should sort by severity rather than relying on array position.' ), + NoExplicitNull, ] = None notes: Annotated[ str | None, Field( description='Seller explanation of the readiness assessment, recommendations for improvement, or context about what the buyer needs to change.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/measurement_terms.py b/src/adcp/types/domains/core/measurement_terms.py index 5e8b7144e..11c0d9720 100644 --- a/src/adcp/types/domains/core/measurement_terms.py +++ b/src/adcp/types/domains/core/measurement_terms.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/measurement_terms.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import makegood_remedy @@ -43,6 +43,7 @@ class BillingMeasurement(AdCPBaseModel): ge=0.0, lt=100.0, ), + NoExplicitNull, ] = None measurement_window: Annotated[ str | None, @@ -59,6 +60,7 @@ class BillingMeasurement(AdCPBaseModel): 'downloads_30d', ], ), + NoExplicitNull, ] = None finalization_deadline_hours: Annotated[ SchemaInt | None, @@ -66,6 +68,7 @@ class BillingMeasurement(AdCPBaseModel): description='Maximum hours by which the authoritative party MUST publish a final record (`is_final: true` / `finalized_at` on `get_media_buy_delivery`, or `final: true` / `finalized_at` on `report_usage`). **Anchor:** when `measurement_window` is set, hours are counted from the close of that window (e.g., 240h after `c7` close = ~10 days after the 7-day DVR accumulation completes); when `measurement_window` is absent, hours are counted from `reporting_period.end`. Picking a single anchor avoids ambiguity for windowed channels where `reporting_period.end` and window close differ by days. The deadline applies to whichever party is named in `vendor` — seller, buyer, or third-party vendor — symmetrically. When the deadline elapses without a final record, the counterparty MAY fall back to its own attestation for invoicing (seller falls back to seller-attested numbers via `get_media_buy_delivery`; buyer falls back to a buyer-attested `report_usage` push), and the breach is treated like any other measurement-terms breach under `makegood_policy`. Absent means no contractual deadline — finalization is best-effort and disagreements resolve out of band.', ge=0, ), + NoExplicitNull, ] = None @@ -78,10 +81,12 @@ class MeasurementTerms(AdCPBaseModel): Field( description="Which vendor's value for the billing metric governs invoicing. The billing metric is determined by the pricing_model on the selected pricing_option (e.g., impressions for CPM, completed views for CPCV, commissionable_value for revenue_share)." ), + NoExplicitNull, ] = None makegood_policy: Annotated[ MakegoodPolicy | None, Field( description='Remedies available when a performance standard or billing measurement variance is breached. Seller declares which remedy types they support. When a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/measurement_window.py b/src/adcp/types/domains/core/measurement_window.py index 073da0fae..953852945 100644 --- a/src/adcp/types/domains/core/measurement_window.py +++ b/src/adcp/types/domains/core/measurement_window.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/measurement_window.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -35,6 +35,7 @@ class MeasurementWindow(AdCPBaseModel): ], max_length=500, ), + NoExplicitNull, ] = None duration_days: Annotated[ SchemaInt, @@ -49,10 +50,12 @@ class MeasurementWindow(AdCPBaseModel): description="Expected number of days after delivery before this window's data is available from the measurement vendor. Captures accumulation time plus vendor processing time. Examples: broadcast C7 from VideoAmp ~22 days (7-day accumulation + ~15-day processing); DOOH tentative plays same-day; DOOH final (post-IVT/fraud-check) ~1 day; digital post-SIVT ~2–3 days.", ge=0, ), + NoExplicitNull, ] = None is_guarantee_basis: Annotated[ StrictBool | None, Field( description="Whether this window is the basis for delivery guarantees, reconciliation, and invoicing. A product typically has one guarantee basis window (e.g., C7 for most US broadcast, post-IVT final for DOOH). Buyers reconcile against the guarantee basis window's final numbers." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/media_buy.py b/src/adcp/types/domains/core/media_buy.py index 0fc9af67a..340a92505 100644 --- a/src/adcp/types/domains/core/media_buy.py +++ b/src/adcp/types/domains/core/media_buy.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import canceled_by as canceled_by_1 @@ -37,6 +37,7 @@ class Cancellation(AdCPBaseModel): reason: Annotated[ str | None, Field(description='Reason provided when the media buy was canceled.', max_length=500), + NoExplicitNull, ] = None @@ -53,6 +54,7 @@ class MediaBuy(AdCPBaseModel): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None accepted_proposal_id: Annotated[ str | None, @@ -61,6 +63,7 @@ class MediaBuy(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None accepted_proposal_terms_digest: Annotated[ str | None, @@ -68,9 +71,12 @@ class MediaBuy(AdCPBaseModel): description='Digest of the current accepted proposal commercial_terms, allowing buyers and governance agents to verify the recovered snapshot.', pattern='^sha256:[A-Za-z0-9_-]{43}$', ), + NoExplicitNull, ] = None account: Annotated[ - account_1.Account | None, Field(description='Account billed for this media buy') + account_1.Account | None, + Field(description='Account billed for this media buy'), + NoExplicitNull, ] = None status: media_buy_status.MediaBuyStatus health: Annotated[ @@ -78,18 +84,21 @@ class MediaBuy(AdCPBaseModel): Field( description="Aggregate health based on open impairments[]. Orthogonal to status — a paused, pending, or active buy can each be impaired. Defaults to 'ok' when impairments[] is empty." ), + NoExplicitNull, ] = media_buy_health.MediaBuyHealth.ok impairments: Annotated[ list[impairment.Impairment] | None, Field( description="Open impairments — upstream dependency state changes that affect delivery for at least one package on this buy. Empty when health is 'ok'. Sellers MUST add an entry on next sync/poll response after a referenced resource transitions to an offline state, and MUST remove the entry (flipping health to 'ok' when the array empties) when the resource returns to a serviceable state. Staleness budget: the snapshot MUST reflect the impairment within 5 minutes of impairment.observed_at regardless of buyer poll cadence — sellers cannot rely on rare buyer polls to defer write propagation. See impairment.coherence assertion for the cross-resource invariant." ), + NoExplicitNull, ] = None rejection_reason: Annotated[ str | None, Field( description="Reason provided by the seller when status is 'rejected'. Present only when status is 'rejected'." ), + NoExplicitNull, ] = None confirmed_at: Annotated[ AwareDatetime | None, @@ -100,6 +109,7 @@ class MediaBuy(AdCPBaseModel): cancellation: Annotated[ Cancellation | None, Field(description="Cancellation metadata. Present only when status is 'canceled'."), + NoExplicitNull, ] = None total_budget: Annotated[ StrictFloat, Field(description='Hard aggregate lifetime budget amount', ge=0.0) @@ -110,18 +120,21 @@ class MediaBuy(AdCPBaseModel): description='Current hard aggregate spend ceiling per calendar day. Sellers MUST echo this whenever an aggregate daily cap is set. It bounds total media-buy spend without allocating or reserving spend for packages.', ge=0.0, ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Current hard MediaBuy-level cap. Sellers MUST echo it whenever set. Its counter aggregates exposures across all participating packages; each package targeting_overlay.frequency_cap remains independently binding.' ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, Field( description='IANA timezone defining the shared calendar-day boundary for every aggregate and package daily cap on this media buy. Sellers MUST echo it whenever any daily cap is set.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -129,24 +142,28 @@ class MediaBuy(AdCPBaseModel): description="Single ISO 4217 denomination for total_budget, every package budget/minimum, and every canonical BiddingPolicy monetary field. Every package's selected pricing option MUST declare this currency; packages requiring another currency belong in a separate media buy.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None budget_allocation: Annotated[ budget_allocation_1.BudgetAllocation | None, Field( description='Accepted cross-package budget allocation configuration. Omitted means fixed allocation for legacy buys.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Aggregate pacing strategy for total_budget across the media-buy flight.' ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Media-buy-authored bidding policy. This is the complete default inherited by packages that omit package.bidding; `{automatic:true}` is an explicit authored automatic policy. In seller-optimized mode, cost_per/roas bind to the primary budget_allocation optimization goal. In fixed mode, inherited cost_per requires compatible package primary-goal result units and inherited roas requires value-bearing primary goals. Monetary fields use media_buy.currency; every affected pricing option MUST declare the same currency. Package overrides are permitted only where advertised; conflicts MUST be rejected atomically with BIDDING_PLACEMENT_CONFLICT.' ), + NoExplicitNull, ] = None packages: Annotated[ list[package.Package], Field(description='Array of packages within this media buy') @@ -156,15 +173,19 @@ class MediaBuy(AdCPBaseModel): Field( description='Opaque media-buy-level correlation data echoed unchanged from the create_media_buy request. Sellers MUST include persisted context on read surfaces such as get_media_buys when the media buy was created through AdCP with context, so buyers can reconcile seller-assigned media_buy_id values with their own tracking state. Sellers MAY omit context for media buys created outside AdCP or created without context. Sellers MUST NOT parse this object for business logic.' ), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description="Per-buy override for who receives the invoice. When provided, the seller invoices this entity instead of the account's default billing_entity. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request." ), + NoExplicitNull, ] = None creative_deadline: Annotated[ - AwareDatetime | None, Field(description='ISO 8601 timestamp for creative upload deadline') + AwareDatetime | None, + Field(description='ISO 8601 timestamp for creative upload deadline'), + NoExplicitNull, ] = None revision: Annotated[ SchemaInt, @@ -173,6 +194,10 @@ class MediaBuy(AdCPBaseModel): ge=1, ), ] - created_at: Annotated[AwareDatetime | None, Field(description='Creation timestamp')] = None - updated_at: Annotated[AwareDatetime | None, Field(description='Last update timestamp')] = None - ext: ext_1.ExtensionObject | None = None + created_at: Annotated[ + AwareDatetime | None, Field(description='Creation timestamp'), NoExplicitNull + ] = None + updated_at: Annotated[ + AwareDatetime | None, Field(description='Last update timestamp'), NoExplicitNull + ] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/media_buy_available_action.py b/src/adcp/types/domains/core/media_buy_available_action.py index e1527e69b..1416f4d94 100644 --- a/src/adcp/types/domains/core/media_buy_available_action.py +++ b/src/adcp/types/domains/core/media_buy_available_action.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/media_buy_available_action.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import media_buy_action_mode @@ -45,19 +45,26 @@ class MediaBuyAvailableAction(AdCPBaseModel): Field( description='Compact-lifecycle task for this resolved action: operational control, commercial refinement, or creative lifecycle mutation.' ), + NoExplicitNull, ] = None sla: Annotated[ sla_window.SlaWindow | None, Field( description='Optional SLA commitment for this action on this buy. Absence means no commitment, not zero commitment.' ), + NoExplicitNull, + ] = None + change_term_id: Annotated[ + media_buy_change_term_id.MediaBuyChangeTermId | None, NoExplicitNull + ] = None + terms_ref: Annotated[ + media_buy_legacy_terms_ref.MediaBuyTermsReference | None, NoExplicitNull ] = None - change_term_id: media_buy_change_term_id.MediaBuyChangeTermId | None = None - terms_ref: media_buy_legacy_terms_ref.MediaBuyTermsReference | None = None applicable_package_ids: Annotated[ list[applicable_package_id.ApplicablePackageId] | None, Field( description='For a package-scoped action, the exact packages currently eligible. Omission means every relevant package. Root actions omit this field.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/media_buy_features.py b/src/adcp/types/domains/core/media_buy_features.py index f41afaf1b..000b2836c 100644 --- a/src/adcp/types/domains/core/media_buy_features.py +++ b/src/adcp/types/domains/core/media_buy_features.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy_features.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Dict -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import bidding_policy_capability @@ -23,58 +23,68 @@ class MediaBuyFeatures(AdCPBaseModel): deprecated=True, description='Deprecated 3.x compatibility capability for creatives provided inline in create_media_buy and update_media_buy package payloads. buy_products, accept_proposal, and control_media_buy never accept inline creatives. New integrations use the dedicated creative lifecycle. Removed in 4.0.', ), + NoExplicitNull, ] = None property_list_filtering: Annotated[ StrictBool | None, Field( description='Honors property_list parameter in get_products to filter results to buyer-approved properties' ), + NoExplicitNull, ] = None catalog_management: Annotated[ StrictBool | None, Field( description='Supports sync_catalogs task for catalog feed management with platform review and approval' ), + NoExplicitNull, ] = None catalog_item_availability_updates: Annotated[ StrictBool | None, Field( description='Supports buyer-pushed item_availability_updates and item_availability_queries on sync_catalogs for immediate suppression/restoration and current-state readback in buyer-managed catalogs. Seller declarations may be true only with catalog_management: true; buyer required_features filters may request this feature alone. Requests containing availability operations are synchronous and accept at most 1,000 combined update and query entries. A successful suppress covers selection, dynamic rendering, and cached or pre-generated creatives materialized from the item. Internal lineage MUST retain resolved_account_id, catalog_id, catalog_generation, and item_id. Static creatives supplied or promoted by the buyer without catalog lineage remain outside this automatic guarantee. Suppression persists across feed refreshes and ordinary upserts until restore, expires_at, or catalog deletion. A seller that does not declare true MUST reject availability operations with UNSUPPORTED_FEATURE before lookup or mutation and MUST NOT interpret them as discovery. This does not control seller-owned wholesale inventory or let restore bypass seller controls.' ), + NoExplicitNull, ] = None committed_metrics_supported: Annotated[ StrictBool | None, Field( description="Seller has per-package snapshot infrastructure for the reporting contract. When true, the seller MUST populate `package.committed_metrics` on committed `create_media_buy` responses where `confirmed_at` is non-null, MUST omit `package.committed_metrics` while `confirmed_at` is null for a provisional buy, and MUST honor append-only mid-flight metric additions via `update_media_buy`. The unified `committed_metrics` array (per the metric-accountability design) covers both standard and vendor-defined metric entries, so a single flag is load-bearing. Buyers filtering on this flag are detecting 'this seller can stamp the reporting contract,' which closes the audit gap from PR #3510 where absence of `committed_metrics` was indistinguishable between 'didn't snapshot' and 'snapshot infrastructure not implemented.'" ), + NoExplicitNull, ] = None seller_optimized_budget: Annotated[ StrictBool | None, Field( description="Supports the core seller-optimized shared-budget contract for budget_allocation.mode `seller_optimized`: one hard shared total_budget, seller allocation of that total across the buy's packages against budget_allocation.optimization_goals, media-buy-level pacing, and echo of the allocation configuration on buy read surfaces. Sellers declaring true MUST accept eligible explicit-package and proposal executions that use only these core controls and MUST enforce the aggregate budget. Core media-buy pacing: sellers declaring true MUST accept omitted media-buy pacing (which defaults to even when total_budget is present) and pacing `even` on seller-optimized buys; they MAY reject `asap` or `front_loaded` with `UNSUPPORTED_FEATURE` (error.field `pacing`) before any provider mutation, and MUST NOT silently coerce them to `even`. Package-level controls inside a seller-optimized buy are separate capabilities: package budget caps (seller_optimized_package_budgets), package minimum-spend targets (seller_optimized_min_spend_targets), and package pacing (seller_optimized_package_pacing). A seller declaring this feature but not one of those sub-capabilities MUST reject any request that would leave that package control on a seller-optimized buy with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, and MUST NOT silently drop, soften, or coerce it. Over-subscription validation (`INVALID_REQUEST`) applies only to package controls the seller has declared; see seller_optimized_min_spend_targets. Product combinations may still be rejected when their currencies, optimization capabilities, pricing terms, or delivery constraints are incompatible. Sellers that do not declare this feature MUST reject any request carrying `budget_allocation.mode: 'seller_optimized'` with `UNSUPPORTED_FEATURE` before any provider mutation, and MUST NOT coerce the request to fixed allocation." ), + NoExplicitNull, ] = None seller_optimized_package_budgets: Annotated[ StrictBool | None, Field( description='Honors package `budget` as an optional hard lifetime package spend cap inside a seller-optimized buy: packages[].budget and new_packages[].budget on create_media_buy and update_media_buy, purchases[].budget on buy_products, package budget controls on control_media_buy, and max_spend_percentage on seller-optimized proposal allocations. The cap is a ceiling, not a reserved or current allocation, and package caps may sum above total_budget. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. A seller that declares seller_optimized_budget without this feature MUST reject a request that would leave a package budget on a seller-optimized buy, including an allocation-mode switch that retains fixed-mode package budgets (the buyer clears them with null in the same atomic update), with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, and MUST NOT issue seller-optimized proposals carrying max_spend_percentage. Does not govern fixed allocation, where package budgets remain required.' ), + NoExplicitNull, ] = None seller_optimized_min_spend_targets: Annotated[ StrictBool | None, Field( description='Honors package `min_spend_target` as a soft lifetime minimum-spend target inside a seller-optimized buy: packages[].min_spend_target and new_packages[].min_spend_target on create_media_buy and update_media_buy, purchases[].min_spend_target on buy_products, package min_spend_target controls on control_media_buy, and min_spend_target_percentage on seller-optimized proposal allocations. The seller SHOULD attempt to deliver at least the target before allocating incremental spend elsewhere; it is not a billing or delivery guarantee. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. Sellers declaring this feature MUST reject package minimum-spend targets summing above total_budget with `INVALID_REQUEST` before mutation, and, when they also declare seller_optimized_package_budgets, MUST likewise reject a min_spend_target above its own package budget. A seller that declares seller_optimized_budget without this feature MUST reject a request carrying a numeric min_spend_target with `UNSUPPORTED_FEATURE` before any over-subscription validation or provider mutation, so an over-subscribed target sent to such a seller yields `UNSUPPORTED_FEATURE`, and MUST NOT issue seller-optimized proposals carrying min_spend_target_percentage.' ), + NoExplicitNull, ] = None seller_optimized_package_pacing: Annotated[ StrictBool | None, Field( description='Honors package `pacing` as subordinate per-package pacing inside a seller-optimized buy, in addition to the media-buy-level pacing covered by seller_optimized_budget: packages[].pacing and new_packages[].pacing on create_media_buy and update_media_buy, purchases[].pacing on buy_products, package pacing controls on control_media_buy, and allocation pacing on seller-optimized proposals. Package pacing MUST NOT cause delivery to exceed aggregate media-buy pacing. Meaningful only with seller_optimized_budget: true; seller declarations may be true only with seller_optimized_budget: true, while buyer required_features filters may request this feature alone. Package pacing equal to the effective media-buy pacing adds no subordinate constraint and does not require this feature; buyers SHOULD omit package pacing on seller-optimized buys unless this feature is advertised. A seller that declares seller_optimized_budget without this feature MUST reject a request that would leave package pacing differing from media-buy pacing on a seller-optimized buy, including an allocation-mode switch that retains such fixed-mode package pacing (the buyer can align it in the same update), with `UNSUPPORTED_FEATURE` before any provider mutation, and MUST NOT issue seller-optimized proposals carrying allocation pacing. Does not govern package pacing in fixed allocation.' ), + NoExplicitNull, ] = None bidding_policy: Annotated[ bidding_policy_capability.BiddingPolicyCapability | None, Field( description='Structured support for canonical bidding by authored scope, allocation context, mode, strength, and strength-qualified multi-field combination. Presence does not imply support for both scopes, both allocation modes, or every policy shape. Sellers MUST preserve every advertised semantic exactly and reject unadvertised policies rather than translating them.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/media_buy_frequency_cap_requirement.py b/src/adcp/types/domains/core/media_buy_frequency_cap_requirement.py index 620276de3..9246af93f 100644 --- a/src/adcp/types/domains/core/media_buy_frequency_cap_requirement.py +++ b/src/adcp/types/domains/core/media_buy_frequency_cap_requirement.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy_frequency_cap_requirement.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..enums import media_buy_frequency_cap_control_mode @@ -16,4 +17,5 @@ class MediaBuyFrequencyCapRequirement(FrequencyCapRequirements): supported_control_modes: Annotated[ list[media_buy_frequency_cap_control_mode.MediaBuyFrequencyCapControlMode] | None, Field(min_length=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/media_buy_frequency_cap_support.py b/src/adcp/types/domains/core/media_buy_frequency_cap_support.py index c8c103793..b2d406482 100644 --- a/src/adcp/types/domains/core/media_buy_frequency_cap_support.py +++ b/src/adcp/types/domains/core/media_buy_frequency_cap_support.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy_frequency_cap_support.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import Field from ..enums import media_buy_frequency_cap_control_mode @@ -19,4 +20,5 @@ class MediaBuyFrequencyCapSupport(FrequencyCapConstraints): description='FrequencyCap shapes accepted by the product. max_impressions_and_suppress means both controls may appear together and are enforced with AND semantics.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/media_buy_support.py b/src/adcp/types/domains/core/media_buy_support.py index dcf3c1975..a02b6ef39 100644 --- a/src/adcp/types/domains/core/media_buy_support.py +++ b/src/adcp/types/domains/core/media_buy_support.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy_support.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T12:09:21+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -22,8 +22,9 @@ class ProductMediaBuySupport(AdCPBaseModel): Field( description="This product can participate in the seller's shared counter for a root MediaBuy.frequency_cap. A buy mixing this product with one that lacks this declaration, or whose resolved supported_per_units omits the root cap's per value, is rejected atomically." ), + NoExplicitNull, ] = None - frequency_cap_constraints: ( - media_buy_frequency_cap_support.MediaBuyFrequencyCapSupport | None - ) = None - ext: ext_1.ExtensionObject | None = None + frequency_cap_constraints: Annotated[ + media_buy_frequency_cap_support.MediaBuyFrequencyCapSupport | None, NoExplicitNull + ] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/media_buy_support_requirements.py b/src/adcp/types/domains/core/media_buy_support_requirements.py index 5647803db..978545067 100644 --- a/src/adcp/types/domains/core/media_buy_support_requirements.py +++ b/src/adcp/types/domains/core/media_buy_support_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/media_buy_support_requirements.json -# timestamp: 2026-09-14T14:16:02+00:00 +# timestamp: 2026-10-21T12:09:21+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import media_buy_frequency_cap_requirement @@ -21,7 +21,8 @@ class ProductMediaBuySupportRequirements(AdCPBaseModel): Field( description='Require product participation in one shared MediaBuy frequency-cap counter.' ), + NoExplicitNull, + ] = None + frequency_cap_constraints: Annotated[ + media_buy_frequency_cap_requirement.MediaBuyFrequencyCapRequirement | None, NoExplicitNull ] = None - frequency_cap_constraints: ( - media_buy_frequency_cap_requirement.MediaBuyFrequencyCapRequirement | None - ) = None diff --git a/src/adcp/types/domains/core/missing_metric.py b/src/adcp/types/domains/core/missing_metric.py index a9e5c46ac..173738209 100644 --- a/src/adcp/types/domains/core/missing_metric.py +++ b/src/adcp/types/domains/core/missing_metric.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/missing_metric.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import attribution_methodology as attribution_methodology_1 @@ -21,11 +21,15 @@ class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None class MissingMetric1(AdCPBaseModel): @@ -39,6 +43,7 @@ class MissingMetric1(AdCPBaseModel): Field( description='Mirrors the qualifier on `committed_metrics` so the missing entry preserves the contract distinction (e.g., flagging MRC viewability as missing when only GroupM was reported, vendor-attested completion as missing when only seller-attested was reported, or deterministic_purchase attribution as missing when only probabilistic was reported). MUST match the qualifier on the corresponding `committed_metrics` entry the missing flag refers to.' ), + NoExplicitNull, ] = None @@ -54,6 +59,7 @@ class MissingMetric2(AdCPBaseModel): Field( description='Mirrors the qualifier on the corresponding vendor-scope `committed_metrics` entry. MUST match that entry so reconciliation joins on (vendor, metric_id, qualifier).' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/notification_config.py b/src/adcp/types/domains/core/notification_config.py index 4d4905261..7553e7aed 100644 --- a/src/adcp/types/domains/core/notification_config.py +++ b/src/adcp/types/domains/core/notification_config.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/notification_config.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import auth_scheme @@ -51,6 +51,7 @@ class Authentication(AdCPBaseModel): description='Credentials for the legacy scheme. Bearer: token. HMAC-SHA256: shared secret. Minimum 32 characters. Exchanged out-of-band during onboarding. Write-only.', min_length=32, ), + NoExplicitNull, ] = None @@ -85,6 +86,7 @@ class NotificationConfig(AdCPBaseModel): Field( description='Product webhook representation selected by this subscriber. Use canonical with lifecycle_tools.list_products; legacy is the default for 3.x get_products consumers. Sellers emit exactly canonical_product/canonical_pricing_options or product/pricing_options accordingly. Valid only when event_types includes a product.* event.' ), + NoExplicitNull, ] = ProductPayloadView.legacy authentication: Annotated[ Authentication | None, @@ -92,11 +94,13 @@ class NotificationConfig(AdCPBaseModel): deprecated=True, description="Legacy authentication selector. Same precedence and semantics as `push-notification-config.authentication` — presence opts the seller into Bearer or HMAC-SHA256 signing; absence selects the default RFC 9421 webhook profile keyed off the seller's brand.json `agents[]` JWKS. The same signed-registration downgrade-resistance rules apply to accounts[].notification_configs[].authentication. Deprecated; removed in AdCP 4.0. Credentials are write-only and MUST NOT be echoed on `list_accounts` reads.", ), + NoExplicitNull, ] = None active: Annotated[ StrictBool | None, Field( description="When false, the seller persists the configuration but suppresses fires. Use to pause a noisy subscriber without losing the registration. Sellers MUST NOT skip persisting the entry when `active: false` — the buyer's next `sync_accounts` MUST observe the same array, otherwise the buyer cannot distinguish pause from drop. Paused configs may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time. Reactivation requires full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof." ), + NoExplicitNull, ] = True - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/offering.py b/src/adcp/types/domains/core/offering.py index ea6d21f8f..ac476ff9b 100644 --- a/src/adcp/types/domains/core/offering.py +++ b/src/adcp/types/domains/core/offering.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/offering.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -46,6 +46,7 @@ class GeoTargets(AdCPBaseModel): description="Countries where this offering is relevant. ISO 3166-1 alpha-2 codes (e.g., 'US', 'NL', 'DE').", min_length=1, ), + NoExplicitNull, ] = None regions: Annotated[ list[Region] | None, @@ -53,6 +54,7 @@ class GeoTargets(AdCPBaseModel): description="Regions or states where this offering is relevant. ISO 3166-2 subdivision codes (e.g., 'NL-NH', 'US-CA').", min_length=1, ), + NoExplicitNull, ] = None metros: Annotated[ list[Metro] | None, @@ -60,6 +62,7 @@ class GeoTargets(AdCPBaseModel): description='Metro areas where this offering is relevant. Each entry specifies the classification system and target values.', min_length=1, ), + NoExplicitNull, ] = None postal_areas: Annotated[ list[postal_area.PostalArea] | None, @@ -67,6 +70,7 @@ class GeoTargets(AdCPBaseModel): description='Postal areas where this offering is relevant. Prefer the native country + postal system form. Deprecated legacy country-fused postal-system tokens remain accepted for compatibility.', min_length=1, ), + NoExplicitNull, ] = None @@ -86,58 +90,66 @@ class Offering(AdCPBaseModel): description="Human-readable offering name (e.g., 'Winter Sale', 'Free Trial', 'Enterprise Platform')" ), ] - description: Annotated[str | None, Field(description="Description of what's being offered")] = ( - None - ) + description: Annotated[ + str | None, Field(description="Description of what's being offered"), NoExplicitNull + ] = None tagline: Annotated[ - str | None, Field(description='Short promotional tagline for the offering') + str | None, Field(description='Short promotional tagline for the offering'), NoExplicitNull ] = None valid_from: Annotated[ AwareDatetime | None, Field( description='When the offering becomes available. If not specified, offering is immediately available.' ), + NoExplicitNull, ] = None valid_to: Annotated[ AwareDatetime | None, Field( description='When the offering expires. If not specified, offering has no expiration.' ), + NoExplicitNull, ] = None checkout_url: Annotated[ AnyUrl | None, Field( description="URL for checkout/purchase flow when the brand doesn't support agentic checkout." ), + NoExplicitNull, ] = None landing_url: Annotated[ AnyUrl | None, Field( description="Landing page URL for this offering. For catalog-driven creatives, this is the per-item click-through destination that platforms map to the ad's link-out URL. Every offering in a catalog should have a landing_url unless the format provides its own destination logic." ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, Field( description='Structured asset groups for this offering. Each group carries a typed pool of creative assets (headlines, images, videos, etc.) identified by a group ID that matches format-level vocabulary.' ), + NoExplicitNull, ] = None geo_targets: Annotated[ GeoTargets | None, Field( description="Geographic scope of this offering. Declares where the offering is relevant — for location-specific offerings such as job vacancies, in-store promotions, or local events. Platforms use this to target geographically appropriate audiences and to filter out offerings irrelevant to a user's location. Uses the same geographic structures as targeting_overlay in create_media_buy." ), + NoExplicitNull, ] = None keywords: Annotated[ list[str] | None, Field( description='Keywords for matching this offering to user intent. Hosts use these for retrieval/relevance scoring.' ), + NoExplicitNull, ] = None categories: Annotated[ list[str] | None, Field( description="Categories this offering belongs to (e.g., 'measurement', 'identity', 'programmatic')" ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/offering_asset_group.py b/src/adcp/types/domains/core/offering_asset_group.py index cc091b559..356688f6c 100644 --- a/src/adcp/types/domains/core/offering_asset_group.py +++ b/src/adcp/types/domains/core/offering_asset_group.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/offering_asset_group.json -# timestamp: 2026-10-03T00:36:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import asset_content_type @@ -67,4 +67,4 @@ class OfferingAssetGroup(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/operator_identity.py b/src/adcp/types/domains/core/operator_identity.py index 149b12a0e..b1548d704 100644 --- a/src/adcp/types/domains/core/operator_identity.py +++ b/src/adcp/types/domains/core/operator_identity.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/operator_identity.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import operator_unit as operator_unit_1 @@ -28,4 +28,5 @@ class OperatorIdentity(AdCPBaseModel): Field( description='Optional operator-owned business unit, agency seat, or platform account. Its id participates in account identity; name is mutable display metadata. Omission from a complete desired replacement means the account should have no operator unit.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/operator_unit.py b/src/adcp/types/domains/core/operator_unit.py index d3fe73828..66408003f 100644 --- a/src/adcp/types/domains/core/operator_unit.py +++ b/src/adcp/types/domains/core/operator_unit.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/operator_unit.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -30,4 +30,5 @@ class OperatorUnit(AdCPBaseModel): max_length=200, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/opportunity_context.py b/src/adcp/types/domains/core/opportunity_context.py index 495781001..cf88a322d 100644 --- a/src/adcp/types/domains/core/opportunity_context.py +++ b/src/adcp/types/domains/core/opportunity_context.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/opportunity_context.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import date_range @@ -54,30 +54,37 @@ class OpportunityContext(AdCPBaseModel): pattern='^[A-Za-z0-9_.:-]{1,255}$', ), ] - phase: Annotated[Phase | None, Field(description='Current stage of buyer planning.')] = None + phase: Annotated[ + Phase | None, Field(description='Current stage of buyer planning.'), NoExplicitNull + ] = None intent: Annotated[ Intent | None, Field(description='How seriously the buyer is evaluating supply in this cycle.'), + NoExplicitNull, ] = None planning_horizon: Annotated[ date_range.DateRange | None, Field( description='Inclusive calendar range in which the buyer expects the campaign to run.' ), + NoExplicitNull, ] = None response_deadline: Annotated[ AwareDatetime | None, Field(description='Deadline by which the buyer needs a seller response.'), + NoExplicitNull, ] = None status: Annotated[ Status | None, Field( description='Whether the planning cycle remains open. On calls after initial creation, omission means no status update and MUST NOT reopen or close the opportunity implicitly, except that successful create_media_buy proposal execution explicitly infers accepted closure.' ), + NoExplicitNull, ] = None close_reason: Annotated[ CloseReason | None, Field(description='Why the opportunity closed. Required when status is closed.'), + NoExplicitNull, ] = None close_detail: Annotated[ str | None, @@ -86,4 +93,5 @@ class OpportunityContext(AdCPBaseModel): max_length=500, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/optimization_goal.py b/src/adcp/types/domains/core/optimization_goal.py index b824e1591..b2515d606 100644 --- a/src/adcp/types/domains/core/optimization_goal.py +++ b/src/adcp/types/domains/core/optimization_goal.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/optimization_goal.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import event_type as event_type_1 @@ -42,6 +42,7 @@ class TargetFrequency(AdCPBaseModel): description='Minimum frequency for an entity to be considered meaningfully reached within the window. Impressions that would bring an entity below this threshold are treated as high-value (growing reach). When omitted, the seller uses their platform default (typically 1).', ge=1, ), + NoExplicitNull, ] = None max: Annotated[ SchemaInt | None, @@ -49,6 +50,7 @@ class TargetFrequency(AdCPBaseModel): description='Frequency at which an entity is considered saturated within the window. Impressions toward entities at or above this threshold are treated as lower-value. When both min and max are present, max must be greater than or equal to min. When omitted, the seller determines the saturation point.', ge=1, ), + NoExplicitNull, ] = None window: Annotated[ duration.Duration, @@ -104,18 +106,21 @@ class EventSource(AdCPBaseModel): Field( description="Required when event_type is 'custom'. Platform-specific name for the custom event." ), + NoExplicitNull, ] = None value_field: Annotated[ str | None, Field( description="Which field in the event's custom_data carries the monetary value. The seller must use this field for value extraction and aggregation when computing ROAS and conversion value metrics. Required on at least one entry when target.kind is 'per_ad_spend' or 'maximize_value' — sellers must reject these target kinds when no event source entry includes value_field. When canonical bidding.roas binds to this goal, every referenced value-bearing source MUST declare value_currencies containing the media-buy currency; the seller validates this at buy creation. Once that source contract is declared, log_event rejects monetary records whose currency is missing or absent from the list. For each buy, only records exactly matching its media-buy currency contribute to ROAS; records in other declared currencies remain available to matching buys, and sellers MUST NOT convert between them. When present without a value-oriented target, the seller may use it for delivery reporting but must not change the objective. Common values: 'value', 'order_total', 'profit_margin'. This is not passed as a parameter to underlying platform APIs — the seller maps it to their platform's value ingestion mechanism." ), + NoExplicitNull, ] = None value_factor: Annotated[ StrictFloat | None, Field( description="Unit-scaling multiplier the seller must apply to value_field before aggregation. Use -1 for refund events (negate the value), 0.01 for values in cents, -0.01 for refunds in cents. It MUST NOT be used for currency conversion. A value of 0 zeroes out this source's value contribution (the source still counts for event dedup). Defaults to 1. This is not passed as a parameter to underlying platform APIs — the seller applies it when computing aggregated value metrics." ), + NoExplicitNull, ] = 1 @@ -193,12 +198,14 @@ class OptimizationGoal9(AdCPBaseModel): description='Target cost or return for this event goal. When omitted, the seller optimizes for maximum conversion count within budget — regardless of whether value_field is present on event sources. The presence of value_field alone does not change the optimization objective; it only makes value available for reporting. An explicit target of maximize_value or per_ad_spend is required to steer toward value.', discriminator='kind', ), + NoExplicitNull, ] = None attribution_window: Annotated[ attribution_window_1.AttributionWindow | None, Field( description="Attribution window for this optimization goal — references the canonical `attribution-window` shape (post_click, post_view, model). Values must match an option declared in the seller's `conversion_tracking.attribution_windows` capability. Sellers MUST reject windows not in their declared capabilities. When the entire field is omitted, the seller uses their default window." ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -206,6 +213,7 @@ class OptimizationGoal9(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None @@ -225,24 +233,28 @@ class OptimizationGoal8(AdCPBaseModel): Field( description="Viewability standard the goal is judged against. Required when metric is 'viewable_rate'; optional for 'viewed_seconds' (seller default standard when omitted); not allowed for other metrics. Must be in metric_optimization.supported_viewability_standards when declared. A goal below a same-standard viewability performance_standard never relaxes that standard." ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference | None, Field( description="Measurement vendor judging a viewable_rate or viewed_seconds goal; not allowed for other metrics. When omitted, the seller's default viewability measurement applies. Sellers MUST reject a vendor they cannot optimize against rather than substitute another." ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description="Unit for reach measurement. Required when metric is 'reach'. Must be a value declared in the product's metric_optimization.supported_reach_units." ), + NoExplicitNull, ] = None target_frequency: Annotated[ TargetFrequency | None, Field( description="Target frequency band for reach optimization. Only applicable when metric is 'reach'. Frames frequency as an optimization signal: the seller should treat impressions toward entities already within the [min, max] band as lower-value, and impressions toward unreached entities as higher-value. This shifts budget toward fresh reach rather than re-reaching known users. When omitted, the seller maximizes unique reach without a frequency constraint. A hard cap can still be layered via targeting_overlay.frequency_cap if a ceiling is needed." ), + NoExplicitNull, ] = None view_duration_seconds: Annotated[ StrictFloat | None, @@ -250,6 +262,7 @@ class OptimizationGoal8(AdCPBaseModel): description="Minimum video view duration in seconds that qualifies as a completed_view for this goal. Only applicable when metric is 'completed_views'. When omitted, the seller uses their platform default (typically 2–15 seconds). Common values: 2 (Snap/LinkedIn default), 6 (TikTok), 15 (Snap 15-second views, Meta ThruPlay). Sellers declare which durations they support in metric_optimization.supported_view_durations. Sellers must reject goals with unsupported values — silent rounding would create measurement discrepancies.", gt=0.0, ), + NoExplicitNull, ] = None target: Annotated[ Target | Target14 | None, @@ -257,6 +270,7 @@ class OptimizationGoal8(AdCPBaseModel): description='Target for this metric. When omitted, the seller optimizes for maximum metric volume within budget.', discriminator='kind', ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -264,6 +278,7 @@ class OptimizationGoal8(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None @@ -290,6 +305,7 @@ class OptimizationGoal10(AdCPBaseModel): description="Target for this vendor metric. When omitted, the seller optimizes for maximum metric volume / score within budget. `cost_per` and `threshold_rate` semantics mirror the same target kinds on the `metric` kind — units are vendor-defined and depend on the vendor's `measurement.metrics[]` declaration for this `metric_id`.", discriminator='kind', ), + NoExplicitNull, ] = None priority: Annotated[ SchemaInt | None, @@ -297,6 +313,7 @@ class OptimizationGoal10(AdCPBaseModel): description='Relative priority among sibling goals. Lower numbers rank first. Goals without priority follow explicitly prioritized goals. Ties use array order, so the earliest goal at the lowest explicit priority is primary; when all priorities are omitted, the first goal is primary.', ge=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/outcome_measurement.py b/src/adcp/types/domains/core/outcome_measurement.py index 95e9e18cf..26e52c999 100644 --- a/src/adcp/types/domains/core/outcome_measurement.py +++ b/src/adcp/types/domains/core/outcome_measurement.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/outcome_measurement.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import duration @@ -35,6 +35,7 @@ class OutcomeMeasurement(AdCPBaseModel): Field( description='Attribution window as a structured duration (e.g., {"interval": 30, "unit": "days"}).' ), + NoExplicitNull, ] = None reporting: Annotated[ str, diff --git a/src/adcp/types/domains/core/overlay.py b/src/adcp/types/domains/core/overlay.py index 7e825a113..2b326b754 100644 --- a/src/adcp/types/domains/core/overlay.py +++ b/src/adcp/types/domains/core/overlay.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/overlay.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat @@ -20,16 +20,19 @@ class Visual(AdCPBaseModel): Field( description='URL to a theme-neutral overlay graphic (SVG or PNG). Use when a single file works for all backgrounds, e.g. an SVG using CSS custom properties or currentColor.' ), + NoExplicitNull, ] = None light: Annotated[ AnyUrl | None, Field( description='URL to the overlay graphic for use on light/bright backgrounds (SVG or PNG)' ), + NoExplicitNull, ] = None dark: Annotated[ AnyUrl | None, Field(description='URL to the overlay graphic for use on dark backgrounds (SVG or PNG)'), + NoExplicitNull, ] = None @@ -73,12 +76,14 @@ class Overlay(AdCPBaseModel): Field( description='Human-readable explanation of what this overlay is and how buyers should account for it' ), + NoExplicitNull, ] = None visual: Annotated[ Visual | None, Field( description='Optional visual reference for this overlay element. Useful for creative agents compositing previews and for buyers understanding what will appear over their content. Must include at least one of: url, light, or dark.' ), + NoExplicitNull, ] = None bounds: Annotated[ Bounds, diff --git a/src/adcp/types/domains/core/package.py b/src/adcp/types/domains/core/package.py index 3c9438943..03dfa499c 100644 --- a/src/adcp/types/domains/core/package.py +++ b/src/adcp/types/domains/core/package.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/package.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..enums import canceled_by as canceled_by_1 @@ -39,13 +39,16 @@ class Cancellation(AdCPBaseModel): Field(description='Which party initiated the package cancellation.'), ] reason: Annotated[ - str | None, Field(description='Reason the package was canceled.', max_length=500) + str | None, + Field(description='Reason the package was canceled.', max_length=500), + NoExplicitNull, ] = None acknowledged_at: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp when the seller acknowledged the cancellation. Confirms inventory has been released and billing stopped. Absent until the seller processes the cancellation.' ), + NoExplicitNull, ] = None @@ -59,6 +62,7 @@ class Package(AdCPBaseModel): Field( description="ID of the product this package is based on. For packages created from an explicit create_media_buy package request, sellers MUST echo the request package's product_id on every response package object that represents that requested package." ), + NoExplicitNull, ] = None audience_evidence_selections: Annotated[ list[audience_evidence_selection.AudienceEvidenceSelection] | None, @@ -66,6 +70,7 @@ class Package(AdCPBaseModel): description='Exact immutable audience-evidence snapshots that affected recommendation, eligibility, or package construction. A confirmed package MUST include a package_construction selection matching every buyer audience_evidence_pin and every snapshot used to satisfy package audience_evidence_requirements; this readback remains mandatory on subsequent package read surfaces. This is decision provenance only; applied targeting remains exclusively in targeting_overlay and targeting_resolution.demographics.', min_length=1, ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -73,6 +78,7 @@ class Package(AdCPBaseModel): description='Hard lifetime spend cap for this package in the media-buy currency. Every selected pricing option in an AdCP-authored media buy MUST declare that same currency. In seller-optimized allocation mode this is a ceiling, not a current allocation. May be omitted when the package is bounded only by the shared media-buy total.', ge=0.0, ), + NoExplicitNull, ] = None min_spend_target: Annotated[ StrictFloat | None, @@ -80,6 +86,7 @@ class Package(AdCPBaseModel): description='Soft lifetime spend target accepted for this package under seller-optimized budget allocation. This is an allocation preference, not a billing or delivery guarantee.', ge=0.0, ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -87,13 +94,15 @@ class Package(AdCPBaseModel): description="The hard package spend ceiling per shared media-buy cap day, in the media buy's currency. Sellers MUST echo this whenever a package daily cap is set. It is a subordinate ceiling, not a reserved or current allocation; the media buy's budget_cap_timezone defines its day boundary.", ge=0.0, ), + NoExplicitNull, ] = None - pacing: pacing_1.Pacing | None = None + pacing: Annotated[pacing_1.Pacing | None, NoExplicitNull] = None pricing_option_id: Annotated[ str | None, Field( description="ID of the selected pricing option from the product's pricing_options array" ), + NoExplicitNull, ] = None bid_price: Annotated[ StrictFloat | None, @@ -102,27 +111,33 @@ class Package(AdCPBaseModel): description='DEPRECATED legacy bidding representation. 3.2 sellers normalize accepted legacy input and SHOULD echo bidding instead. Removed in the next major.', ge=0.0, ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Package-authored bidding policy, echoed only when the buyer authored a package override. `{automatic:true}` is an explicit automatic-bidding override. Omission means the package inherits media-buy bidding or, when both scopes are absent, uses provider automatic delivery. Monetary fields are denominated in the media-buy currency. Sellers MUST NOT materialize inherited media-buy policy here.' ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description="Breakdown of the effective price for this package. On fixed-price packages, echoes the pricing option's breakdown. On auction packages, shows the clearing price breakdown including any commission or settlement terms." ), + NoExplicitNull, ] = None impressions: Annotated[ - StrictFloat | None, Field(description='Impression goal for this package', ge=0.0) + StrictFloat | None, + Field(description='Impression goal for this package', ge=0.0), + NoExplicitNull, ] = None catalogs: Annotated[ list[catalog.Catalog] | None, Field( description='Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level — sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Echoed from the create_media_buy request.' ), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -130,6 +145,7 @@ class Package(AdCPBaseModel): deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format IDs supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including dual-emission cases where `format_option_refs` was the winning selector, so read surfaces preserve the original wire contract. Omitted means the request did not carry legacy format_ids unless the seller cannot reconstruct legacy requests created before this field was persisted.', ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -137,36 +153,42 @@ class Package(AdCPBaseModel): description='Structured 3.1+ format option references supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it. Publisher-catalog-backed options are identified by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options are identified by `{ scope: "product", format_option_id }` and resolve only against this package\'s target product. Omitted means the request did not carry format_option_refs unless the seller cannot reconstruct legacy requests created before this field was persisted.', min_length=1, ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description='Direct canonical selector supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including informational-echo cases where `format_ids` was the winning selector, so read surfaces preserve the original wire contract.' ), + NoExplicitNull, ] = None params: Annotated[ dict[str, Any] | None, Field( description='Parameters for the direct canonical selector in `format_kind`, echoed from the create_media_buy request whenever the request included it. Requires `format_kind`; omitted only when the request did not carry direct canonical params or when the seller cannot reconstruct legacy requests created before this field was persisted.' ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting.TargetingOverlay | None, Field( description='Complete effective targeting accepted for this package, including targeting bound through configured product selection plus package-specific targeting. Sellers MUST echo an applied package frequency_cap independently from any MediaBuy root cap. Sellers MUST also echo placement, property, and collection selection so buyers can audit purchased inventory: placements via placement_selection, collections via collection_selection (the committed concrete selectors, materialized even when the selection was produced through collection_list references).' ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ package_targeting_resolution.PackageTargetingResolution | None, Field( description="Execution details for the package's accepted targeting. Sellers MUST include targeting_resolution.demographics whenever demographic targeting was requested or applied." ), + NoExplicitNull, ] = None measurement_terms: Annotated[ measurement_terms_1.MeasurementTerms | None, Field( description="Agreed billing measurement and makegood terms for this package. Reflects what was negotiated — may differ from the buyer's proposal or the product's defaults. When present, these terms are binding for the package's duration." ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[performance_standard.PerformanceStandard] | None, @@ -174,6 +196,7 @@ class Package(AdCPBaseModel): description='Agreed performance standards for this package. When any entry specifies a vendor, creatives assigned to this package MUST include corresponding tracker_script or tracker_pixel assets from that vendor.', min_length=1, ), + NoExplicitNull, ] = None committed_metrics: Annotated[ list[committed_metric.CommittedMetric] | None, @@ -212,12 +235,14 @@ class Package(AdCPBaseModel): ], min_length=1, ), + NoExplicitNull, ] = None creative_assignments: Annotated[ list[creative_assignment.CreativeAssignment] | None, Field( description='Creative assets assigned to this package, including the committed package-scoped rotation policy. Omitted rotation_mode reads as weighted for backward compatibility; all assignments resolve to one effective mode, and sequential positions are unique within each package-local group.' ), + NoExplicitNull, ] = None formats_to_provide: Annotated[ list[package_format_snapshot.PackageFormatSnapshot] | None, @@ -225,12 +250,14 @@ class Package(AdCPBaseModel): description='Immutable canonical creative contracts established for this package. Each entry is a PackageFormatSnapshot of the selected effective Product format declaration. A package whose selected format carries tracker_execution_contract MUST retain and return this checklist even after creative coverage is complete; the live Product is never substituted for the package snapshot.', min_length=1, ), + NoExplicitNull, ] = None formats_pending: Annotated[ list[package_format_snapshot.PackageFormatSnapshot] | None, Field( description='PackageFormatSnapshot entries from formats_to_provide that do not yet have creative coverage through sync_creatives or inline assignment. Every entry MUST equal its formats_to_provide snapshot after RFC 8785 canonicalization and, when product_snapshot_digest is present, carry the identical digest. An empty emitted array means every required format is covered. Absence means readiness was not reported.' ), + NoExplicitNull, ] = None format_ids_to_provide: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -238,6 +265,7 @@ class Package(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_to_provide retained for older 3.x peers. New sellers emit canonical formats_to_provide declarations.', ), + NoExplicitNull, ] = None format_ids_pending: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -245,6 +273,7 @@ class Package(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_pending retained for older 3.x peers. New sellers emit canonical formats_pending declarations. An empty emitted array means every projected requirement is covered. Absence means legacy readiness was not reported and MUST NOT be interpreted as full coverage.', ), + NoExplicitNull, ] = None optimization_goals: Annotated[ list[optimization_goal.OptimizationGoal] | None, @@ -252,34 +281,40 @@ class Package(AdCPBaseModel): description='Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.', min_length=1, ), + NoExplicitNull, ] = None start_time: Annotated[ AwareDatetime | None, Field( description="Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Sellers SHOULD always include the resolved value in responses, even when inherited." ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description="Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Sellers SHOULD always include the resolved value in responses, even when inherited." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field( description='Whether this package is paused by the buyer. Paused packages do not deliver impressions. Defaults to false.' ), + NoExplicitNull, ] = False canceled: Annotated[ StrictBool | None, Field( description='Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated. Defaults to false.' ), + NoExplicitNull, ] = False cancellation: Annotated[ Cancellation | None, Field(description='Cancellation metadata. Present only when canceled is true.'), + NoExplicitNull, ] = None agency_estimate_number: Annotated[ str | None, @@ -287,17 +322,20 @@ class Package(AdCPBaseModel): description="Agency estimate or authorization number for this package. Echoed from the buyer's request. When present on the package, takes precedence over the media buy-level estimate number.", max_length=100, ), + NoExplicitNull, ] = None creative_deadline: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies." ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque package-level correlation data echoed unchanged in responses, webhooks, and read surfaces. Buyers targeting mixed seller populations SHOULD include a per-package correlation value here, commonly context.buyer_ref, so responses from legacy sellers that do not echo product_id can still be mapped back to the requested product or line item. Sellers MUST preserve this object unchanged and MUST NOT parse it for business logic.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/package_delivery_metric_value.py b/src/adcp/types/domains/core/package_delivery_metric_value.py index e54dda9c1..ac0c801b4 100644 --- a/src/adcp/types/domains/core/package_delivery_metric_value.py +++ b/src/adcp/types/domains/core/package_delivery_metric_value.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/package_delivery_metric_value.json -# timestamp: 2026-10-04T00:56:30+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import attribution_methodology as attribution_methodology_1 @@ -26,30 +26,35 @@ class Qualifier(AdCPBaseModel): Field( description='Viewability standard under which this row was measured. MRC and GroupM define materially different thresholds; never sum across standards.' ), + NoExplicitNull, ] = None completion_source: Annotated[ completion_source_1.CompletionSource | None, Field( description='Source of `completion_rate` attestation for this row — `seller_attested` from player/ad server, `vendor_attested` from a third-party measurement vendor. The two paths can yield different rates; never sum across sources.' ), + NoExplicitNull, ] = None attribution_methodology: Annotated[ attribution_methodology_1.AttributionMethodology | None, Field( description='Attribution methodology under which this outcome row was computed (`deterministic_purchase`, `probabilistic`, `panel_based`, `modeled`). Outcome metrics measured under different methodologies represent materially different numbers; never sum across methodologies.' ), + NoExplicitNull, ] = None attribution_window: Annotated[ duration.Duration | None, Field( description="Attribution window for this outcome row. Object-valued duration (`{interval, unit}`), not a shorthand string. Outcome metrics measured over different windows represent the same metric over different time periods; the partition keeps them as separate rows so buyers don't accidentally aggregate." ), + NoExplicitNull, ] = None lift_dimension: Annotated[ lift_dimension_1.LiftDimension | None, Field( description='Brand-lift dimension this row represents (awareness, consideration, favorability, purchase intent, or ad recall). Required when `metric_id` is `brand_lift`. Each dimension is a separate surveyed outcome with its own sample size and confidence interval; never sum across dimensions.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/package_format_snapshot.py b/src/adcp/types/domains/core/package_format_snapshot.py index f7caf677d..3bda32ec6 100644 --- a/src/adcp/types/domains/core/package_format_snapshot.py +++ b/src/adcp/types/domains/core/package_format_snapshot.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/package_format_snapshot.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..enums import channels, daast_version, vast_version @@ -54,6 +54,7 @@ class PackageFormatSnapshot17(AdCPBaseModel): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -61,18 +62,21 @@ class PackageFormatSnapshot17(AdCPBaseModel): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -80,6 +84,7 @@ class PackageFormatSnapshot17(AdCPBaseModel): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -87,6 +92,7 @@ class PackageFormatSnapshot17(AdCPBaseModel): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Any params: Any @@ -98,6 +104,7 @@ class PackageFormatSnapshot1(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -105,12 +112,14 @@ class PackageFormatSnapshot1(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -118,60 +127,70 @@ class PackageFormatSnapshot1(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -179,12 +198,14 @@ class PackageFormatSnapshot1(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: image.CanonicalFormatImage @@ -196,6 +217,7 @@ class PackageFormatSnapshot2(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -203,12 +225,14 @@ class PackageFormatSnapshot2(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -216,60 +240,70 @@ class PackageFormatSnapshot2(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -277,12 +311,14 @@ class PackageFormatSnapshot2(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: html5.CanonicalFormatHtml5Banner @@ -294,6 +330,7 @@ class PackageFormatSnapshot3(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -301,12 +338,14 @@ class PackageFormatSnapshot3(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -314,60 +353,70 @@ class PackageFormatSnapshot3(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -375,12 +424,14 @@ class PackageFormatSnapshot3(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: display_tag.CanonicalFormatDisplayTag @@ -392,6 +443,7 @@ class PackageFormatSnapshot4(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -399,12 +451,14 @@ class PackageFormatSnapshot4(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -412,60 +466,70 @@ class PackageFormatSnapshot4(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -473,12 +537,14 @@ class PackageFormatSnapshot4(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: image_carousel.CanonicalFormatImageCarousel @@ -490,6 +556,7 @@ class PackageFormatSnapshot5(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -497,12 +564,14 @@ class PackageFormatSnapshot5(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -510,60 +579,70 @@ class PackageFormatSnapshot5(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -571,12 +650,14 @@ class PackageFormatSnapshot5(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: video_hosted.CanonicalFormatHostedVideo @@ -588,6 +669,7 @@ class PackageFormatSnapshot6(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -595,12 +677,14 @@ class PackageFormatSnapshot6(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -608,60 +692,70 @@ class PackageFormatSnapshot6(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -669,12 +763,14 @@ class PackageFormatSnapshot6(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: video_vast.CanonicalFormatVastVideo @@ -686,6 +782,7 @@ class PackageFormatSnapshot7(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -693,12 +790,14 @@ class PackageFormatSnapshot7(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -706,60 +805,70 @@ class PackageFormatSnapshot7(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -767,12 +876,14 @@ class PackageFormatSnapshot7(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: audio_hosted.CanonicalFormatHostedAudio @@ -784,6 +895,7 @@ class PackageFormatSnapshot8(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -791,12 +903,14 @@ class PackageFormatSnapshot8(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -804,60 +918,70 @@ class PackageFormatSnapshot8(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -865,12 +989,14 @@ class PackageFormatSnapshot8(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_vast'] = 'audio_vast' params: audio_vast.CanonicalFormatVastAudio @@ -882,6 +1008,7 @@ class PackageFormatSnapshot9(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -889,12 +1016,14 @@ class PackageFormatSnapshot9(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -902,60 +1031,70 @@ class PackageFormatSnapshot9(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -963,12 +1102,14 @@ class PackageFormatSnapshot9(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: audio_daast.CanonicalFormatDaastAudio @@ -980,6 +1121,7 @@ class PackageFormatSnapshot10(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -987,12 +1129,14 @@ class PackageFormatSnapshot10(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1000,60 +1144,70 @@ class PackageFormatSnapshot10(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1061,12 +1215,14 @@ class PackageFormatSnapshot10(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven @@ -1078,6 +1234,7 @@ class PackageFormatSnapshot11(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1085,12 +1242,14 @@ class PackageFormatSnapshot11(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1098,60 +1257,70 @@ class PackageFormatSnapshot11(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1159,12 +1328,14 @@ class PackageFormatSnapshot11(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: native_in_feed.CanonicalFormatNativeInFeed @@ -1176,6 +1347,7 @@ class PackageFormatSnapshot12(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1183,12 +1355,14 @@ class PackageFormatSnapshot12(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1196,60 +1370,70 @@ class PackageFormatSnapshot12(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1257,12 +1441,14 @@ class PackageFormatSnapshot12(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: responsive_creative.CanonicalFormatResponsiveCreative @@ -1274,6 +1460,7 @@ class PackageFormatSnapshot13(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1281,12 +1468,14 @@ class PackageFormatSnapshot13(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1294,60 +1483,70 @@ class PackageFormatSnapshot13(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1355,12 +1554,14 @@ class PackageFormatSnapshot13(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement @@ -1372,6 +1573,7 @@ class PackageFormatSnapshot14(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1379,12 +1581,14 @@ class PackageFormatSnapshot14(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1392,60 +1596,70 @@ class PackageFormatSnapshot14(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1453,12 +1667,14 @@ class PackageFormatSnapshot14(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay @@ -1470,6 +1686,7 @@ class PackageFormatSnapshot15(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1477,12 +1694,14 @@ class PackageFormatSnapshot15(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1490,60 +1709,70 @@ class PackageFormatSnapshot15(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1551,12 +1780,14 @@ class PackageFormatSnapshot15(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['coordinated_placements'] = 'coordinated_placements' params: coordinated_placements.CanonicalFormatCoordinatedPlacements @@ -1568,6 +1799,7 @@ class PackageFormatSnapshot16(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1575,12 +1807,14 @@ class PackageFormatSnapshot16(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1588,60 +1822,70 @@ class PackageFormatSnapshot16(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1649,12 +1893,14 @@ class PackageFormatSnapshot16(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['custom'] = 'custom' params: Annotated[ @@ -1675,6 +1921,7 @@ class PackageFormatSnapshot18(PackageFormatSnapshot1): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1682,18 +1929,21 @@ class PackageFormatSnapshot18(PackageFormatSnapshot1): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1701,6 +1951,7 @@ class PackageFormatSnapshot18(PackageFormatSnapshot1): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1708,6 +1959,7 @@ class PackageFormatSnapshot18(PackageFormatSnapshot1): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: image.CanonicalFormatImage @@ -1723,6 +1975,7 @@ class PackageFormatSnapshot19(PackageFormatSnapshot2): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1730,18 +1983,21 @@ class PackageFormatSnapshot19(PackageFormatSnapshot2): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1749,6 +2005,7 @@ class PackageFormatSnapshot19(PackageFormatSnapshot2): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1756,6 +2013,7 @@ class PackageFormatSnapshot19(PackageFormatSnapshot2): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: html5.CanonicalFormatHtml5Banner @@ -1771,6 +2029,7 @@ class PackageFormatSnapshot20(PackageFormatSnapshot3): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1778,18 +2037,21 @@ class PackageFormatSnapshot20(PackageFormatSnapshot3): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1797,6 +2059,7 @@ class PackageFormatSnapshot20(PackageFormatSnapshot3): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1804,6 +2067,7 @@ class PackageFormatSnapshot20(PackageFormatSnapshot3): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: display_tag.CanonicalFormatDisplayTag @@ -1819,6 +2083,7 @@ class PackageFormatSnapshot21(PackageFormatSnapshot4): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1826,18 +2091,21 @@ class PackageFormatSnapshot21(PackageFormatSnapshot4): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1845,6 +2113,7 @@ class PackageFormatSnapshot21(PackageFormatSnapshot4): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1852,6 +2121,7 @@ class PackageFormatSnapshot21(PackageFormatSnapshot4): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: image_carousel.CanonicalFormatImageCarousel @@ -1867,6 +2137,7 @@ class PackageFormatSnapshot22(PackageFormatSnapshot5): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1874,18 +2145,21 @@ class PackageFormatSnapshot22(PackageFormatSnapshot5): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1893,6 +2167,7 @@ class PackageFormatSnapshot22(PackageFormatSnapshot5): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1900,6 +2175,7 @@ class PackageFormatSnapshot22(PackageFormatSnapshot5): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: video_hosted.CanonicalFormatHostedVideo @@ -1915,6 +2191,7 @@ class PackageFormatSnapshot23(PackageFormatSnapshot6): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1922,18 +2199,21 @@ class PackageFormatSnapshot23(PackageFormatSnapshot6): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1941,6 +2221,7 @@ class PackageFormatSnapshot23(PackageFormatSnapshot6): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1948,6 +2229,7 @@ class PackageFormatSnapshot23(PackageFormatSnapshot6): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: video_vast.CanonicalFormatVastVideo @@ -1963,6 +2245,7 @@ class PackageFormatSnapshot24(PackageFormatSnapshot7): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -1970,18 +2253,21 @@ class PackageFormatSnapshot24(PackageFormatSnapshot7): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -1989,6 +2275,7 @@ class PackageFormatSnapshot24(PackageFormatSnapshot7): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -1996,6 +2283,7 @@ class PackageFormatSnapshot24(PackageFormatSnapshot7): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: audio_hosted.CanonicalFormatHostedAudio @@ -2011,6 +2299,7 @@ class PackageFormatSnapshot25(PackageFormatSnapshot8): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2018,18 +2307,21 @@ class PackageFormatSnapshot25(PackageFormatSnapshot8): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2037,6 +2329,7 @@ class PackageFormatSnapshot25(PackageFormatSnapshot8): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2044,6 +2337,7 @@ class PackageFormatSnapshot25(PackageFormatSnapshot8): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['audio_vast'] = 'audio_vast' params: audio_vast.CanonicalFormatVastAudio @@ -2059,6 +2353,7 @@ class PackageFormatSnapshot26(PackageFormatSnapshot9): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2066,18 +2361,21 @@ class PackageFormatSnapshot26(PackageFormatSnapshot9): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2085,6 +2383,7 @@ class PackageFormatSnapshot26(PackageFormatSnapshot9): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2092,6 +2391,7 @@ class PackageFormatSnapshot26(PackageFormatSnapshot9): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: audio_daast.CanonicalFormatDaastAudio @@ -2107,6 +2407,7 @@ class PackageFormatSnapshot27(PackageFormatSnapshot10): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2114,18 +2415,21 @@ class PackageFormatSnapshot27(PackageFormatSnapshot10): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2133,6 +2437,7 @@ class PackageFormatSnapshot27(PackageFormatSnapshot10): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2140,6 +2445,7 @@ class PackageFormatSnapshot27(PackageFormatSnapshot10): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven @@ -2155,6 +2461,7 @@ class PackageFormatSnapshot28(PackageFormatSnapshot11): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2162,18 +2469,21 @@ class PackageFormatSnapshot28(PackageFormatSnapshot11): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2181,6 +2491,7 @@ class PackageFormatSnapshot28(PackageFormatSnapshot11): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2188,6 +2499,7 @@ class PackageFormatSnapshot28(PackageFormatSnapshot11): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: native_in_feed.CanonicalFormatNativeInFeed @@ -2203,6 +2515,7 @@ class PackageFormatSnapshot29(PackageFormatSnapshot12): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2210,18 +2523,21 @@ class PackageFormatSnapshot29(PackageFormatSnapshot12): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2229,6 +2545,7 @@ class PackageFormatSnapshot29(PackageFormatSnapshot12): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2236,6 +2553,7 @@ class PackageFormatSnapshot29(PackageFormatSnapshot12): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: responsive_creative.CanonicalFormatResponsiveCreative @@ -2251,6 +2569,7 @@ class PackageFormatSnapshot30(PackageFormatSnapshot13): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2258,18 +2577,21 @@ class PackageFormatSnapshot30(PackageFormatSnapshot13): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2277,6 +2599,7 @@ class PackageFormatSnapshot30(PackageFormatSnapshot13): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2284,6 +2607,7 @@ class PackageFormatSnapshot30(PackageFormatSnapshot13): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement @@ -2299,6 +2623,7 @@ class PackageFormatSnapshot31(PackageFormatSnapshot14): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2306,18 +2631,21 @@ class PackageFormatSnapshot31(PackageFormatSnapshot14): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2325,6 +2653,7 @@ class PackageFormatSnapshot31(PackageFormatSnapshot14): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2332,6 +2661,7 @@ class PackageFormatSnapshot31(PackageFormatSnapshot14): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay @@ -2347,6 +2677,7 @@ class PackageFormatSnapshot32(PackageFormatSnapshot15): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2354,18 +2685,21 @@ class PackageFormatSnapshot32(PackageFormatSnapshot15): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2373,6 +2707,7 @@ class PackageFormatSnapshot32(PackageFormatSnapshot15): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2380,6 +2715,7 @@ class PackageFormatSnapshot32(PackageFormatSnapshot15): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['coordinated_placements'] = 'coordinated_placements' params: coordinated_placements.CanonicalFormatCoordinatedPlacements @@ -2395,6 +2731,7 @@ class PackageFormatSnapshot33(PackageFormatSnapshot16): description='Product whose effective selected format was snapshotted. Required whenever tracker_execution_contract is present.', min_length=1, ), + NoExplicitNull, ] = None placement_refs: Annotated[ list[placement_ref.PlacementReference] | None, @@ -2402,18 +2739,21 @@ class PackageFormatSnapshot33(PackageFormatSnapshot16): description='Nonempty, duplicate-free effective placement set, sorted by publisher_domain then placement_id using ascending UTF-8 bytes without Unicode normalization. Omission binds the product-wide common effective contract; it never means an inferred list of all current placements.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for first-class VAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for first-class DAAST tracker matching when no sibling delivery document supplies one. The value is immutable for the package.' ), + NoExplicitNull, ] = None tracker_execution_contract_digest: Annotated[ str | None, @@ -2421,6 +2761,7 @@ class PackageFormatSnapshot33(PackageFormatSnapshot16): description='SHA-256 of RFC 8785 canonical JSON for tracker_execution_contract alone. Required if and only if the snapshot contains that contract.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None product_snapshot_digest: Annotated[ str | None, @@ -2428,6 +2769,7 @@ class PackageFormatSnapshot33(PackageFormatSnapshot16): description="Immutable SHA-256 digest of the package format snapshot's closed product-binding preimage. Required for every contract-bearing snapshot and paired with product_id whenever present.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None format_kind: Literal['custom'] = 'custom' params: Annotated[dict[str, Any], Field(description="Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.")] diff --git a/src/adcp/types/domains/core/package_signal_targeting.py b/src/adcp/types/domains/core/package_signal_targeting.py index d96d92c3d..d58b4485e 100644 --- a/src/adcp/types/domains/core/package_signal_targeting.py +++ b/src/adcp/types/domains/core/package_signal_targeting.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/package_signal_targeting.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from . import activation_key as activation_key_1 @@ -22,18 +22,21 @@ class PackageSignalTargeting4(AdCPBaseModel): Field( description="Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost." ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str | None, Field( description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the product option exposes a separate runtime or activation handle, and pass it verbatim. Buyers SHOULD prefer an exposed segment handle over reconstructing condition identity from categorical values because the handle can carry provider namespace and methodology distinctions.' ), + NoExplicitNull, ] = None activation_key: Annotated[ activation_key_1.ActivationKey | None, Field( description='Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.' ), + NoExplicitNull, ] = None @@ -81,12 +84,14 @@ class PackageSignalTargeting3(AdCPBaseModel): Field( description="Minimum value, inclusive. Omit for no minimum. Should be within the signal definition's range when declared." ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description="Maximum value, inclusive. Omit for no maximum. Should be within the signal definition's range when declared." ), + NoExplicitNull, ] = None @@ -99,18 +104,21 @@ class PackageSignalTargeting5(PackageSignalTargeting1, PackageSignalTargeting4): Field( description="Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost." ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str | None, Field( description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the product option exposes a separate runtime or activation handle, and pass it verbatim. Buyers SHOULD prefer an exposed segment handle over reconstructing condition identity from categorical values because the handle can carry provider namespace and methodology distinctions.' ), + NoExplicitNull, ] = None activation_key: Annotated[ activation_key_1.ActivationKey | None, Field( description='Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.' ), + NoExplicitNull, ] = None @@ -123,18 +131,21 @@ class PackageSignalTargeting6(PackageSignalTargeting2, PackageSignalTargeting4): Field( description="Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost." ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str | None, Field( description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the product option exposes a separate runtime or activation handle, and pass it verbatim. Buyers SHOULD prefer an exposed segment handle over reconstructing condition identity from categorical values because the handle can carry provider namespace and methodology distinctions.' ), + NoExplicitNull, ] = None activation_key: Annotated[ activation_key_1.ActivationKey | None, Field( description='Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.' ), + NoExplicitNull, ] = None @@ -147,18 +158,21 @@ class PackageSignalTargeting7(PackageSignalTargeting3, PackageSignalTargeting4): Field( description="Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost." ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str | None, Field( description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the product option exposes a separate runtime or activation handle, and pass it verbatim. Buyers SHOULD prefer an exposed segment handle over reconstructing condition identity from categorical values because the handle can carry provider namespace and methodology distinctions.' ), + NoExplicitNull, ] = None activation_key: Annotated[ activation_key_1.ActivationKey | None, Field( description='Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/package_targeting_resolution.py b/src/adcp/types/domains/core/package_targeting_resolution.py index 2c7d8c445..3ff114749 100644 --- a/src/adcp/types/domains/core/package_targeting_resolution.py +++ b/src/adcp/types/domains/core/package_targeting_resolution.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/package_targeting_resolution.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import demographic_targeting_resolution @@ -23,4 +23,4 @@ class PackageTargetingResolution(AdCPBaseModel): description='Canonical demographic predicate and exact seller execution details. Include whenever demographic targeting was requested or applied.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/pagination_request.py b/src/adcp/types/domains/core/pagination_request.py index 95ae0f082..633a44acb 100644 --- a/src/adcp/types/domains/core/pagination_request.py +++ b/src/adcp/types/domains/core/pagination_request.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/pagination_request.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -17,8 +17,10 @@ class PaginationRequest(AdCPBaseModel): max_results: Annotated[ SchemaInt | None, Field(description='Maximum number of items to return per page', ge=1, le=100), + NoExplicitNull, ] = 50 cursor: Annotated[ str | None, Field(description='Opaque cursor from a previous response to fetch the next page'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/pagination_response.py b/src/adcp/types/domains/core/pagination_response.py index 741a459e8..9c12bac40 100644 --- a/src/adcp/types/domains/core/pagination_response.py +++ b/src/adcp/types/domains/core/pagination_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/pagination_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -22,6 +22,7 @@ class PaginationResponse(AdCPBaseModel): Field( description='Opaque cursor to pass in the next request to fetch the next page. Only present when has_more is true.' ), + NoExplicitNull, ] = None total_count: Annotated[ SchemaInt | None, @@ -29,4 +30,5 @@ class PaginationResponse(AdCPBaseModel): description='Total number of items matching the query across all pages. Optional because not all backends can efficiently compute this.', ge=0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/performance_feedback.py b/src/adcp/types/domains/core/performance_feedback.py index e00dcc6b7..de74472de 100644 --- a/src/adcp/types/domains/core/performance_feedback.py +++ b/src/adcp/types/domains/core/performance_feedback.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/performance_feedback.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import attribution_methodology as attribution_methodology_1 @@ -43,11 +43,15 @@ class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None class Metric(AdCPBaseModel): @@ -66,6 +70,7 @@ class Metric(AdCPBaseModel): Field( description='Disambiguator — same shape as `committed_metrics.qualifier`. Carry `viewability_standard` for MRC vs GroupM viewability feedback; `completion_source` for seller- vs vendor-attested completion feedback; `attribution_methodology` and `attribution_window` for outcome-metric feedback (the buyer pinning a specific methodology or window when the feedback is methodology-sensitive). Closed (`additionalProperties: false`); new keys ship explicitly in subsequent minors.' ), + NoExplicitNull, ] = None @@ -92,6 +97,7 @@ class Metric7(AdCPBaseModel): Field( description='Optional disambiguator mirroring the vendor-scope qualifier on `committed_metrics` — same closed key set as standard-scope entries.' ), + NoExplicitNull, ] = None @@ -108,9 +114,12 @@ class PerformanceFeedback(AdCPBaseModel): Field( description='Specific package within the media buy (if feedback is package-specific)' ), + NoExplicitNull, ] = None creative_id: Annotated[ - str | None, Field(description='Specific creative asset (if feedback is creative-specific)') + str | None, + Field(description='Specific creative asset (if feedback is creative-specific)'), + NoExplicitNull, ] = None measurement_period: Annotated[ MeasurementPeriod, Field(description='Time period for performance measurement') @@ -128,6 +137,7 @@ class PerformanceFeedback(AdCPBaseModel): deprecated=True, description='**Deprecated as of this minor.** The legacy free-form metric enum that mixes metrics, verification, and attribution into one list. New implementations SHOULD use `metric` (the discriminated `(scope, metric_id, qualifier)` row shape) and populate `metric_type` with a best-effort string for one-minor backwards compatibility. When both `metric` and `metric_type` are present, consumers MUST use `metric` for dispatch. Removed at the next major. See [docs/measurement/taxonomy](https://docs.adcontextprotocol.org/docs/measurement/taxonomy) for why the layered shape replaces the flat enum.', ), + NoExplicitNull, ] = None metric: Annotated[ Metric | Metric7 | None, @@ -135,6 +145,7 @@ class PerformanceFeedback(AdCPBaseModel): description='The metric this feedback row pertains to, using the same `(scope, metric_id, qualifier)` row shape as `committed_metrics` and package-level delivery values (`metric_values` or `vendor_metric_values`). Preferred over the legacy `metric_type` field for new implementations. Brings performance-feedback into the same atomic unit and dispatch model as the rest of the measurement surface — buyer agents reconcile feedback against the contract surface using the row-level join on `(scope, metric_id, qualifier)`. **Optional and may be omitted entirely for holistic feedback** (e.g., a trader flagging a campaign as underperforming without a specific metric in mind — `performance_index` plus the response narrative carry the signal). Senders SHOULD populate `metric` when the feedback is metric-specific so consumers can route it to the right optimization path; senders MAY omit it for general performance feedback.', discriminator='scope', ), + NoExplicitNull, ] = None feedback_source: Annotated[ feedback_source_1.FeedbackSource, Field(description='Source of the performance data') @@ -144,6 +155,7 @@ class PerformanceFeedback(AdCPBaseModel): Field( description="Vendor that produced this feedback. SHOULD be populated when `feedback_source` is `third_party_measurement` or `verification_partner` AND a single attesting vendor exists — without it, the row is unattributed and consumers can't verify authorization, resolve metric definitions, or route disputes. OMIT for blended outputs where no single vendor owns the result: MMM mixes (Nielsen MMM, Analytic Partners, in-house mix models combining multiple vendor inputs), multi-touch attribution outputs that join across vendors, and clean-room outputs (LiveRamp, Habu, AWS Clean Rooms) where the clean room is not the measurement source. For these cases, leave `vendor` absent and use the response's narrative payload to describe provenance. Optional for `buyer_attribution` and `platform_analytics` (those sources are implicit from context). The vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Same identity discipline as `vendor_metric_value.vendor` and `performance-standard.vendor`." ), + NoExplicitNull, ] = None status: Annotated[Status, Field(description='Processing status of the performance feedback')] submitted_at: Annotated[ @@ -154,4 +166,5 @@ class PerformanceFeedback(AdCPBaseModel): Field( description='ISO 8601 timestamp when feedback was applied to optimization algorithms' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/performance_feedback_assertion.py b/src/adcp/types/domains/core/performance_feedback_assertion.py index b7ee9f325..54f6dcf84 100644 --- a/src/adcp/types/domains/core/performance_feedback_assertion.py +++ b/src/adcp/types/domains/core/performance_feedback_assertion.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/performance_feedback_assertion.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..enums import feedback_source as feedback_source_1 @@ -28,12 +28,13 @@ class Evidence(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - sample_size: Annotated[SchemaInt | None, Field(ge=1)] = None + sample_size: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None confidence_interval: Annotated[ ConfidenceInterval | None, Field( description='Confidence interval on the performance_index scale. sample_size may be omitted when the producer cannot disclose it.' ), + NoExplicitNull, ] = None @@ -54,6 +55,7 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): description='Receiver-scoped package identifier when the assertion is package-specific. The orchestrator maps its measurement-facing identifier to seller-local identifiers during fan-out.', min_length=1, ), + NoExplicitNull, ] = None creative_id: Annotated[ str | None, @@ -61,6 +63,7 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): description='Receiver-scoped creative identifier when the assertion is creative-specific. The orchestrator maps its measurement-facing identifier to seller-local identifiers during fan-out.', min_length=1, ), + NoExplicitNull, ] = None measurement_period: Annotated[ datetime_range.DatetimeRange, @@ -78,12 +81,14 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): Field( description='Expectation represented by performance_index = 1.0. Compact-contract producers MUST populate this field; it remains optional in the schema so existing 3.x request payloads remain valid.' ), + NoExplicitNull, ] = None metric: Annotated[ performance_feedback_metric.PerformanceFeedbackMetric | None, Field( description='Metric this assertion describes. Omit only for holistic feedback that is not attributable to one metric.' ), + NoExplicitNull, ] = None metric_type: Annotated[ metric_type_1.MetricType | None, @@ -91,12 +96,14 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): deprecated=True, description='Deprecated legacy metric classification retained for existing request payloads. New implementations SHOULD use metric; when both are present consumers MUST use metric for dispatch.', ), + NoExplicitNull, ] = None producer: Annotated[ brand_ref.BrandReference | None, Field( description='Brand that produced the analysis. The receiving orchestrator gateway verifies this reference against authenticated caller identity and preserves it during seller fan-out.' ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference | None, @@ -104,10 +111,12 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): deprecated=True, description='Deprecated alias for producer retained for previously documented request payloads. When both are present consumers use producer.', ), + NoExplicitNull, ] = None feedback_source: Annotated[ feedback_source_1.FeedbackSource | None, Field(description='Legacy categorical source hint retained for existing request payloads.'), + NoExplicitNull, ] = None methodology: Annotated[ str | None, @@ -116,6 +125,7 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): max_length=100, min_length=1, ), + NoExplicitNull, ] = None methodology_version: Annotated[ str | None, @@ -124,6 +134,7 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): max_length=100, min_length=1, ), + NoExplicitNull, ] = None study_ref: Annotated[ str | None, @@ -132,27 +143,33 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None evidence: Annotated[ Evidence | None, Field( description='Small inline evidence summary used to weight the assertion without transporting the full study.' ), + NoExplicitNull, ] = None evidence_ref: Annotated[ AnyUrl | None, Field( description='Provider-hosted evidence or result reference for parties authorized to inspect the full study.' ), + NoExplicitNull, ] = None as_of: Annotated[ - AwareDatetime | None, Field(description='When the producer computed this assertion.') + AwareDatetime | None, + Field(description='When the producer computed this assertion.'), + NoExplicitNull, ] = None final: Annotated[ StrictBool | None, Field( description='Whether the producer expects this assertion to be revised as data matures.' ), + NoExplicitNull, ] = None supersedes_feedback_id: Annotated[ str | None, @@ -160,4 +177,5 @@ class PerformanceFeedbackAssertion(AdCPBaseModel): description='Receiver-issued feedback_id of the earlier assertion this one replaces at the same hop.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/performance_feedback_metric.py b/src/adcp/types/domains/core/performance_feedback_metric.py index b349e74cc..e67e0af9f 100644 --- a/src/adcp/types/domains/core/performance_feedback_metric.py +++ b/src/adcp/types/domains/core/performance_feedback_metric.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/performance_feedback_metric.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import attribution_methodology as attribution_methodology_1 @@ -21,11 +21,15 @@ class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None class PerformanceFeedbackMetric1(AdCPBaseModel): @@ -39,6 +43,7 @@ class PerformanceFeedbackMetric1(AdCPBaseModel): Field( description='Identity-affecting metric qualifiers. Statistical evidence and feedback-producer identity do not belong here.' ), + NoExplicitNull, ] = None @@ -59,6 +64,7 @@ class PerformanceFeedbackMetric2(AdCPBaseModel): Field( description='Optional disambiguator mirroring the vendor-scope qualifier on `committed_metrics` — same closed key set as standard-scope entries.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/performance_standard.py b/src/adcp/types/domains/core/performance_standard.py index 9026fbe00..075a62128 100644 --- a/src/adcp/types/domains/core/performance_standard.py +++ b/src/adcp/types/domains/core/performance_standard.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/performance_standard.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import performance_standard_metric, viewability_standard @@ -34,6 +34,7 @@ class PerformanceStandard(AdCPBaseModel): Field( description="Measurement standard. Required when metric is 'viewability' (MRC and GroupM define materially different thresholds). Omit for other metrics." ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference, diff --git a/src/adcp/types/domains/core/placement.py b/src/adcp/types/domains/core/placement.py index 74e87e5fa..e5b3e7aaa 100644 --- a/src/adcp/types/domains/core/placement.py +++ b/src/adcp/types/domains/core/placement.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/placement.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from collections.abc import Sequence from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, model_validator from ..enums import ( @@ -66,6 +66,7 @@ class ProductDoohPlacementAttributes(AdCPBaseModel): description='Scheduled duration of one ad slot in seconds. This is an inventory fact used for loop and share calculations, not the creative-duration contract.', ge=1, ), + NoExplicitNull, ] = None loop_duration_seconds: Annotated[ SchemaInt | None, @@ -73,13 +74,15 @@ class ProductDoohPlacementAttributes(AdCPBaseModel): description='Duration of the full ad loop rotation in seconds and the canonical source for loop duration.', ge=1, ), + NoExplicitNull, ] = None - screen_resolution: ProductDoohScreenResolution | None = None + screen_resolution: Annotated[ProductDoohScreenResolution | None, NoExplicitNull] = None motion: Annotated[ dooh_motion_type.DoohMotionType | None, Field( description='Physical motion capability of a visual DOOH screen, not an accepted-format declaration. Omit for audio-only placements.' ), + NoExplicitNull, ] = None @@ -105,21 +108,26 @@ class Placement(AdCPBaseModel): description="For publisher_ref, the domain whose adagents.json declares the placement and part of canonical identity. For seller_inline, optional inventory-publisher attribution only; it does not grant the seller authority to mint IDs in that publisher's catalog namespace.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None seller_agent: Annotated[ seller_agent_ref.SellerAgentReference | None, Field( description='Sales agent that defines a seller_inline placement. Together with placement_id this is its self-contained identity. New 3.2 sellers SHOULD populate it; legacy product-context inline placements may omit it. Not used for publisher_ref.' ), + NoExplicitNull, ] = None name: Annotated[ str | None, Field( description='Human-readable name for the placement (e.g., \'Homepage Banner\', \'Article Sidebar\'). Required for `kind: "seller_inline"`. May be omitted for publisher-referenced placements because buyers resolve the name from the publisher declaration identified by `{publisher_domain, placement_id}`.' ), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Detailed description of where and how the placement appears') + str | None, + Field(description='Detailed description of where and how the placement appears'), + NoExplicitNull, ] = None mode: Annotated[ Mode, @@ -132,6 +140,7 @@ class Placement(AdCPBaseModel): Field( description="Optional tags for grouping placements within a product (e.g., 'homepage', 'native', 'premium'). When the placement_id comes from the publisher registry, these should align with the registry tags unless the product is narrowing scope." ), + NoExplicitNull, ] = None format_ids: Annotated[ Sequence[format_id.FormatReferenceStructuredObject] | None, @@ -140,6 +149,7 @@ class Placement(AdCPBaseModel): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format placement narrowing. Can include concrete, template, or parameterized format IDs. When present on a product placement, this field narrows the product-level `format_ids` contract and MUST NOT introduce formats the product does not accept. Use canonical `format_options`.', min_length=1, ), + NoExplicitNull, ] = None format_options: Annotated[ list[product_format_declaration.ProductFormatDeclaration] | None, @@ -147,6 +157,7 @@ class Placement(AdCPBaseModel): description="Canonical seller-side narrowing for this product placement. When present, these declarations are intersected with the product-level format_options and MUST NOT introduce a format outside that product upper bound. For kind publisher_ref, buyers MUST also resolve {publisher_domain, placement_id} in the publisher's adagents.json and intersect the publisher catalog constraint: use the public placement's format_options when present (resolving bare format_option_id references against same-file top-level formats[]), otherwise use applicable top-level formats[] scoped to that placement's properties. Omitting this inline field removes only the seller-inline layer; it does not bypass a publisher placement or property-scoped narrowing. The placement inherits the full product-level set only when no applicable publisher catalog narrowing exists. Unresolved publisher placement or format-option references fail closed. Locale policy participates in the same intersection: when the product policy is absent, a placement may introduce any concrete policy as a narrowing of the unconstrained option; when both are present, every placement accepted_language_range must be contained by a product range under RFC 4647 Basic Filtering (`fr-CA` narrows `fr`; `fr` does not narrow `fr-CA`). Buyers compute effective locale eligibility independently for each placement. Any effective locale-constrained route is canonical-only and has no projecting product or placement format_id.", min_length=1, ), + NoExplicitNull, ] = None video_placement_types: Annotated[ list[video_placement_type.VideoPlacementType] | None, @@ -154,6 +165,7 @@ class Placement(AdCPBaseModel): description='Declared video placement types for this product placement, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None audio_distribution_types: Annotated[ list[audio_distribution_type.AudioDistributionType] | None, @@ -161,6 +173,7 @@ class Placement(AdCPBaseModel): description='Declared audio distribution types for this product placement, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ list[sponsored_placement_type.SponsoredPlacementType] | None, @@ -168,6 +181,7 @@ class Placement(AdCPBaseModel): description='Declared sponsored-placement types for this product placement, distinguishing where the catalog-driven retail-media placement renders on the retailer surface. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ list[social_placement_surface.SocialPlacementSurface] | None, @@ -175,6 +189,7 @@ class Placement(AdCPBaseModel): description='Declared social-placement surfaces for this product placement, distinguishing the in-app surface where the social placement renders. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None identifiers: Annotated[ list[Identifier] | None, @@ -182,8 +197,11 @@ class Placement(AdCPBaseModel): description='Optional external inventory identifiers for this placement, using the same {type, value} shape as property identifiers. Externally governed IDs should be authority-prefixed (e.g., space:1234931339, geopath:30961, fcc:73953). Seller-local IDs are opaque values scoped by the surrounding publisher namespace. Useful for DOOH venue and installed-endpoint IDs, broadcast facility IDs, and any channel where placements map to externally registered inventory. For kind: publisher_ref, the effective identifier set is the union of the resolved publisher declaration and this product declaration, de-duplicated by exact (type, value); a product cannot suppress a publisher-declared identifier by omission.', min_length=1, ), + NoExplicitNull, ] = None - dooh_placement_attributes: ProductDoohPlacementAttributes | None = None + dooh_placement_attributes: Annotated[ProductDoohPlacementAttributes | None, NoExplicitNull] = ( + None + ) @model_validator(mode='after') def _require_schema_required_group(self) -> Placement: diff --git a/src/adcp/types/domains/core/placement_definition.py b/src/adcp/types/domains/core/placement_definition.py index 31bcb111a..1a42aab88 100644 --- a/src/adcp/types/domains/core/placement_definition.py +++ b/src/adcp/types/domains/core/placement_definition.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_definition.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, model_validator from ..enums import audio_distribution_type @@ -59,6 +59,7 @@ class PublisherDoohPlacementAttributes(AdCPBaseModel): description='Default scheduled duration of one ad slot in seconds. This is an inventory fact used for loop and share calculations, not the creative-duration contract.', ge=1, ), + NoExplicitNull, ] = None loop_duration_seconds: Annotated[ SchemaInt | None, @@ -66,13 +67,15 @@ class PublisherDoohPlacementAttributes(AdCPBaseModel): description='Duration of the full ad loop rotation in seconds and the canonical source for loop duration.', ge=1, ), + NoExplicitNull, ] = None - screen_resolution: PublisherDoohScreenResolution | None = None + screen_resolution: Annotated[PublisherDoohScreenResolution | None, NoExplicitNull] = None motion: Annotated[ dooh_motion_type.DoohMotionType | None, Field( description='Physical motion capability of a visual DOOH screen, not an accepted-format declaration. Omit for audio-only placements.' ), + NoExplicitNull, ] = None @@ -88,6 +91,7 @@ class FormatOptions(AdCPBaseModel): Field( description='Optional placement-local locale policy. When the resolved top-level format declares a policy, every placement range must be contained by one of its ranges; otherwise this introduces a narrowing of the unconstrained format. The resolved effective route remains canonical-only.' ), + NoExplicitNull, ] = None @@ -105,13 +109,16 @@ class PlacementDefinition(AdCPBaseModel): ), ] description: Annotated[ - str | None, Field(description='Description of where and how this placement appears.') + str | None, + Field(description='Description of where and how this placement appears.'), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, Field( description="Tags for grouping and querying placements across properties and products (e.g., 'homepage', 'native', 'premium', 'pre_roll')." ), + NoExplicitNull, ] = None property_ids: Annotated[ list[property_id.PropertyId] | None, @@ -119,6 +126,7 @@ class PlacementDefinition(AdCPBaseModel): description='Property IDs in this adagents.json where this placement can appear.', min_length=1, ), + NoExplicitNull, ] = None property_tags: Annotated[ list[property_tag.PropertyTag] | None, @@ -126,6 +134,7 @@ class PlacementDefinition(AdCPBaseModel): description="Property tags in this adagents.json where this placement can appear. Useful for network-wide positions such as 'pre_roll' or 'homepage_native_feed'.", min_length=1, ), + NoExplicitNull, ] = None collection_ids: Annotated[ list[str] | None, @@ -133,6 +142,7 @@ class PlacementDefinition(AdCPBaseModel): description='Optional collection IDs in this adagents.json where this placement is valid. Use to narrow a placement to specific content programs carried on the selected properties.', min_length=1, ), + NoExplicitNull, ] = None channels: Annotated[ list[channels_1.MediaChannel] | None, @@ -140,18 +150,21 @@ class PlacementDefinition(AdCPBaseModel): description='Advertising channels where this placement can run. Products that reference the placement may narrow this set but should not broaden it.', min_length=1, ), + NoExplicitNull, ] = None presentation_ref: Annotated[ presentation_ref_1.PlacementPresentationReference | None, Field( description="Optional publisher-specific declarative frame for representing this placement's real chrome offline. It composes around the selected creative rendering unless the publisher-delegated preview_provider route explicitly covers placement presentation. It MUST NOT be promoted to or copied onto a shared format entry." ), + NoExplicitNull, ] = None preview_provider: Annotated[ preview_provider_1.PublisherDesignatedPreviewProvider | None, Field( description="Optional publisher delegation to a callable AdCP preview provider for specific format options on this placement. This publisher-origin route is the only mechanism that grants preview authority inside the placement's scope; an agent's own rendering_origin description does not." ), + NoExplicitNull, ] = None format_options: Annotated[ list[FormatOptions | product_format_declaration.ProductFormatDeclaration] | None, @@ -159,6 +172,7 @@ class PlacementDefinition(AdCPBaseModel): description="Optional 3.1+ canonical format-option declarations supported by this placement. This `adagents.json` placement surface supports two entry shapes: (1) reference an entry in the same file's top-level `formats[]` by `format_option_id` only — buyers resolve the full declaration from `formats[]` by matching `format_option_id` (recommended; avoids duplication). Top-level formats may be publisher-owned custom formats or narrowed canonical formats; their `format_kind` is the canonical anchor that the placement reference inherits. (2) carry an inline `ProductFormatDeclaration` directly — for placement-specific canonical narrowing that doesn't fit a reusable catalog entry. This bare-reference shape is placement-catalog specific; Product `format_options[]` entries are always full `ProductFormatDeclaration` objects with required `format_kind` and `params`.\n\nProduct-level formats remain the upper bound for a sellable product. Catalog placement formats describe placement support; when a product references the placement and also declares product-level formats, buyers use the intersection for that product placement. A catalog placement format that is absent from the product-level declaration is not accepted for that product unless the product explicitly includes it. When the product locale policy is absent, placement locale_policy may introduce any concrete narrowing; when both are present, every placement accepted_language_range must be contained by a product range under RFC 4647 Basic Filtering. An effective placement policy is canonical-only: its resolved catalog and matching product declarations cannot project through legacy format_ids.\n\n**Format-option reference shape.** A format-option reference entry SHOULD carry ONLY `format_option_id` — extra fields are allowed (`additionalProperties: true`) so adopters who want to attach a placement-local override like `display_name` or a narrower `locale_policy` don't get rejected by the branch boundary, but buyer SDKs MUST resolve the format from the top-level `formats[]` by `format_option_id` and apply additional fields on the entry as placement-level overrides (NOT as a partial inline declaration). If a publisher needs to materially narrow the format at the placement, use the inline-declaration form instead.\n\n**Resolution scope is same-file only.** `format_option_id` resolves only within this file's top-level `formats[]`; cross-file references are not supported by design because same-file resolution keeps validation bounded and prevents a file from squatting on or narrowing another publisher's format_option_id. When `format_options[]` references a `format_option_id` not declared in the file's top-level `formats[]`, validators MUST surface this as `FORMAT_OPTION_UNRESOLVED` on the response `errors[]`. Buyers MUST fail closed for that placement (drop the format from the placement's accepted set) rather than silently dropping the placement or guessing intent.", min_length=1, ), + NoExplicitNull, ] = None video_placement_types: Annotated[ list[video_placement_type.VideoPlacementType] | None, @@ -166,6 +180,7 @@ class PlacementDefinition(AdCPBaseModel): description='Declared video placement types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None audio_distribution_types: Annotated[ list[audio_distribution_type.AudioDistributionType] | None, @@ -173,6 +188,7 @@ class PlacementDefinition(AdCPBaseModel): description='Declared audio distribution types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ list[sponsored_placement_type.SponsoredPlacementType] | None, @@ -180,6 +196,7 @@ class PlacementDefinition(AdCPBaseModel): description='Declared sponsored-placement types for this publisher placement, distinguishing where the catalog-driven retail-media placement renders on the retailer surface. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ list[social_placement_surface.SocialPlacementSurface] | None, @@ -187,6 +204,7 @@ class PlacementDefinition(AdCPBaseModel): description='Declared social-placement surfaces for this publisher placement, distinguishing the in-app surface where the social placement renders. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None identifiers: Annotated[ list[Identifier] | None, @@ -194,9 +212,12 @@ class PlacementDefinition(AdCPBaseModel): description='Optional external inventory identifiers for this placement, using the same {type, value} shape as property identifiers. Externally governed IDs should be authority-prefixed (e.g., space:1234931339, geopath:30961, fcc:73953). Seller-local IDs are opaque values scoped by the surrounding publisher namespace. Product-level placement declarations may carry additional identifiers but SHOULD NOT contradict publisher-declared identifiers for the same type.', min_length=1, ), + NoExplicitNull, ] = None - dooh_placement_attributes: PublisherDoohPlacementAttributes | None = None - ext: ext_1.ExtensionObject | None = None + dooh_placement_attributes: Annotated[ + PublisherDoohPlacementAttributes | None, NoExplicitNull + ] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> PlacementDefinition: diff --git a/src/adcp/types/domains/core/placement_delivery_metrics.py b/src/adcp/types/domains/core/placement_delivery_metrics.py index 6b908e6b6..b88891ba9 100644 --- a/src/adcp/types/domains/core/placement_delivery_metrics.py +++ b/src/adcp/types/domains/core/placement_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from . import placement_identity as placement_identity_1 @@ -18,12 +19,14 @@ class PlacementDeliveryMetrics(DeliveryMetrics): Field( description='Self-contained identity. publisher_ref resolves in adagents.json; seller_inline is scoped by seller_agent.' ), + NoExplicitNull, ] = None placement_name: Annotated[ str | None, Field( description='Current human-readable placement name. Convenience metadata only; placement_identity is stable identity.' ), + NoExplicitNull, ] = None placement_id: Annotated[ str, @@ -37,6 +40,7 @@ class PlacementDeliveryMetrics(DeliveryMetrics): description='Legacy publisher attribution. For publisher_ref identity it MUST equal placement_identity.publisher_domain. For seller_inline it identifies inventory context only and is not identity authority.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/placement_evidence.py b/src/adcp/types/domains/core/placement_evidence.py index 7ef6fcb9e..9897f25f4 100644 --- a/src/adcp/types/domains/core/placement_evidence.py +++ b/src/adcp/types/domains/core/placement_evidence.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_evidence.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictFloat @@ -21,17 +21,22 @@ class PlacementEvidence(AdCPBaseModel): ), ] captured_at: Annotated[ - AwareDatetime | None, Field(description='When the artifact was captured') + AwareDatetime | None, Field(description='When the artifact was captured'), NoExplicitNull ] = None latitude: Annotated[ - StrictFloat | None, Field(description='Capture location latitude', ge=-90.0, le=90.0) + StrictFloat | None, + Field(description='Capture location latitude', ge=-90.0, le=90.0), + NoExplicitNull, ] = None longitude: Annotated[ - StrictFloat | None, Field(description='Capture location longitude', ge=-180.0, le=180.0) + StrictFloat | None, + Field(description='Capture location longitude', ge=-180.0, le=180.0), + NoExplicitNull, ] = None notes: Annotated[ str | None, Field( description="Context that doesn't fit the structured fields (e.g., 'shot from the eastbound approach at 40mph equivalent')" ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/placement_presentation.py b/src/adcp/types/domains/core/placement_presentation.py index 39fb84e90..2a19c3262 100644 --- a/src/adcp/types/domains/core/placement_presentation.py +++ b/src/adcp/types/domains/core/placement_presentation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/placement_presentation.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field, field_validator @@ -18,7 +18,9 @@ class Canvas(AdCPBaseModel): ) width: Annotated[SchemaInt, Field(ge=1, le=8192)] height: Annotated[SchemaInt, Field(ge=1, le=8192)] - background_color: Annotated[str | None, Field(pattern='^#[0-9A-Fa-f]{6}$')] = '#ffffff' + background_color: Annotated[str | None, Field(pattern='^#[0-9A-Fa-f]{6}$'), NoExplicitNull] = ( + '#ffffff' + ) class Fit(StrEnum): @@ -138,5 +140,7 @@ class PlacementPresentationDocument(AdCPBaseModel): ), ] decorations: Annotated[ - list[BoxDecoration | TextDecoration | ImageDecoration] | None, Field(max_length=100) + list[BoxDecoration | TextDecoration | ImageDecoration] | None, + Field(max_length=100), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/placement_property_delivery_metrics.py b/src/adcp/types/domains/core/placement_property_delivery_metrics.py index 05f362f52..bed839cc2 100644 --- a/src/adcp/types/domains/core/placement_property_delivery_metrics.py +++ b/src/adcp/types/domains/core/placement_property_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_property_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from . import identifier as identifier_1 @@ -28,7 +29,7 @@ class PlacementPropertyDeliveryMetrics(DeliveryMetrics): ), ] placement_name: Annotated[ - str | None, Field(description='Current convenience name for the placement.') + str | None, Field(description='Current convenience name for the placement.'), NoExplicitNull ] = None publisher_domain: Annotated[ str, @@ -46,9 +47,10 @@ class PlacementPropertyDeliveryMetrics(DeliveryMetrics): Field( description="Canonical publisher-scoped property catalog identity when available. Its publisher_domain MUST equal the row's publisher_domain." ), + NoExplicitNull, ] = None property_name: Annotated[ - str | None, Field(description='Current convenience name for the property.') + str | None, Field(description='Current convenience name for the property.'), NoExplicitNull ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/placement_ref.py b/src/adcp/types/domains/core/placement_ref.py index 1a095e0dc..01e4e99e5 100644 --- a/src/adcp/types/domains/core/placement_ref.py +++ b/src/adcp/types/domains/core/placement_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_ref.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -20,6 +20,7 @@ class PlacementReference(AdCPBaseModel): description='Domain where the adagents.json declaring a publisher-catalog placement is hosted, or the inventory publisher associated with an inline placement. Omitted only for legacy single-publisher product-context references.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None placement_id: Annotated[ str, diff --git a/src/adcp/types/domains/core/placement_selection.py b/src/adcp/types/domains/core/placement_selection.py index 911c8ad8e..13305b557 100644 --- a/src/adcp/types/domains/core/placement_selection.py +++ b/src/adcp/types/domains/core/placement_selection.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/placement_selection.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -18,7 +18,7 @@ class PlacementSelection2(AdCPBaseModel): extra='forbid', ) mode: Literal['default'] = 'default' - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class PlacementSelection1(AdCPBaseModel): @@ -33,7 +33,7 @@ class PlacementSelection1(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None PlacementSelection = Annotated[ diff --git a/src/adcp/types/domains/core/planned_delivery.py b/src/adcp/types/domains/core/planned_delivery.py index d30ef3fb2..3ef41da84 100644 --- a/src/adcp/types/domains/core/planned_delivery.py +++ b/src/adcp/types/domains/core/planned_delivery.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/planned_delivery.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import channels as channels_1 @@ -24,9 +24,12 @@ class Geo(AdCPBaseModel): countries: Annotated[ list[str] | None, Field(description='ISO 3166-1 alpha-2 country codes where ads will deliver.'), + NoExplicitNull, ] = None regions: Annotated[ - list[str] | None, Field(description='ISO 3166-2 subdivision codes where ads will deliver.') + list[str] | None, + Field(description='ISO 3166-2 subdivision codes where ads will deliver.'), + NoExplicitNull, ] = None @@ -40,6 +43,7 @@ class PlannedDelivery(AdCPBaseModel): description='Seller-assigned media buy identifier. Optional on a purchase-phase prepare/check because the service may not assign the identifier until commit; required on modification and delivery lifecycle checks.', min_length=1, ), + NoExplicitNull, ] = None proposal_id: Annotated[ str | None, @@ -48,6 +52,7 @@ class PlannedDelivery(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None proposal_terms_digest: Annotated[ str | None, @@ -55,27 +60,35 @@ class PlannedDelivery(AdCPBaseModel): description='Digest of the proposal commercial_terms. The governance agent compares it to the digest bound during the intent check.', pattern='^sha256:[A-Za-z0-9_-]{43}$', ), + NoExplicitNull, + ] = None + geo: Annotated[ + Geo | None, Field(description='Geographic targeting the seller will apply.'), NoExplicitNull ] = None - geo: Annotated[Geo | None, Field(description='Geographic targeting the seller will apply.')] = ( - None - ) channels: Annotated[ list[channels_1.MediaChannel] | None, Field(description='Channels the seller will deliver on.'), + NoExplicitNull, ] = None start_time: Annotated[ - AwareDatetime | None, Field(description='Actual flight start the seller will use.') + AwareDatetime | None, + Field(description='Actual flight start the seller will use.'), + NoExplicitNull, ] = None end_time: Annotated[ - AwareDatetime | None, Field(description='Actual flight end the seller will use.') + AwareDatetime | None, + Field(description='Actual flight end the seller will use.'), + NoExplicitNull, ] = None frequency_cap: Annotated[ frequency_cap_1.FrequencyCap | None, Field(description='Frequency cap the seller will apply.'), + NoExplicitNull, ] = None audience_summary: Annotated[ str | None, Field(description='Human-readable summary of the audience the seller will target.'), + NoExplicitNull, ] = None audience_targeting: Annotated[ list[audience_selector.AudienceSelector] | None, @@ -83,10 +96,12 @@ class PlannedDelivery(AdCPBaseModel): description='Structured audience targeting the seller will activate. Each entry is either a signal reference or a descriptive criterion. When present, governance agents MUST use this for bias/fairness validation and SHOULD ignore audience_summary for validation purposes. The audience_summary field is a human-readable rendering of this array, not an independent declaration.', min_length=1, ), + NoExplicitNull, ] = None total_budget: Annotated[ StrictFloat | None, Field(description='Total budget the seller will deliver against.', ge=0.0), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -94,12 +109,14 @@ class PlannedDelivery(AdCPBaseModel): description='Hard aggregate daily spend ceiling the seller will enforce. Governance checks compare it with the authorized execution controls; it does not allocate spend to packages.', ge=0.0, ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, Field( description='IANA timezone defining the calendar-day boundary for every daily cap on the planned media buy.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -107,25 +124,30 @@ class PlannedDelivery(AdCPBaseModel): description='ISO 4217 currency code for the budget. Governance execution checks require it whenever total_budget is present and require it to match the intent-authorized currency.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None budget_allocation: Annotated[ budget_allocation_1.BudgetAllocation | None, Field( description='Seller-accepted cross-package allocation authority and goals. Presence with seller_optimized mode means automatic within-buy reallocations are part of the committed delivery, not separate modification actions.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field(description='Aggregate pacing strategy the seller will apply to total_budget.'), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Seller-interpreted media-buy bidding policy used for governance and delivery transparency. Goal-bound controls follow budget-allocation scope semantics and monetary fields use the planned delivery currency. Package-authored overrides, including explicit automatic overrides, remain on packages rather than being copied into this aggregate field.' ), + NoExplicitNull, ] = None enforced_policies: Annotated[ list[str] | None, Field(description='Registry policy IDs the seller will enforce for this delivery.'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/positive_postal_area_support.py b/src/adcp/types/domains/core/positive_postal_area_support.py index 89e553e00..000bb58c3 100644 --- a/src/adcp/types/domains/core/positive_postal_area_support.py +++ b/src/adcp/types/domains/core/positive_postal_area_support.py @@ -1,25 +1,26 @@ # generated by datamodel-codegen: # filename: core/positive_postal_area_support.json -# timestamp: 2026-10-03T14:21:15+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull from pydantic import Field from .postal_area_support import PostalAreaSupport class PositivePostalAreaSupport(PostalAreaSupport): - us_zip: Annotated[Literal[True] | None, Field(deprecated=True)] = None - us_zip_plus_four: Annotated[Literal[True] | None, Field(deprecated=True)] = None - gb_outward: Annotated[Literal[True] | None, Field(deprecated=True)] = None - gb_full: Annotated[Literal[True] | None, Field(deprecated=True)] = None - ca_fsa: Annotated[Literal[True] | None, Field(deprecated=True)] = None - ca_full: Annotated[Literal[True] | None, Field(deprecated=True)] = None - de_plz: Annotated[Literal[True] | None, Field(deprecated=True)] = None - fr_code_postal: Annotated[Literal[True] | None, Field(deprecated=True)] = None - au_postcode: Annotated[Literal[True] | None, Field(deprecated=True)] = None - ch_plz: Annotated[Literal[True] | None, Field(deprecated=True)] = None - at_plz: Annotated[Literal[True] | None, Field(deprecated=True)] = None + us_zip: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + us_zip_plus_four: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + gb_outward: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + gb_full: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + ca_fsa: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + ca_full: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + de_plz: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + fr_code_postal: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + au_postcode: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + ch_plz: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None + at_plz: Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/postal_area_support.py b/src/adcp/types/domains/core/postal_area_support.py index 2dc9e2910..150f128bd 100644 --- a/src/adcp/types/domains/core/postal_area_support.py +++ b/src/adcp/types/domains/core/postal_area_support.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/postal_area_support.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Dict, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool @@ -38,25 +38,25 @@ class PostalAreaSupport(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - US: Annotated[list[ME] | None, Field(min_length=1)] = None - GB: Annotated[list[GBEnum] | None, Field(min_length=1)] = None - CA: Annotated[list[CAEnum] | None, Field(min_length=1)] = None - DE: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - CH: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - AT: Annotated[list[Literal['plz']] | None, Field(min_length=1)] = None - FR: Annotated[list[Literal['code_postal']] | None, Field(min_length=1)] = None - AU: Annotated[list[Literal['postcode']] | None, Field(min_length=1)] = None - BR: Annotated[list[Literal['cep']] | None, Field(min_length=1)] = None - IN: Annotated[list[Literal['pin']] | None, Field(min_length=1)] = None - ZA: Annotated[list[Literal['postal_code']] | None, Field(min_length=1)] = None - us_zip: Annotated[StrictBool | None, Field(deprecated=True)] = None - us_zip_plus_four: Annotated[StrictBool | None, Field(deprecated=True)] = None - gb_outward: Annotated[StrictBool | None, Field(deprecated=True)] = None - gb_full: Annotated[StrictBool | None, Field(deprecated=True)] = None - ca_fsa: Annotated[StrictBool | None, Field(deprecated=True)] = None - ca_full: Annotated[StrictBool | None, Field(deprecated=True)] = None - de_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None - fr_code_postal: Annotated[StrictBool | None, Field(deprecated=True)] = None - au_postcode: Annotated[StrictBool | None, Field(deprecated=True)] = None - ch_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None - at_plz: Annotated[StrictBool | None, Field(deprecated=True)] = None + US: Annotated[list[ME] | None, Field(min_length=1), NoExplicitNull] = None + GB: Annotated[list[GBEnum] | None, Field(min_length=1), NoExplicitNull] = None + CA: Annotated[list[CAEnum] | None, Field(min_length=1), NoExplicitNull] = None + DE: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + CH: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + AT: Annotated[list[Literal['plz']] | None, Field(min_length=1), NoExplicitNull] = None + FR: Annotated[list[Literal['code_postal']] | None, Field(min_length=1), NoExplicitNull] = None + AU: Annotated[list[Literal['postcode']] | None, Field(min_length=1), NoExplicitNull] = None + BR: Annotated[list[Literal['cep']] | None, Field(min_length=1), NoExplicitNull] = None + IN: Annotated[list[Literal['pin']] | None, Field(min_length=1), NoExplicitNull] = None + ZA: Annotated[list[Literal['postal_code']] | None, Field(min_length=1), NoExplicitNull] = None + us_zip: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + us_zip_plus_four: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + gb_outward: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + gb_full: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ca_fsa: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ca_full: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + de_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + fr_code_postal: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + au_postcode: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + ch_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None + at_plz: Annotated[StrictBool | None, Field(deprecated=True), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/preview_provider.py b/src/adcp/types/domains/core/preview_provider.py index db2e2065a..e4317c416 100644 --- a/src/adcp/types/domains/core/preview_provider.py +++ b/src/adcp/types/domains/core/preview_provider.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/preview_provider.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool, field_validator @@ -33,6 +33,7 @@ class Route(AdCPBaseModel): Field( description="True only when the publisher delegates both creative rendering and the complete placement-specific frame to this route. When false or omitted, consumers compose any presentation_ref around the provider's creative render." ), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/core/price.py b/src/adcp/types/domains/core/price.py index 2efecc705..2e28ed3e4 100644 --- a/src/adcp/types/domains/core/price.py +++ b/src/adcp/types/domains/core/price.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/price.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -36,4 +36,5 @@ class Price(AdCPBaseModel): Field( description="Billing period. 'night' for hotel rates, 'month' or 'year' for salaries and rentals, 'one_time' for purchase prices. Omit when the period is obvious from context (e.g., a vehicle price is always one-time)." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/principal_changed_webhook.py b/src/adcp/types/domains/core/principal_changed_webhook.py index d95c37469..18933a007 100644 --- a/src/adcp/types/domains/core/principal_changed_webhook.py +++ b/src/adcp/types/domains/core/principal_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/principal_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from . import ext as ext_1 @@ -91,5 +91,6 @@ class PrincipalChangedWebhook(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/principal_declarations.py b/src/adcp/types/domains/core/principal_declarations.py index a80f31bad..9ba9e9f22 100644 --- a/src/adcp/types/domains/core/principal_declarations.py +++ b/src/adcp/types/domains/core/principal_declarations.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/principal_declarations.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -35,6 +35,7 @@ class AgentDeclarations(AdCPBaseModel): max_length=8, min_length=1, ), + NoExplicitNull, ] = None webhook_signing_algorithms: Annotated[ list[WebhookSigningAlgorithm] | None, @@ -42,6 +43,7 @@ class AgentDeclarations(AdCPBaseModel): description="RFC 9421 webhook-signing algorithms the caller can verify. The accepted intersection with the seller's webhook_signing.algorithms MUST be non-empty when the caller has any active webhook subscriber; an empty intersection fails the sync request with UNSUPPORTED_FEATURE because delivery would be unverifiable.", min_length=1, ), + NoExplicitNull, ] = None experimental_features: Annotated[ list[experimental_feature_id.ExperimentalFeatureId] | None, @@ -50,4 +52,5 @@ class AgentDeclarations(AdCPBaseModel): max_length=32, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/principal_declarations_state.py b/src/adcp/types/domains/core/principal_declarations_state.py index 0160698bd..78e55ff0b 100644 --- a/src/adcp/types/domains/core/principal_declarations_state.py +++ b/src/adcp/types/domains/core/principal_declarations_state.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/principal_declarations_state.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import principal_declarations @@ -55,6 +55,7 @@ class PrincipalDeclarationsState(AdCPBaseModel): description='The single AdCP minor version the seller will use for asynchronous payload shapes toward this principal. MUST be a member of accepted.async_adcp_versions and MUST be present whenever that set is non-empty, so the buyer knows the exact payload contract rather than inferring it from the intersection.', pattern='^\\d+\\.\\d+$', ), + NoExplicitNull, ] = None exclusions: Annotated[ list[Exclusion] | None, @@ -62,4 +63,5 @@ class PrincipalDeclarationsState(AdCPBaseModel): description="Every declared value that is absent from the accepted intersection, with the seller's reason. Present whenever declared and accepted differ, so a buyer can see why a capability it relies on was not accepted instead of diffing the two sets.", max_length=64, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/principal_state.py b/src/adcp/types/domains/core/principal_state.py index e903c6ce9..01364216a 100644 --- a/src/adcp/types/domains/core/principal_state.py +++ b/src/adcp/types/domains/core/principal_state.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/principal_state.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -37,7 +37,7 @@ class RetiredDestination(AdCPBaseModel): min_length=1, ), ] - revoked_at: AwareDatetime | None = None + revoked_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class PrincipalState(AdCPBaseModel): @@ -50,6 +50,7 @@ class PrincipalState(AdCPBaseModel): description='Current agent-level webhook subscribers. authentication.credentials is always omitted because it is write-only.', max_length=16, ), + NoExplicitNull, ] = None reporting_destinations: Annotated[ list[agent_reporting_destination_state.AgentReportingDestinationState] | None, @@ -57,12 +58,14 @@ class PrincipalState(AdCPBaseModel): description='Current reusable reporting destination bindings and setup states. destination_id and destination_ref values MUST each be unique within this caller-scoped array; superseded generations of a current destination appear in its prior_destination_refs.', max_length=64, ), + NoExplicitNull, ] = None declarations: Annotated[ principal_declarations_state.PrincipalDeclarationsState | None, Field( description='Declared consumption facts and the seller-computed accepted intersection. Present when and only when the seller supports the declarations section.' ), + NoExplicitNull, ] = None retired_destinations: Annotated[ list[RetiredDestination] | None, @@ -70,4 +73,5 @@ class PrincipalState(AdCPBaseModel): description='Destinations the caller revoked by omitting them from a submitted reporting_destinations section, retained while any generation remains resolvable for reporting history. Enumerable only by the owning principal. Reusing a retired destination_id requires fresh registration and proof and produces a new generation.', max_length=64, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/product.py b/src/adcp/types/domains/core/product.py index fadcdbbf5..b7bfb1bf2 100644 --- a/src/adcp/types/domains/core/product.py +++ b/src/adcp/types/domains/core/product.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/product.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat, ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, StrictBool, model_validator @@ -80,6 +80,7 @@ class PublisherProperty81(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -87,6 +88,7 @@ class PublisherProperty81(AdCPBaseModel): description="Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['all'], @@ -145,6 +147,7 @@ class ConversionTracking(AdCPBaseModel): description="Action sources relevant to this product (e.g. a retail media product might have 'in_store' and 'website', while a display product might only have 'website')", min_length=1, ), + NoExplicitNull, ] = None supported_targets: Annotated[ list[SupportedTarget5] | None, @@ -152,12 +155,14 @@ class ConversionTracking(AdCPBaseModel): description='Target kinds available for event goals on this product. Values match target.kind on the optimization goal. cost_per: target cost per conversion event. per_ad_spend: target return on ad spend (requires value_field on event sources). maximize_value: maximize total conversion value without a specific ratio target (requires value_field). Only these target kinds are accepted — goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes conversion count within budget — no declaration needed for that mode. When omitted, buyers can still set target-less event goals.', min_length=1, ), + NoExplicitNull, ] = None platform_managed: Annotated[ StrictBool | None, Field( description="Whether the seller provides its own always-on measurement (e.g. Amazon sales attribution for Amazon advertisers). When true, sync_event_sources response will include seller-managed event sources with managed_by='seller'." ), + NoExplicitNull, ] = None @@ -172,16 +177,19 @@ class CatalogMatch(AdCPBaseModel): Field( description="GTINs from the buyer's catalog that are eligible on this product's inventory. Standard GTIN formats (GTIN-8 through GTIN-14). Only present for product-type catalogs with GTIN matching." ), + NoExplicitNull, ] = None matched_ids: Annotated[ list[str] | None, Field( description="Item IDs from the buyer's catalog that matched this product's inventory. The ID type depends on the catalog type and content_id_type (e.g., SKUs for product catalogs, job_ids for job catalogs, offering_ids for offering catalogs)." ), + NoExplicitNull, ] = None matched_count: Annotated[ SchemaInt | None, Field(description="Number of catalog items that matched this product's inventory.", ge=0), + NoExplicitNull, ] = None submitted_count: Annotated[ SchemaInt, @@ -209,9 +217,12 @@ class MaterialSubmission(AdCPBaseModel): url: Annotated[ AnyUrl | None, Field(description='HTTPS URL for uploading or submitting physical creative materials'), + NoExplicitNull, ] = None email: Annotated[ - EmailStr | None, Field(description='Email address for creative material submission') + EmailStr | None, + Field(description='Email address for creative material submission'), + NoExplicitNull, ] = None instructions: Annotated[ str | None, @@ -219,8 +230,9 @@ class MaterialSubmission(AdCPBaseModel): description='Human-readable instructions for material submission (file naming conventions, shipping address, etc.)', max_length=2000, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class PublisherProperty82(AdCPBaseModel): @@ -254,6 +266,7 @@ class PublisherProperty83(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -261,6 +274,7 @@ class PublisherProperty83(AdCPBaseModel): description="Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['by_tag'], Field(description='Discriminator indicating selection by property tags') @@ -302,12 +316,14 @@ class MetricOptimization(AdCPBaseModel): description="Reach units this product can optimize for. Required when supported_metrics includes 'reach'. Buyers must set reach_unit to a value in this list on reach optimization goals — sellers reject unsupported values.", min_length=1, ), + NoExplicitNull, ] = None supported_view_durations: Annotated[ list[SupportedViewDuration] | None, Field( description="Video view duration thresholds (in seconds) this product supports for completed_views goals. Only relevant when supported_metrics includes 'completed_views'. When absent, the seller uses their platform default. Buyers must set view_duration_seconds to a value in this list — sellers reject unsupported values." ), + NoExplicitNull, ] = None supported_viewability_standards: Annotated[ list[viewability_standard.ViewabilityStandard] | None, @@ -315,12 +331,14 @@ class MetricOptimization(AdCPBaseModel): description="Viewability standards this product can optimize viewable_rate goals against. Only relevant when supported_metrics includes 'viewable_rate'. When absent, buyers cannot assume a specific standard is supported and sellers reject unsupported values. Buyers must set the goal's standard to a value in this list when it is present.", min_length=1, ), + NoExplicitNull, ] = None supported_targets: Annotated[ list[SupportedTarget] | None, Field( description='Target kinds available for metric goals on this product. Values match target.kind on the optimization goal. Only these target kinds are accepted — goals with unlisted target kinds will be rejected. When omitted, buyers can set target-less metric goals (maximize volume within budget) but cannot set specific targets.' ), + NoExplicitNull, ] = None @@ -337,10 +355,12 @@ class Provider(AdCPBaseModel): context_match: Annotated[ StrictBool | None, Field(description='Whether this provider handles context match for this product.'), + NoExplicitNull, ] = False identity_match: Annotated[ StrictBool | None, Field(description='Whether this provider handles identity match for this product.'), + NoExplicitNull, ] = False countries: Annotated[ list[Country] | None, @@ -348,6 +368,7 @@ class Provider(AdCPBaseModel): description="ISO 3166-1 alpha-2 country codes this provider serves for identity match. The router uses this to select the correct regional provider based on the request's country field. Required when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None uid_types: Annotated[ list[uid_type.UidType] | None, @@ -355,6 +376,7 @@ class Provider(AdCPBaseModel): description="Identity types this regional provider can resolve. The router filters providers whose uid_types includes the request's uid_type. Required when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None @@ -373,16 +395,19 @@ class TrustedMatch(AdCPBaseModel): Field( description="Whether this product supports Identity Match requests. When true, the publisher's TMP router will send identity match requests to evaluate user eligibility." ), + NoExplicitNull, ] = False response_types: Annotated[ list[response_type.TmpResponseType] | None, Field(description='What the publisher can accept back from context match.', min_length=1), + NoExplicitNull, ] = [response_type.TmpResponseType.activation] dynamic_brands: Annotated[ StrictBool | None, Field( description="Whether the buyer can select a brand at match time. When false (default), the brand must be specified on the media buy/package. When true, the buyer's offer can include any brand — the publisher applies approval rules at match time. Enables multi-brand agreements where the holding company or buyer agent selects brand based on context." ), + NoExplicitNull, ] = False providers: Annotated[ list[Provider] | None, @@ -390,6 +415,7 @@ class TrustedMatch(AdCPBaseModel): description="TMP providers integrated with this product's inventory. Each entry identifies a provider by agent_url (from the registry) and declares what match types it supports for this product. The product-level context_match and identity_match booleans declare what the product supports overall; the per-provider booleans declare which provider handles each match type. Enables buyer discovery: 'find products where a specific provider does context matching.'", min_length=1, ), + NoExplicitNull, ] = None @@ -402,13 +428,17 @@ class ProductCard(AdCPBaseModel): Field( description='Hero image for the card. Recommended ~300x400 (4:3 portrait) for the standard card layout; receivers may scale.' ), + NoExplicitNull, ] = None title: Annotated[ - str | None, Field(description='Card title (typically the product name).', max_length=60) + str | None, + Field(description='Card title (typically the product name).', max_length=60), + NoExplicitNull, ] = None description: Annotated[ str | None, Field(description='Short descriptive blurb shown below the title.', max_length=200), + NoExplicitNull, ] = None price_label: Annotated[ str | None, @@ -416,6 +446,7 @@ class ProductCard(AdCPBaseModel): description="Formatted price or pricing summary (e.g., 'From $5 CPM', 'Auction floor $0.50 CPC'). Free-text — receivers render verbatim.", max_length=30, ), + NoExplicitNull, ] = None cta_label: Annotated[ str | None, @@ -423,6 +454,7 @@ class ProductCard(AdCPBaseModel): description="Call-to-action button label (e.g., 'View details', 'Get proposal').", max_length=25, ), + NoExplicitNull, ] = None @@ -433,6 +465,7 @@ class DeliveryMeasurement(AdCPBaseModel): description="Measurement vendors used for this product, as structured `BrandRef` identities. Multiple entries when multiple vendors play different roles (e.g., the ad server plus a separate viewability vendor like IAS or DV; or a retail-media seller plus a third-party retail measurement vendor like Circana or NielsenIQ). Each vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Distinct from `performance_standards[].vendor` which carries vendor identity for *committed* metrics with thresholds — this field carries vendor identity for the overall measurement story, including non-committed-but-reported metrics.", min_length=1, ), + NoExplicitNull, ] = None provider: Annotated[ str | None, @@ -440,12 +473,14 @@ class DeliveryMeasurement(AdCPBaseModel): deprecated=True, description="**Deprecated as of this minor.** Free-form measurement provider description (e.g., 'Google Ad Manager with IAS viewability', 'Nielsen DAR', 'Geopath for DOOH impressions'). New implementations SHOULD use the structured `vendors` field instead. Retained for one-minor backwards compatibility; removed at the next major. When both `vendors` and `provider` are present, consumers MUST use `vendors` for vendor identity and treat `provider` as informational text.", ), + NoExplicitNull, ] = None notes: Annotated[ str | None, Field( description="Additional details about measurement methodology in plain language (e.g., 'MRC-accredited viewability. 50% in-view for 1s display / 2s video', 'Panel-based demographic measurement updated monthly'). Free-form prose for context that doesn't fit the structured `vendors` field." ), + NoExplicitNull, ] = None @@ -456,35 +491,42 @@ class ProductCardDetailed(AdCPBaseModel): hero_image: Annotated[ image_asset.ImageAsset | None, Field(description='Primary hero image at the top of the detailed view.'), + NoExplicitNull, ] = None carousel_images: Annotated[ list[image_asset.ImageAsset] | None, Field(description='Additional images for a swipeable carousel below the hero.'), + NoExplicitNull, + ] = None + title: Annotated[ + str | None, Field(description='Page title (typically the product name).'), NoExplicitNull ] = None - title: Annotated[str | None, Field(description='Page title (typically the product name).')] = ( - None - ) description: Annotated[ str | None, Field( description='Full descriptive copy. Markdown allowed in client renderers that support it; otherwise treat as plain text.' ), + NoExplicitNull, ] = None specifications: Annotated[ list[Specification] | None, Field( description="Structured key/value specifications (e.g., 'Aspect ratio: 9:16', 'Duration: 30s'). Each item is a labeled fact about the product." ), + NoExplicitNull, + ] = None + price_label: Annotated[ + str | None, Field(description='Formatted price or pricing summary.'), NoExplicitNull + ] = None + cta_label: Annotated[ + str | None, Field(description='Call-to-action button label.'), NoExplicitNull ] = None - price_label: Annotated[str | None, Field(description='Formatted price or pricing summary.')] = ( - None - ) - cta_label: Annotated[str | None, Field(description='Call-to-action button label.')] = None reference_assets: Annotated[ list[product_card_reference_asset.ProductCardReferenceAsset] | None, Field( description='Typed seller collateral for buyer planning — coverage maps, sample renders, environment photos, media kits. Distinct from hero_image/carousel_images, which are display-oriented.' ), + NoExplicitNull, ] = None @@ -504,12 +546,14 @@ class AudienceActivation(AdCPBaseModel): Field( description="The seller's preferred path when the buyer supports several. MUST also appear in methods." ), + NoExplicitNull, ] = None notes: Annotated[ str | None, Field( description='Free-text caveats (onboarding lead times, data-format constraints, regional restrictions).' ), + NoExplicitNull, ] = None @@ -557,6 +601,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: Field( description="Advertising channels this product is sold as. Products inherit from their properties' supported_channels but may narrow the scope. For example, a product covering YouTube properties might be sold as ['ctv'] even though those properties support ['olv', 'social', 'ctv']." ), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -564,6 +609,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format compatibility path. Products MUST carry `format_ids`, `format_options`, or both during the 3.x migration window. New products MUST author canonical `format_options[]`; sellers MAY additionally project those declarations to `format_ids` for legacy buyers. When both fields are present they MUST describe the same underlying formats, and buyers MUST prefer `format_options`. Do not author a new product from `format_ids` alone.', ), + NoExplicitNull, ] = None format_options: Annotated[ list[product_format_declaration.ProductFormatDeclaration] | None, @@ -571,6 +617,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description="Canonical format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, platform_extensions, and optional locale_policy. New 3.2 products MUST carry format_options; a seller MAY additionally project the same declarations to deprecated format_ids for older 3.x peers. A declaration carrying locale_policy is canonical-only because legacy format_ids cannot preserve locale eligibility; no product or placement format_id may project to an effective locale-constrained option.\n\nWhen placements are published, product-level format_options are the union of formats deliverable somewhere in the product and the upper bound for every placement. A placement's effective accepted set is the intersection of every applicable layer: (1) the product format_options; (2) the product placement's inline format_options, when present; and (3) for kind publisher_ref, the named publisher's adagents.json catalog narrowing. Resolve layer 3 by locating the matching placements[] entry: use its format_options when present, resolving bare format_option_id references against that same file's top-level formats[]; otherwise use top-level formats[] applicable to the placement's property_ids/property_tags. An omitted optional layer is unconstrained, but an unresolved publisher placement or format-option reference MUST fail closed. A publisher-referenced placement without inline product format_options therefore does NOT inherit the full product union when the publisher catalog supplies narrower placement or property-scoped acceptance.\n\nMatch publisher-declared options by {publisher_domain, format_option_id}, match product-local options by format_option_id when publisher_domain is omitted, and otherwise match declarations with the same format_kind whose narrower parameters satisfy the broader declaration. A product- or placement-level declaration MUST NOT introduce a format outside the product upper bound. Locale policy follows the same intersection. If the product locale policy is absent, a placement may introduce any concrete policy as a narrowing of an unconstrained option; when both are present, every placement range must be contained by a product range under RFC 4647 Basic Filtering. Locale eligibility is checked independently for every placement where an assignment may serve.\n\nFor a product or package containing multiple included placements, a single creative intended for every placement MUST lie in the intersection of every selected placement's effective set. Distinct per-placement creatives MAY use the union, but the selected creative set MUST cover every included placement; uncovered inventory MUST be rejected or refined, never silently omitted. If a product spans multiple publishers but omits placements[], there is no public routing key for per-placement creatives: its format_options MUST therefore be the common intersection accepted across every selected publisher/property scope. A seller that needs a union of publisher-specific formats MUST publish placements[] with publisher-scoped identities and narrowing. Commercial terms such as price, floor, availability, and deal eligibility are product facts, not format parameters.", min_length=1, ), + NoExplicitNull, ] = None placements: Annotated[ list[placement.Placement] | None, @@ -578,6 +625,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description="Optional array of specific public placements within this product. Product placements declare `kind` to distinguish publisher-catalog placements (`publisher_ref`) from sales-agent-defined placements (`seller_inline`). Publisher references use canonical `{publisher_domain, placement_id}` identity and may omit name because adagents.json resolves it. New seller-inline placements SHOULD carry `seller_agent`; legacy rows without it remain scoped to the enclosing seller and product. A seller-inline publisher_domain is inventory attribution, not authority to mint an ID in that publisher's catalog namespace. Each placement MUST declare mode: targetable or included. Creative assignments route creatives only after placement inventory is purchased.", min_length=1, ), + NoExplicitNull, ] = None video_placement_types: Annotated[ list[video_placement_type.VideoPlacementType] | None, @@ -585,6 +633,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Declared video placement types that may be included in this product, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Use on OLV, CTV, and other video products when buyers need to distinguish instream, accompanying-content, interstitial, and standalone/no-content inventory. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `video_placement_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None audio_distribution_types: Annotated[ list[audio_distribution_type.AudioDistributionType] | None, @@ -592,6 +641,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Declared audio distribution types that may be included in this product, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Use on radio, streaming-audio, podcast, gaming, and other audio products when buyers need to distinguish music streaming services, FM/AM broadcast, podcasts, catch-up radio, web radio, video-game audio, and text-to-speech inventory without changing the buyer-facing channel or adagents.json property type. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `audio_distribution_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ list[sponsored_placement_type.SponsoredPlacementType] | None, @@ -599,6 +649,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Declared sponsored-placement types that may be included in this product, distinguishing where catalog-driven retail-media placements render on the retailer surface (sponsored search, sponsored display, or sponsored native). Use on retail-media products when buyers need to distinguish search-keyed, display, and native in-grid sponsored inventory. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `sponsored_placement_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ list[social_placement_surface.SocialPlacementSurface] | None, @@ -606,6 +657,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Declared social-placement surfaces that may be included in this product, distinguishing the in-app surface where social placements render (feed, stories, short_video, explore, or search). Use on social products when buyers need to distinguish feed, story, short-video, and discovery surfaces. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `social_placement_surfaces`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.', min_length=1, ), + NoExplicitNull, ] = None delivery_type: delivery_type_1.DeliveryType exclusivity: Annotated[ @@ -613,6 +665,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: Field( description="Whether this product offers exclusive access to its inventory. Defaults to 'none' when absent. Most relevant for guaranteed products tied to specific collections or placements." ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[pricing_option.PricingOption], @@ -626,6 +679,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: Field( description="Forecasted delivery metrics for this product. Concrete discovery targeting scopes the forecast to those effective values. When discovery requested only required_overlay_support for a dimension, the forecast describes the product's discovery/default scope and is not a value-specific forecast for every later selection; buyers rediscover with concrete targeting_overlay values when they need that forecast." ), + NoExplicitNull, ] = None outcome_measurement: Annotated[ outcome_measurement_1.OutcomeMeasurement | None, @@ -633,18 +687,21 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: deprecated=True, description='**Deprecated as of this minor.** Outcome capabilities (incremental sales lift, brand lift, foot traffic, etc.) are now declared via `reporting_capabilities.available_metrics` (the same path used for impressions, conversions, ROAS) with `qualifier.attribution_methodology` and `qualifier.attribution_window` carrying the methodology and window on commit. New implementations SHOULD use the unified pattern; this field is retained for one-minor backwards compatibility and removed at the next major. See `outcome-measurement.json` description for migration guidance.', ), + NoExplicitNull, ] = None delivery_measurement: Annotated[ DeliveryMeasurement | None, Field( description='Measurement vendors and methodology for delivery metrics. The buyer accepts the declared vendors as the source of truth for the buy. When absent, buyers should apply their own measurement defaults. Senders SHOULD populate `vendors` (structured BrandRef array) for new implementations; the legacy `provider` string field is deprecated and retained for one-minor backwards compatibility.' ), + NoExplicitNull, ] = None measurement_terms: Annotated[ measurement_terms_1.MeasurementTerms | None, Field( description="Seller's default billing measurement and makegood terms. Declares who counts the billing metric and what remedies apply when thresholds are breached. Buyers may propose different terms at media buy creation — sellers accept, reject (TERMS_REJECTED), or adjust per their policy." ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[performance_standard.PerformanceStandard] | None, @@ -652,12 +709,14 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description="Seller's default performance standards for this product: viewability, IVT, completion rate, brand safety, attention score. Buyers may propose different standards at media buy creation. When absent, no structured performance standards apply.", min_length=1, ), + NoExplicitNull, ] = None cancellation_policy: Annotated[ cancellation_policy_1.CancellationPolicy | None, Field( description='Cancellation terms for this product. Declares the minimum notice period required before cancellation takes effect and any penalties for insufficient notice. Relevant for guaranteed delivery products. Buyers accept these terms by creating a media buy against the product.' ), + NoExplicitNull, ] = None allowed_actions: Annotated[ list[product_allowed_action.ProductAllowedAction] | None, @@ -665,20 +724,23 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Actions buyers may perform on buys created against this product, scoped to statuses and modes. Advisory template — the authoritative per-buy capability is `available_actions[]` on the buy response, which resolves modes against current buy state, account tier, and negotiated terms. Buyers SHOULD use this for pre-flight product selection ("which products let me self-serve cancel within 72hr?") and read `available_actions[]` for runtime decisions. The array is uniquely keyed by `action` — sellers MUST NOT emit two entries with the same `action` value. Absence means the seller has not declared a structured action surface for this product — buyers fall back to `valid_actions[]` on buy responses for the flat string vocabulary.', min_length=1, ), + NoExplicitNull, ] = None reporting_capabilities: reporting_capabilities_1.ReportingCapabilities - creative_policy: creative_policy_1.CreativePolicy | None = None + creative_policy: Annotated[creative_policy_1.CreativePolicy | None, NoExplicitNull] = None is_custom: Annotated[ StrictBool | None, Field( description='Whether this product is a request-specific configured offer rather than a reusable baseline product. Sellers MUST set true when targeting, disclosed resolution, pricing, forecast assumptions, inventory, or terms are bound for a particular discovery/refinement lineage. Products issued through targeting-aware discovery include expires_at even when exact acceptance omits targeting_resolution. For backward compatibility, is_custom alone does not make expires_at schema-required.' ), + NoExplicitNull, ] = None property_targeting_allowed: Annotated[ StrictBool | None, Field( description="Whether buyers can select a subset of this product's publisher_properties through targeting_overlay.property_list. When false, the product is fixed inventory: it matches requested property targeting only when its inherent property set already satisfies the request, or when a configured product discloses additional inventory through targeting_resolution." ), + NoExplicitNull, ] = False data_provider_signals: Annotated[ list[data_provider_signal_selector.DataProviderSignalSelector] | None, @@ -686,6 +748,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: deprecated=True, description='Deprecated. Legacy/non-selectable metadata for data-provider signals already bundled into or associated with this product. This field does not provide buyer-selectable options, prices, or seller activation handles. Use included_signals for non-selectable product signal metadata, or signal_targeting_options for selectable package-level signal groups.', ), + NoExplicitNull, ] = None included_signals: Annotated[ list[signal_listing.SignalListing] | None, @@ -693,6 +756,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description="Non-selectable signal metadata for signals already included in, bundled with, or planned into this product. These signals describe what the product is; buyers do not select them in packages[].targeting_overlay.signal_targeting_groups and this field does not imply package-level signal targeting. Use signal_ref scope 'data_provider' or 'signal_source' to reference externally defined signals without redefining their name or value_type. Use signal_ref scope 'product' with name and value_type when the included signal is defined only by this product.", min_length=1, ), + NoExplicitNull, ] = None signal_targeting_options: Annotated[ list[product_signal_targeting_option.ProductSignalTargetingOption] | None, @@ -700,50 +764,58 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description="Inline seller-offered signals that may be applied to packages for this product at create_media_buy time. Each entry references a named signal definition with signal_ref scope 'product' for a product-local signal option, scope 'data_provider' for an external signal definition published in adagents.json signals[] that the seller is authorized to apply, or scope 'signal_source' for a source-native signal. Product-local options define name and value_type inline; data-provider and signal-source options may omit those fields when the referenced definition or source is authoritative. Use this field when the selectable menu is product-specific, has product-specific pricing or activation handles, is the relevant subset for a brief/refine result, or should be rendered without an additional get_signals call. Wholesale products may omit this field and rely on get_signals for the selectable signal feed. Buyers select eligible signals through packages[].targeting_overlay.signal_targeting_groups when signal_targeting_rules allow; fixed/default entries are applied by the seller and echoed on the package state. Sellers MUST set signal_targeting_allowed to true whenever this field is present. Bundled, non-selectable signal metadata belongs in included_signals; legacy data_provider_signals may appear only for backwards compatibility.", min_length=1, ), + NoExplicitNull, ] = None signal_targeting_rules: Annotated[ signal_targeting_rules_1.SignalTargetingRules | None, Field( description='Composition rules for selecting signals on this product. The selectable signal menu may come from inline signal_targeting_options or from get_signals when a wholesale product omits inline options. This is product-scoped because products may be backed by different ad servers with different Boolean targeting support and group limits.' ), + NoExplicitNull, ] = None signal_targeting_allowed: Annotated[ StrictBool | None, Field( description='Whether this product has a package-level signal_targeting_groups surface. When false (default), signals are bundled into the product terms and cannot be selected or explicitly echoed as package signal groups. When true, eligible signals from inline signal_targeting_options or from get_signals may be buyer-selected or seller-applied according to signal_targeting_rules and are represented through packages[].targeting_overlay.signal_targeting_groups. Editability is controlled by signal_targeting_rules; fixed/default-only products still set this to true when applied signal groups are echoed.' ), + NoExplicitNull, ] = False demographic_targeting: Annotated[ demographic_targeting_capability.DemographicTargetingCapability | None, Field( description='Exact demographic execution available for this product. Buyers MUST use this product-scoped declaration, not the seller-wide get_adcp_capabilities rollup, to preflight a demographic predicate.' ), + NoExplicitNull, ] = None overlay_support: Annotated[ targeting_overlay_support.TargetingOverlaySupport | None, Field( description='Binding product-scoped targeting dimensions the buyer may set independently on packages after discovery. Presence guarantees selectable capability subject to disclosed limits, not inventory or a forecast for every possible value. Targeting satisfied only through inherent product scope does not appear here. Returned products MUST cover every field requested through get_products.required_overlay_support. A later supported selection with no available inventory returns PRODUCT_UNAVAILABLE on create; an update outside the original priced envelope may return REQUOTE_REQUIRED.' ), + NoExplicitNull, ] = None media_buy_support: Annotated[ media_buy_support_1.ProductMediaBuySupport | None, Field( description='Binding product participation in shared MediaBuy-level controls. This is separate from overlay_support because a root frequency cap aggregates exposures across packages rather than targeting one package. Returned products MUST cover every field requested through required_media_buy_support.' ), + NoExplicitNull, ] = None - identity: product_identity.ProductIdentity | None = None + identity: Annotated[product_identity.ProductIdentity | None, NoExplicitNull] = None execution_requirements: Annotated[ list[product_execution_requirement.ProductExecutionRequirement] | None, Field( description="Experimental (`media_buy.execution_requirements`). Account resources a package on this product needs before `create_media_buy` succeeds. Every entry is required. Account-independent: it does not change `cache_scope` and carries no account resource IDs or names. When present, it plus the `required_connections` of the package's selected format declarations is complete for the kinds in `product-execution-requirement.json`: a seller MUST NOT reject a package that satisfies all of them for lacking an undeclared kind. Absence means undeclared. A declaring seller MUST reject an unmet `event_source` or `catalog` entry on a buyer-supplied `create_media_buy` `packages[]` or `update_media_buy` `new_packages[]` entry, or an update that removes a satisfying binding, with `VALIDATION_ERROR`, `error.field` at the binding, and `error.details` per `error-details/execution-requirement-unmet.json`.", min_length=1, ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ product_targeting_resolution.ProductTargetingResolution | None, Field( description='Discovery-time targeting resolution bound to this configured product. modifications sparsely disclose product-specific differences from get_products.targeting_overlay. Request-level brief interpretation is returned once on GetProductsResponse.targeting_resolution. Exact structured overlay values are not repeated. Selecting product_id accepts the disclosed modifications; product forecast and pricing MUST reflect them.' ), + NoExplicitNull, ] = None audience_evidence: Annotated[ list[audience_evidence_1.AudienceEvidence] | None, @@ -751,6 +823,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Immutable population-level evidence explaining why this inventory may suit an audience. This supports discovery, comparison, and planning only. It does not imply exact demographic targeting, user-level signal membership, or legal-age verification. Sellers MUST publish each distinct snapshot with a new snapshot_id and content_digest.', min_length=1, ), + NoExplicitNull, ] = None audience_evidence_selections: Annotated[ list[audience_evidence_selection.AudienceEvidenceSelection] | None, @@ -758,6 +831,7 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Exact evidence snapshots that satisfied required eligibility or affected seller ranking for this get_products result. When audience_evidence_requirements was supplied and evidence influenced inclusion or rank, sellers MUST return the relevant selections; an absent-evidence match under evidence_presence when_available has no selection. Product selections use decision_use recommendation or eligibility.', min_length=1, ), + NoExplicitNull, ] = None catalog_types: Annotated[ list[catalog_type.CatalogType] | None, @@ -765,18 +839,21 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Catalog types this product supports for catalog-driven campaigns. A sponsored product listing declares ["product"], a job board declares ["job", "offering"]. Buyers match synced catalogs to products via this field.', min_length=1, ), + NoExplicitNull, ] = None metric_optimization: Annotated[ MetricOptimization | None, Field( description="Metric optimization capabilities for this product. Presence indicates the product supports optimization_goals with kind: 'metric'. No event source or conversion tracking setup required — the seller tracks these metrics natively." ), + NoExplicitNull, ] = None vendor_metric_optimization: Annotated[ vendor_metric_optimization_1.VendorMetricOptimization | None, Field( description="Vendor-attested metric optimization capabilities for this product. Presence indicates the product supports `optimization_goals` with `kind: 'vendor_metric'` — the seller's bidding stack can steer delivery toward a specific vendor's measurement (e.g., DV/IAS/Adelaide attention, Scope3 emissions, Kantar brand lift, retail-media partner metrics). Distinct from `metric_optimization` (seller-native metrics with no vendor binding) and from `reporting_capabilities.vendor_metrics` (which declares what the product can *report* rather than what it can *optimize against*). A product may report a vendor metric without being able to optimize for it. Buyers MUST verify the goal's `(vendor, metric_id)` is in `supported_metrics` AND that the package's `committed_metrics[]` includes a matching `{ scope: 'vendor', vendor, metric_id }` entry — optimization without committed reporting is unverifiable and is rejected at the wire level." ), + NoExplicitNull, ] = None max_optimization_goals: Annotated[ SchemaInt | None, @@ -784,48 +861,56 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Maximum number of optimization_goals this product accepts on a package. When absent, no limit is declared. Most social platforms accept only 1 goal — buyers sending arrays longer than this value should expect the seller to use only the highest-priority (lowest priority number) goal.', ge=1, ), + NoExplicitNull, ] = None measurement_readiness: Annotated[ measurement_readiness_1.MeasurementReadiness | None, Field( description="Assessment of whether the buyer's event source setup is sufficient for this product to optimize effectively. Only present when the seller can evaluate the buyer's account context. Buyers should check this before creating media buys with event-based optimization goals." ), + NoExplicitNull, ] = None conversion_tracking: Annotated[ ConversionTracking | None, Field( description="Conversion event tracking for this product. Presence indicates the product supports optimization_goals with kind: 'event'. Seller-level capabilities (supported event types, UID types, attribution windows) are declared in get_adcp_capabilities." ), + NoExplicitNull, ] = None catalog_match: Annotated[ CatalogMatch | None, Field( description='When the buyer provides a catalog on get_products, indicates which catalog items are eligible for this product. Only present for products where catalog matching is relevant (e.g., sponsored product listings, job boards, hotel ads).' ), + NoExplicitNull, ] = None brief_relevance: Annotated[ str | None, Field( description='Explanation of why this product matches the brief (only included when brief is provided)' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='Expiration timestamp. Required for request-specific configured products whose targeting resolution, price, forecast, inventory, or terms are time-bound. After this time, a seller that still recognizes the issued configured ID within the authenticated account and referenced discovery/refinement lineage rejects create_media_buy with PRODUCT_EXPIRED and the buyer re-runs get_products. Once the seller no longer retains an expiry tombstone, or whenever the ID belongs to another account or lineage, PRODUCT_NOT_FOUND applies instead; sellers are not required to retain tombstones indefinitely and MUST NOT disclose cross-tenant existence through error choice.' ), + NoExplicitNull, ] = None product_card: Annotated[ ProductCard | None, Field( description='Optional standard visual card for displaying this product in user interfaces (catalog browsers, dashboards, agent UIs). Distinct from `format` — product_card describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection. Receivers render the card directly from these fields.' ), + NoExplicitNull, ] = None product_card_detailed: Annotated[ ProductCardDetailed | None, Field( description='Optional detailed card with hero + carousel + structured specifications, for rich product presentation (media-kit-style pages, full product detail views). Distinct from `format` — describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection.' ), + NoExplicitNull, ] = None collections: Annotated[ list[collection_selector.CollectionSelector] | None, @@ -833,12 +918,14 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Collections available in this product. Each entry references collections declared in an adagents.json by domain and collection ID. Buyers resolve full collection objects from the referenced adagents.json. Product selectors must name explicit collection_ids — the domain-only bulk-grant selector form is for authorization scoping, not product composition.', min_length=1, ), + NoExplicitNull, ] = None collection_targeting_allowed: Annotated[ StrictBool | None, Field( description="Whether buyers can select a subset of this product's collections through targeting_overlay.collection_list or targeting_overlay.collection_selection. When false, the product is a fixed bundle (a collection_selection that exactly restates the complete bundle remains an inherent match); when true, collection selection is a product-scoped overlay capability." ), + NoExplicitNull, ] = False list_applications: Annotated[ list[inventory_list_application.InventoryListApplication] | None, @@ -846,41 +933,47 @@ def _coerce_publisher_property_models(cls, data: Any) -> Any: description='Product-scoped receipts for every effective property- or collection-list targeting reference. Sellers MUST return one receipt per application regardless of response field projection; exclusion applications receive a receipt even when summary.matched is zero, while zero matches for any inclusion application make the product ineligible and it is not returned. Each receipt uses the same pre-list product inventory baseline; pricing and forecast reflect inventory remaining after all effective lists are composed.', min_length=1, ), + NoExplicitNull, ] = None installments: Annotated[ list[installment.Installment] | None, Field( description='Specific installments included in this product. Each installment references its parent through canonical collection_ref when the product spans multiple collections or publisher namespaces; collection_id remains a deprecated single-namespace shorthand. When absent with collections present, the product covers the collections broadly (run-of-collection).' ), + NoExplicitNull, ] = None enforced_policies: Annotated[ list[str] | None, Field( description='Registry policy IDs the seller enforces for this product. Enforcement level comes from the policy registry. Buyers can filter products by required policies.' ), + NoExplicitNull, + ] = None + acceptance_policy_profile_ids: Annotated[ + acceptance_policy_profile_ids_1.AcceptancePolicyProfileIds | None, NoExplicitNull ] = None - acceptance_policy_profile_ids: ( - acceptance_policy_profile_ids_1.AcceptancePolicyProfileIds | None - ) = None trusted_match: Annotated[ TrustedMatch | None, Field( description='Trusted Match Protocol capabilities for this product. When present, the product supports real-time contextual and/or identity matching via TMP. Buyers use this to determine what response types the publisher can accept and whether brands can be selected dynamically at match time.' ), + NoExplicitNull, ] = None audience_activation: Annotated[ AudienceActivation | None, Field( description="How buyer audience data can reach this product's targeting. Absence means undeclared — buyers SHOULD treat it as needs-clarification rather than non-support, except under an audience_activation_methods filter, where sellers MUST exclude undeclared products. Declare when the product accepts buyer audiences. Experimental: sellers declaring this MUST list media_buy.audience_activation in experimental_features on get_adcp_capabilities." ), + NoExplicitNull, ] = None material_submission: Annotated[ MaterialSubmission | None, Field( description="Instructions for submitting physical creative materials (print, static OOH, cinema). Present only for products requiring physical delivery outside the digital creative assignment flow. Buyer agents MUST validate url and email domains against the seller's known domains (from adagents.json) before submitting materials. Never auto-submit without human confirmation." ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> Product: diff --git a/src/adcp/types/domains/core/product_allocation.py b/src/adcp/types/domains/core/product_allocation.py index 92e36e431..1fe2e9dfc 100644 --- a/src/adcp/types/domains/core/product_allocation.py +++ b/src/adcp/types/domains/core/product_allocation.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/product_allocation.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import pacing as pacing_1 @@ -28,6 +28,7 @@ class ProductAllocation(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None min_spend_target_percentage: Annotated[ StrictFloat | None, @@ -36,6 +37,7 @@ class ProductAllocation(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None max_spend_percentage: Annotated[ StrictFloat | None, @@ -44,44 +46,52 @@ class ProductAllocation(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Recommended subordinate package pacing. On proposal execution this becomes pacing on the derived package and MUST NOT cause delivery to exceed aggregate proposal/media-buy pacing. On a committed proposal it is a firm delivery term. On a seller-optimized proposal, sellers MUST NOT emit it unless they advertise media_buy.features.seller_optimized_package_pacing.' ), + NoExplicitNull, ] = None pricing_option_id: Annotated[ str | None, Field( description="Selected pricing option ID from the product's pricing_options array. Required when the containing proposal is committed so create_media_buy executes the exact disclosed commercial terms; optional on legacy draft proposals." ), + NoExplicitNull, ] = None rationale: Annotated[ str | None, Field(description='Explanation of why this product and allocation are recommended'), + NoExplicitNull, ] = None sequence: Annotated[ SchemaInt | None, Field(description='Optional ordering hint for multi-line-item plans (1-based)', ge=1), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, Field( description="Categorical tags for this allocation (e.g., 'desktop', 'german', 'mobile') - useful for grouping/filtering allocations by dimension" ), + NoExplicitNull, ] = None start_time: Annotated[ AwareDatetime | None, Field( description='Recommended flight start date/time for this allocation in ISO 8601 format. Allows publishers to propose per-flight scheduling within a proposal. When omitted, the allocation applies to the full campaign date range.' ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description='Recommended flight end date/time for this allocation in ISO 8601 format. Allows publishers to propose per-flight scheduling within a proposal. When omitted, the allocation applies to the full campaign date range.' ), + NoExplicitNull, ] = None daypart_targets: Annotated[ list[daypart_target.DaypartTarget] | None, @@ -89,9 +99,11 @@ class ProductAllocation(AdCPBaseModel): description="Recommended time windows for this allocation in spot-plan proposals. Each entry's timezone defaults to inventory_local when omitted, and entries MAY use different clocks.", min_length=1, ), + NoExplicitNull, ] = None forecast: Annotated[ delivery_forecast.DeliveryForecast | None, Field(description='Forecasted delivery metrics for this allocation'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/product_allowed_action.py b/src/adcp/types/domains/core/product_allowed_action.py index 3f1c2c29c..5e2e79e00 100644 --- a/src/adcp/types/domains/core/product_allowed_action.py +++ b/src/adcp/types/domains/core/product_allowed_action.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/product_allowed_action.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import media_buy_action_mode, media_buy_status @@ -37,22 +37,26 @@ class ProductAllowedAction(AdCPBaseModel): description='Media buy statuses in which this action is permitted. When absent, the action is permitted in all non-terminal statuses (`pending_creatives`, `pending_start`, `active`, `paused`).', min_length=1, ), + NoExplicitNull, ] = None sla: Annotated[ sla_window.SlaWindow | None, Field( description='Optional SLA commitment for this action on this product. Absence means no commitment.' ), + NoExplicitNull, ] = None constraints: Annotated[ change_term_constraints.MediaBuyChangeTermConstraints | None, Field( description='Optional advisory machine-readable bounds buyers can use during product selection. The proposal must restate any binding bounds in commercial_terms.change_terms[].constraints.' ), + NoExplicitNull, ] = None terms_ref: Annotated[ str | None, Field( description='Optional advisory pointer to published commercial terms governing this product action. It is not a proposal change-term identity and never grants a binding change right; a proposal materializes binding rights under commercial_terms.change_terms[].term_id.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/product_audience_evidence_requirements.py b/src/adcp/types/domains/core/product_audience_evidence_requirements.py index d3421157a..4687cb3e1 100644 --- a/src/adcp/types/domains/core/product_audience_evidence_requirements.py +++ b/src/adcp/types/domains/core/product_audience_evidence_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/product_audience_evidence_requirements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat from ..enums import audience_evidence_methodology, audience_resolution_method, audience_subject_type @@ -45,6 +45,7 @@ class MaximumAge(Duration): Field( description="Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." ), + NoExplicitNull, ] = None @@ -54,7 +55,7 @@ class AcceptedAttestationIssuers1(AdCPBaseModel): ) type: Literal['brand'] = 'brand' brand: brand_key.BrandKey - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AcceptedAttestationIssuers2(AdCPBaseModel): @@ -63,7 +64,7 @@ class AcceptedAttestationIssuers2(AdCPBaseModel): ) type: Literal['agent'] = 'agent' agent_url: AnyUrl - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AcceptedAttestationIssuers3(AdCPBaseModel): @@ -72,7 +73,7 @@ class AcceptedAttestationIssuers3(AdCPBaseModel): ) type: Literal['origin'] = 'origin' origin: AnyUrl - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None AcceptedAttestationIssuers = Annotated[ @@ -88,28 +89,40 @@ class ProductAudienceEvidenceRequirements(AdCPBaseModel): requirement_mode: RequirementMode evidence_presence: EvidencePresence accepted_methodologies: Annotated[ - list[audience_evidence_methodology.AudienceEvidenceMethodology] | None, Field(min_length=1) + list[audience_evidence_methodology.AudienceEvidenceMethodology] | None, + Field(min_length=1), + NoExplicitNull, ] = None excluded_methodologies: Annotated[ - list[audience_evidence_methodology.AudienceEvidenceMethodology] | None, Field(min_length=1) + list[audience_evidence_methodology.AudienceEvidenceMethodology] | None, + Field(min_length=1), + NoExplicitNull, + ] = None + accepted_evidence_types: Annotated[ + list[AcceptedEvidenceType] | None, Field(min_length=1), NoExplicitNull + ] = None + accepted_providers: Annotated[ + list[brand_key.BrandKey] | None, Field(min_length=1), NoExplicitNull + ] = None + excluded_providers: Annotated[ + list[brand_key.BrandKey] | None, Field(min_length=1), NoExplicitNull ] = None - accepted_evidence_types: Annotated[list[AcceptedEvidenceType] | None, Field(min_length=1)] = ( - None - ) - accepted_providers: Annotated[list[brand_key.BrandKey] | None, Field(min_length=1)] = None - excluded_providers: Annotated[list[brand_key.BrandKey] | None, Field(min_length=1)] = None accepted_subject_types: Annotated[ - list[audience_subject_type.AudienceSubjectType] | None, Field(min_length=1) + list[audience_subject_type.AudienceSubjectType] | None, Field(min_length=1), NoExplicitNull ] = None accepted_resolution_methods: Annotated[ - list[audience_resolution_method.AudienceResolutionMethod] | None, Field(min_length=1) + list[audience_resolution_method.AudienceResolutionMethod] | None, + Field(min_length=1), + NoExplicitNull, ] = None - minimum_confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0)] = None - maximum_age: MaximumAge | None = None - methodology_documentation_required: StrictBool | None = False - independent_attestation_required: StrictBool | None = False + minimum_confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0), NoExplicitNull] = None + maximum_age: Annotated[MaximumAge | None, NoExplicitNull] = None + methodology_documentation_required: Annotated[StrictBool | None, NoExplicitNull] = False + independent_attestation_required: Annotated[StrictBool | None, NoExplicitNull] = False accepted_attestation_issuers: Annotated[ - list[AcceptedAttestationIssuers] | None, Field(min_length=1) + list[AcceptedAttestationIssuers] | None, Field(min_length=1), NoExplicitNull + ] = None + accepted_attestation_claim_types: Annotated[ + list[AnyUrl] | None, Field(min_length=1), NoExplicitNull ] = None - accepted_attestation_claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/product_card_reference_asset.py b/src/adcp/types/domains/core/product_card_reference_asset.py index b3586e47e..2249e8301 100644 --- a/src/adcp/types/domains/core/product_card_reference_asset.py +++ b/src/adcp/types/domains/core/product_card_reference_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/product_card_reference_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from .assets import image_asset, markdown_asset, url_asset, video_asset @@ -37,6 +37,7 @@ class ProductCardReferenceAsset(AdCPBaseModel): Field( description="Human-readable label for the asset role. Required when role is 'other'; optional otherwise." ), + NoExplicitNull, ] = None asset: Annotated[ image_asset.ImageAsset @@ -48,5 +49,7 @@ class ProductCardReferenceAsset(AdCPBaseModel): ), ] description: Annotated[ - str | None, Field(description='Optional human-readable context about this asset.') + str | None, + Field(description='Optional human-readable context about this asset.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/product_execution_requirement.py b/src/adcp/types/domains/core/product_execution_requirement.py index 71063d91a..db7c16bcc 100644 --- a/src/adcp/types/domains/core/product_execution_requirement.py +++ b/src/adcp/types/domains/core/product_execution_requirement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/product_execution_requirement.json -# timestamp: 2026-10-04T00:53:27+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -27,8 +27,9 @@ class ProductExecutionRequirement1(AdCPBaseModel): description='Event types that satisfy the requirement (any-of): the satisfying `event_sources[]` entry MUST use one of these `event_type` values. Omit when any event type satisfies it.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ProductExecutionRequirement2(AdCPBaseModel): @@ -43,7 +44,7 @@ class ProductExecutionRequirement2(AdCPBaseModel): min_length=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Status(StrEnum): @@ -76,6 +77,7 @@ class Connection(DownstreamConnectionRequirement): Field( description='Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.' ), + NoExplicitNull, ] = None @@ -90,7 +92,7 @@ class ProductExecutionRequirement3(AdCPBaseModel): description='The required connection. Account-independent: `status` is omitted or `unknown`; `resource_ref`, `connection_id`, and `expires_at` MUST be omitted; `required_for`, when present, includes `create_media_buy`.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None ProductExecutionRequirement = Annotated[ diff --git a/src/adcp/types/domains/core/product_filters.py b/src/adcp/types/domains/core/product_filters.py index b10a9860f..b96781d40 100644 --- a/src/adcp/types/domains/core/product_filters.py +++ b/src/adcp/types/domains/core/product_filters.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product_filters.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat @@ -48,8 +48,12 @@ class BudgetRange(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - min: Annotated[StrictFloat | None, Field(description='Minimum budget amount', ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(description='Maximum budget amount', ge=0.0)] = None + min: Annotated[ + StrictFloat | None, Field(description='Minimum budget amount', ge=0.0), NoExplicitNull + ] = None + max: Annotated[ + StrictFloat | None, Field(description='Maximum budget amount', ge=0.0), NoExplicitNull + ] = None currency: Annotated[ str, Field( @@ -88,10 +92,12 @@ class Provider(AdCPBaseModel): context_match: Annotated[ StrictBool | None, Field(description='When true, require this provider to support context match.'), + NoExplicitNull, ] = None identity_match: Annotated[ StrictBool | None, Field(description='When true, require this provider to support identity match.'), + NoExplicitNull, ] = None @@ -119,6 +125,7 @@ class AudienceActivationMethods1(AdCPBaseModel): Field( description='Require this buyer agent on tmp_identity_match entries. Buyers typically filter on their own agent_url.' ), + NoExplicitNull, ] = None @@ -141,12 +148,14 @@ class RequiredGeoTargetingItem(AdCPBaseModel): description='ISO 3166-1 alpha-2 country code. Required for native postal_area system filters; not applicable to country, region, or metro filters.', pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None system: Annotated[ str | None, Field( description="Optional classification system within the level. Use for a specific metro system (e.g., 'nielsen_dma'), native postal_area system (e.g., 'zip' with country 'US'), or deprecated legacy postal alias (e.g., 'us_zip'). Not applicable for country/region which use ISO standards." ), + NoExplicitNull, ] = None @@ -161,6 +170,7 @@ class SignalTargetingItem4(AdCPBaseModel): Field( description="Desired use of this signal on the product. 'include' requires the product option to allow use in an any group. 'exclude' requires the product option to allow use in a none group." ), + NoExplicitNull, ] = TargetingMode.include @@ -190,7 +200,9 @@ class Keyword(AdCPBaseModel): extra='forbid', ) keyword: Annotated[str, Field(description='The keyword to target', min_length=1)] - match_type: match_type_1.MatchType | None = match_type_1.MatchType.broad + match_type: Annotated[match_type_1.MatchType | None, NoExplicitNull] = ( + match_type_1.MatchType.broad + ) class TrustedMatch(AdCPBaseModel): @@ -203,6 +215,7 @@ class TrustedMatch(AdCPBaseModel): description='Filter to products with specific TMP providers and match types. Each entry identifies a provider by agent_url and optionally requires specific match capabilities. Products must match at least one entry.', min_length=1, ), + NoExplicitNull, ] = None response_types: Annotated[ list[response_type.TmpResponseType] | None, @@ -210,6 +223,7 @@ class TrustedMatch(AdCPBaseModel): description="Filter to products supporting specific TMP response types (e.g., 'activation', 'creative', 'catalog_items'). Products must support at least one of the listed types.", min_length=1, ), + NoExplicitNull, ] = None @@ -220,6 +234,7 @@ class SignalTargetingItem1(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -227,6 +242,7 @@ class SignalTargetingItem1(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[Literal['binary'], Field(description='Discriminator for binary signals')] = 'binary' value: Annotated[ @@ -244,6 +260,7 @@ class SignalTargetingItem2(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -251,6 +268,7 @@ class SignalTargetingItem2(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['categorical'], Field(description='Discriminator for categorical signals') @@ -271,6 +289,7 @@ class SignalTargetingItem3(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -278,6 +297,7 @@ class SignalTargetingItem3(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['numeric'], Field(description='Discriminator for numeric signals') @@ -287,12 +307,14 @@ class SignalTargetingItem3(AdCPBaseModel): Field( description="Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided. Should be >= signal's range.min if defined." ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description="Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided. Should be <= signal's range.max if defined." ), + NoExplicitNull, ] = None @@ -302,6 +324,7 @@ class SignalTargetingItem5(SignalTargetingItem1, SignalTargetingItem4): Field( description="Desired use of this signal on the product. 'include' requires the product option to allow use in an any group. 'exclude' requires the product option to allow use in a none group." ), + NoExplicitNull, ] = TargetingMode.include @@ -311,6 +334,7 @@ class SignalTargetingItem6(SignalTargetingItem2, SignalTargetingItem4): Field( description="Desired use of this signal on the product. 'include' requires the product option to allow use in an any group. 'exclude' requires the product option to allow use in a none group." ), + NoExplicitNull, ] = TargetingMode.include @@ -320,6 +344,7 @@ class SignalTargetingItem7(SignalTargetingItem3, SignalTargetingItem4): Field( description="Desired use of this signal on the product. 'include' requires the product option to allow use in an any group. 'exclude' requires the product option to allow use in a none group." ), + NoExplicitNull, ] = TargetingMode.include @@ -341,28 +366,37 @@ class GeoProximityItem(AdCPBaseModel): lat: Annotated[ StrictFloat | None, Field(description='Latitude in decimal degrees (WGS 84)', ge=-90.0, le=90.0), + NoExplicitNull, ] = None lng: Annotated[ StrictFloat | None, Field(description='Longitude in decimal degrees (WGS 84)', ge=-180.0, le=180.0), + NoExplicitNull, ] = None label: Annotated[ str | None, Field(description="Human-readable label (e.g., 'Düsseldorf', 'Heathrow Airport')"), + NoExplicitNull, ] = None travel_time: Annotated[ - TravelTime | None, Field(description='Travel time limit for isochrone calculation') + TravelTime | None, + Field(description='Travel time limit for isochrone calculation'), + NoExplicitNull, ] = None transport_mode: Annotated[ transport_mode_1.TransportMode | None, Field( description='Transportation mode for isochrone calculation. Required when travel_time is provided.' ), + NoExplicitNull, + ] = None + radius: Annotated[ + Radius | None, Field(description='Simple radius from the point'), NoExplicitNull ] = None - radius: Annotated[Radius | None, Field(description='Simple radius from the point')] = None geometry: Annotated[ Geometry | None, Field(description='Pre-computed GeoJSON geometry defining the proximity boundary'), + NoExplicitNull, ] = None @@ -374,6 +408,7 @@ class AudienceActivationMethods2(AdCPBaseModel): transport: Annotated[ cloud_storage_protocol.CloudStorageProtocol | None, Field(description='Require this storage protocol (file_transfer only).'), + NoExplicitNull, ] = None directions: Annotated[ list[Direction] | None, @@ -381,10 +416,12 @@ class AudienceActivationMethods2(AdCPBaseModel): description="Require a non-empty intersection with the method's declared directions (file_transfer only).", min_length=1, ), + NoExplicitNull, ] = None vendor: Annotated[ brand_ref.BrandReference | None, Field(description="Require the method's vendor to match this reference."), + NoExplicitNull, ] = None @@ -396,6 +433,7 @@ class AudienceActivationMethods3(AdCPBaseModel): vendor: Annotated[ brand_ref.BrandReference | None, Field(description="Require the method's vendor to match this reference."), + NoExplicitNull, ] = None @@ -407,6 +445,7 @@ class AudienceActivationMethods4(AdCPBaseModel): vendor: Annotated[ brand_ref.BrandReference | None, Field(description="Require the method's vendor to match this reference."), + NoExplicitNull, ] = None @@ -418,6 +457,7 @@ class AudienceActivationMethods5(AdCPBaseModel): vendor: Annotated[ brand_ref.BrandReference | None, Field(description="Require the method's vendor to match this reference."), + NoExplicitNull, ] = None @@ -430,12 +470,14 @@ class RequiredVendorMetric(AdCPBaseModel): Field( description='Pin to a specific vendor. Optional — omit to match any vendor offering this `metric_id`.' ), + NoExplicitNull, ] = None metric_id: Annotated[ vendor_metric_id.VendorMetricId | None, Field( description="Pin to an exact metric identifier within a vendor's vocabulary. Optional — omit to match any metric from the pinned vendor. Cross-vendor identifier matching only works after vendors converge on identifiers (typically post-standardization)." ), + NoExplicitNull, ] = None @@ -443,18 +485,20 @@ class ProductFilters(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - delivery_type: delivery_type_1.DeliveryType | None = None + delivery_type: Annotated[delivery_type_1.DeliveryType | None, NoExplicitNull] = None exclusivity: Annotated[ exclusivity_1.Exclusivity | None, Field( description="Filter by exclusivity level. Returns products matching the specified exclusivity (e.g., 'exclusive' returns only sole-sponsorship products)." ), + NoExplicitNull, ] = None is_fixed_price: Annotated[ StrictBool | None, Field( description='Legacy filter for fixed versus auction pricing availability. true returns options with fixed_price; false returns auction options whose price is established through bid_price. Contingent options such as revenue_share match neither value and MUST be omitted whenever this filter is present. Use pricing_structures to discover contingent pricing. Products with both fixed and auction options match both true and false, but sellers MUST return only entries matching the requested structure.' ), + NoExplicitNull, ] = None pricing_structures: Annotated[ list[pricing_structure.PricingStructure] | None, @@ -462,6 +506,7 @@ class ProductFilters(AdCPBaseModel): description='Filter by how the payable price is determined. fixed selects options with fixed_price, auction selects options established through bid_price, and contingent selects options calculated from a measured business outcome after delivery (currently revenue_share). Products match when at least one pricing option has a requested structure. Sellers MUST return only matching pricing_options entries. When combined with is_fixed_price, both filters apply and the returned entries must satisfy both.', min_length=1, ), + NoExplicitNull, ] = None pricing_currencies: Annotated[ list[PricingCurrency] | None, @@ -469,6 +514,7 @@ class ProductFilters(AdCPBaseModel): description='Filter by currencies the buyer can use for the media product transaction, using ISO 4217 currency codes. Products match when they offer at least one product-level pricing_options entry in one of the requested currencies and any seller-applied or otherwise mandatory product-scoped signal charges are satisfiable in one of those currencies or have no incremental price. Mandatory custom signal pricing without currency is not satisfiable for this filter unless the seller can truthfully treat it as having no incremental price. Sellers MUST return only product pricing_options entries whose currency is in this list so buyers can select deterministically from discovery. This filter does not require pruning optional signal or vendor add-on pricing; buyers should avoid optional add-ons priced only in unsupported currencies.', min_length=1, ), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -477,6 +523,7 @@ class ProductFilters(AdCPBaseModel): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Filter by legacy named-format references. Use `format_kinds` or `format_option_refs`.', min_length=1, ), + NoExplicitNull, ] = None format_kinds: Annotated[ list[str] | None, @@ -484,6 +531,7 @@ class ProductFilters(AdCPBaseModel): description='Filter to products accepting any of these canonical format kinds.', min_length=1, ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -491,28 +539,36 @@ class ProductFilters(AdCPBaseModel): description='Filter to products accepting any of these exact publisher- or product-scoped canonical format options.', min_length=1, ), + NoExplicitNull, ] = None standard_formats_only: Annotated[ - StrictBool | None, Field(description='Only return products accepting IAB standard formats') + StrictBool | None, + Field(description='Only return products accepting IAB standard formats'), + NoExplicitNull, ] = None min_exposures: Annotated[ SchemaInt | None, Field(description='Minimum exposures/impressions needed for measurement validity', ge=1), + NoExplicitNull, ] = None start_date: Annotated[ date | None, Field( description='Campaign start date (ISO 8601 date format: YYYY-MM-DD) for availability checks' ), + NoExplicitNull, ] = None end_date: Annotated[ date | None, Field( description='Campaign end date (ISO 8601 date format: YYYY-MM-DD) for availability checks' ), + NoExplicitNull, ] = None budget_range: Annotated[ - BudgetRange | None, Field(description='Budget range to filter appropriate products') + BudgetRange | None, + Field(description='Budget range to filter appropriate products'), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -521,6 +577,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy coverage filter. On compact discovery tasks use criteria.offer_filters.countries. Return products whose inventory covers at least one requested area; this does not impose delivery targeting or require selectable targeting support. Retained get_products handlers MUST preserve the original coverage predicate.', min_length=1, ), + NoExplicitNull, ] = None regions: Annotated[ list[Region] | None, @@ -529,6 +586,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy coverage filter. On compact discovery tasks use criteria.offer_filters.regions. Return products whose inventory covers at least one requested area; this does not impose delivery targeting or require selectable targeting support. Retained get_products handlers MUST preserve the original coverage predicate.', min_length=1, ), + NoExplicitNull, ] = None metros: Annotated[ list[Metro] | None, @@ -537,6 +595,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy coverage filter. On compact discovery tasks use criteria.offer_filters.metros. Return products whose inventory covers at least one requested area; this does not impose delivery targeting or require selectable targeting support. Retained get_products handlers MUST preserve the original coverage predicate.', min_length=1, ), + NoExplicitNull, ] = None channels: Annotated[ list[channels_1.MediaChannel] | None, @@ -544,6 +603,7 @@ class ProductFilters(AdCPBaseModel): description="Filter by advertising channels (e.g., ['display', 'ctv', 'dooh'])", min_length=1, ), + NoExplicitNull, ] = None video_placement_types: Annotated[ list[video_placement_type.VideoPlacementType] | None, @@ -551,6 +611,7 @@ class ProductFilters(AdCPBaseModel): description='Filter product metadata by declared video placement types, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type; buyers needing exact placement inventory use targeting_overlay.placement_selection against targetable placements. This filter has set semantics for wholesale feed canonicalization.', min_length=1, ), + NoExplicitNull, ] = None audio_distribution_types: Annotated[ list[audio_distribution_type.AudioDistributionType] | None, @@ -558,6 +619,7 @@ class ProductFilters(AdCPBaseModel): description='Filter product metadata by declared audio distribution types, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type. This filter has set semantics for wholesale feed canonicalization.', min_length=1, ), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ list[sponsored_placement_type.SponsoredPlacementType] | None, @@ -565,6 +627,7 @@ class ProductFilters(AdCPBaseModel): description='Filter retail-media product metadata by declared sponsored-placement types (sponsored search, sponsored display, or sponsored native). A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type. This filter has set semantics for wholesale feed canonicalization.', min_length=1, ), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ list[social_placement_surface.SocialPlacementSurface] | None, @@ -572,6 +635,7 @@ class ProductFilters(AdCPBaseModel): description='Filter social-product metadata by declared placement surfaces (feed, stories, short_video, explore, or search). A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested surface; buyers needing an exact public placement use targeting_overlay.placement_selection. This filter has set semantics for wholesale feed canonicalization.', min_length=1, ), + NoExplicitNull, ] = None required_axe_integrations: Annotated[ list[AnyUrl] | None, @@ -580,12 +644,14 @@ class ProductFilters(AdCPBaseModel): description='Deprecated: Use trusted_match filter instead. Filter to products executable through specific agentic ad exchanges. URLs are canonical identifiers.', min_length=1, ), + NoExplicitNull, ] = None trusted_match: Annotated[ TrustedMatch | None, Field( description='Filter products by Trusted Match Protocol capabilities. Only products with matching TMP support are returned.' ), + NoExplicitNull, ] = None audience_activation_methods: Annotated[ list[ @@ -601,12 +667,14 @@ class ProductFilters(AdCPBaseModel): description="Filter to products whose audience_activation.methods matches at least one requested entry (OR across entries). Within an entry every specified field must match (AND); omitted optional fields are wildcards; directions matches on non-empty intersection, and a method that omits directions matches any requested directions; vendor matches on domain, plus brand_id when specified. Fields inapplicable to the requested pattern (e.g. transport on a clean_room entry) make that entry unsatisfiable. Sellers MUST exclude products with no audience_activation declaration when this filter is present; exclusions MAY be reported via filter_diagnostics.excluded_by. Experimental: part of the media_buy.audience_activation surface — buyers SHOULD check the seller's experimental_features before filtering on it; sellers that do not list the feature ignore this filter.", min_length=1, ), + NoExplicitNull, ] = None required_features: Annotated[ media_buy_features.MediaBuyFeatures | None, Field( description='Filter to products from sellers supporting specific protocol features. Only features set to true are used for filtering.' ), + NoExplicitNull, ] = None required_geo_targeting: Annotated[ list[RequiredGeoTargetingItem] | None, @@ -615,6 +683,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED. Use get_products.required_overlay_support, which is product-scoped and applies consistently to geographic and non-geographic targeting dimensions.', min_length=1, ), + NoExplicitNull, ] = None signal_targeting: Annotated[ list[SignalTargetingItem] | None, @@ -623,6 +692,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy signal-option eligibility filter. Retained get_products handlers MUST preserve the signal identity, value predicate, and requested include/exclude capability. This does not activate delivery targeting. Native buyers use criteria.targeting_overlay.signal_targeting_groups only for concrete delivery selections, preserving exclusion through group operators; copying targeting_mode into targeting_overlay.signal_targeting does not preserve it.', min_length=1, ), + NoExplicitNull, ] = None postal_areas: Annotated[ list[postal_area.PostalArea] | None, @@ -631,6 +701,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy coverage filter. On compact discovery tasks use criteria.offer_filters.postal_areas. Return products whose inventory covers at least one requested area; this does not impose delivery targeting or require selectable targeting support. Retained get_products handlers MUST preserve the original coverage predicate.', min_length=1, ), + NoExplicitNull, ] = None geo_proximity: Annotated[ list[GeoProximityItem] | None, @@ -639,6 +710,7 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy coverage filter. On compact discovery tasks use criteria.offer_filters.geo_proximity. Return products whose inventory covers at least one requested area; this does not impose delivery targeting or require selectable targeting support. Retained get_products handlers MUST preserve the original coverage predicate.', min_length=1, ), + NoExplicitNull, ] = None required_performance_standards: Annotated[ list[performance_standard.PerformanceStandard] | None, @@ -646,6 +718,7 @@ class ProductFilters(AdCPBaseModel): description="Filter to products that can meet the buyer's performance standard requirements. Each entry specifies a metric, minimum threshold, and optionally a required vendor and standard. Products that cannot meet these thresholds or do not support the specified vendors are excluded. Use this to tell the seller upfront: 'I need DoubleVerify for viewability at 70% MRC.'", min_length=1, ), + NoExplicitNull, ] = None required_metrics: Annotated[ list[available_metric.AvailableMetric] | None, @@ -658,6 +731,7 @@ class ProductFilters(AdCPBaseModel): ], min_length=1, ), + NoExplicitNull, ] = None required_vendor_metrics: Annotated[ list[RequiredVendorMetric] | None, @@ -678,6 +752,7 @@ class ProductFilters(AdCPBaseModel): ], min_length=1, ), + NoExplicitNull, ] = None keywords: Annotated[ list[Keyword] | None, @@ -686,16 +761,19 @@ class ProductFilters(AdCPBaseModel): description='DEPRECATED legacy product eligibility filter. Retained get_products handlers MUST preserve the requested keyword eligibility and match_type (default broad). This is not an instruction to add package keyword targeting. Native buyers use criteria.targeting_overlay.keyword_targets only when they intend a delivery constraint, or criteria.required_overlay_support.keyword_targets for future selectability.', min_length=1, ), + NoExplicitNull, ] = None audience_evidence_requirements: Annotated[ audience_evidence_requirements_1.AudienceEvidenceRequirements | None, Field( description='Buyer policy for evaluating Product.audience_evidence. In required mode, sellers MUST apply the evidence_presence and admissibility semantics and exclude non-matching products; they MUST NOT ignore an unsupported hard requirement. In preferred mode, sellers use matches for ranking and explain the evidence selected. Buyers SHOULD inspect media_buy.audience_evidence capabilities before sending this object.' ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Vendor-namespaced extension parameters for seller-specific filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; do not interpolate into LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/product_format_declaration.py b/src/adcp/types/domains/core/product_format_declaration.py index 7f0f65645..47f37d788 100644 --- a/src/adcp/types/domains/core/product_format_declaration.py +++ b/src/adcp/types/domains/core/product_format_declaration.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/product_format_declaration.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, Field, StrictBool from ..enums import channels @@ -44,6 +44,7 @@ class ProductFormatDeclaration1(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -51,12 +52,14 @@ class ProductFormatDeclaration1(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -64,60 +67,70 @@ class ProductFormatDeclaration1(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -125,12 +138,14 @@ class ProductFormatDeclaration1(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: image.CanonicalFormatImage @@ -142,6 +157,7 @@ class ProductFormatDeclaration2(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -149,12 +165,14 @@ class ProductFormatDeclaration2(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -162,60 +180,70 @@ class ProductFormatDeclaration2(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -223,12 +251,14 @@ class ProductFormatDeclaration2(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: html5.CanonicalFormatHtml5Banner @@ -240,6 +270,7 @@ class ProductFormatDeclaration3(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -247,12 +278,14 @@ class ProductFormatDeclaration3(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -260,60 +293,70 @@ class ProductFormatDeclaration3(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -321,12 +364,14 @@ class ProductFormatDeclaration3(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: display_tag.CanonicalFormatDisplayTag @@ -338,6 +383,7 @@ class ProductFormatDeclaration4(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -345,12 +391,14 @@ class ProductFormatDeclaration4(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -358,60 +406,70 @@ class ProductFormatDeclaration4(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -419,12 +477,14 @@ class ProductFormatDeclaration4(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: image_carousel.CanonicalFormatImageCarousel @@ -436,6 +496,7 @@ class ProductFormatDeclaration5(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -443,12 +504,14 @@ class ProductFormatDeclaration5(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -456,60 +519,70 @@ class ProductFormatDeclaration5(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -517,12 +590,14 @@ class ProductFormatDeclaration5(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: video_hosted.CanonicalFormatHostedVideo @@ -534,6 +609,7 @@ class ProductFormatDeclaration6(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -541,12 +617,14 @@ class ProductFormatDeclaration6(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -554,60 +632,70 @@ class ProductFormatDeclaration6(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -615,12 +703,14 @@ class ProductFormatDeclaration6(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: video_vast.CanonicalFormatVastVideo @@ -632,6 +722,7 @@ class ProductFormatDeclaration7(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -639,12 +730,14 @@ class ProductFormatDeclaration7(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -652,60 +745,70 @@ class ProductFormatDeclaration7(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -713,12 +816,14 @@ class ProductFormatDeclaration7(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: audio_hosted.CanonicalFormatHostedAudio @@ -730,6 +835,7 @@ class ProductFormatDeclaration8(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -737,12 +843,14 @@ class ProductFormatDeclaration8(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -750,60 +858,70 @@ class ProductFormatDeclaration8(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -811,12 +929,14 @@ class ProductFormatDeclaration8(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_vast'] = 'audio_vast' params: audio_vast.CanonicalFormatVastAudio @@ -828,6 +948,7 @@ class ProductFormatDeclaration9(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -835,12 +956,14 @@ class ProductFormatDeclaration9(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -848,60 +971,70 @@ class ProductFormatDeclaration9(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -909,12 +1042,14 @@ class ProductFormatDeclaration9(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: audio_daast.CanonicalFormatDaastAudio @@ -926,6 +1061,7 @@ class ProductFormatDeclaration10(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -933,12 +1069,14 @@ class ProductFormatDeclaration10(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -946,60 +1084,70 @@ class ProductFormatDeclaration10(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1007,12 +1155,14 @@ class ProductFormatDeclaration10(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven @@ -1024,6 +1174,7 @@ class ProductFormatDeclaration11(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1031,12 +1182,14 @@ class ProductFormatDeclaration11(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1044,60 +1197,70 @@ class ProductFormatDeclaration11(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1105,12 +1268,14 @@ class ProductFormatDeclaration11(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: native_in_feed.CanonicalFormatNativeInFeed @@ -1122,6 +1287,7 @@ class ProductFormatDeclaration12(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1129,12 +1295,14 @@ class ProductFormatDeclaration12(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1142,60 +1310,70 @@ class ProductFormatDeclaration12(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1203,12 +1381,14 @@ class ProductFormatDeclaration12(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: responsive_creative.CanonicalFormatResponsiveCreative @@ -1220,6 +1400,7 @@ class ProductFormatDeclaration13(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1227,12 +1408,14 @@ class ProductFormatDeclaration13(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1240,60 +1423,70 @@ class ProductFormatDeclaration13(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1301,12 +1494,14 @@ class ProductFormatDeclaration13(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement @@ -1318,6 +1513,7 @@ class ProductFormatDeclaration14(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1325,12 +1521,14 @@ class ProductFormatDeclaration14(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1338,60 +1536,70 @@ class ProductFormatDeclaration14(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1399,12 +1607,14 @@ class ProductFormatDeclaration14(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay @@ -1416,6 +1626,7 @@ class ProductFormatDeclaration15(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1423,12 +1634,14 @@ class ProductFormatDeclaration15(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1436,60 +1649,70 @@ class ProductFormatDeclaration15(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1497,12 +1720,14 @@ class ProductFormatDeclaration15(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['coordinated_placements'] = 'coordinated_placements' params: coordinated_placements.CanonicalFormatCoordinatedPlacements @@ -1514,6 +1739,7 @@ class ProductFormatDeclaration16(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1521,12 +1747,14 @@ class ProductFormatDeclaration16(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1534,60 +1762,70 @@ class ProductFormatDeclaration16(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1595,12 +1833,14 @@ class ProductFormatDeclaration16(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['custom'] = 'custom' params: Annotated[ diff --git a/src/adcp/types/domains/core/product_identity.py b/src/adcp/types/domains/core/product_identity.py index 1741ddb12..9f5164981 100644 --- a/src/adcp/types/domains/core/product_identity.py +++ b/src/adcp/types/domains/core/product_identity.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/product_identity.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool @@ -26,4 +26,5 @@ class ProductIdentity(AdCPBaseModel): description="Human-readable methodology for this product's custom reach unit. Required when the product reports reach or frequency with reach_unit custom, including frequency-only reporting.", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/product_offer_filters.py b/src/adcp/types/domains/core/product_offer_filters.py index da9a509ab..0227ced72 100644 --- a/src/adcp/types/domains/core/product_offer_filters.py +++ b/src/adcp/types/domains/core/product_offer_filters.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product_offer_filters.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat @@ -114,8 +114,8 @@ class Provider(AdCPBaseModel): extra='allow', ) agent_url: AnyUrl - context_match: StrictBool | None = None - identity_match: StrictBool | None = None + context_match: Annotated[StrictBool | None, NoExplicitNull] = None + identity_match: Annotated[StrictBool | None, NoExplicitNull] = None class TravelTime(AdCPBaseModel): @@ -137,6 +137,7 @@ class GeoProximityItem(AdCPBaseModel): ge=-90.0, le=90.0, ), + NoExplicitNull, ] = None lng: Annotated[ StrictFloat | None, @@ -145,48 +146,54 @@ class GeoProximityItem(AdCPBaseModel): ge=-180.0, le=180.0, ), + NoExplicitNull, ] = None label: Annotated[ str | None, Field( description="Human-readable label for this entry (e.g., 'Düsseldorf', 'Heathrow Airport', 'Primary trade area')." ), + NoExplicitNull, ] = None travel_time: Annotated[ TravelTime | None, Field( description='Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks.' ), + NoExplicitNull, ] = None transport_mode: Annotated[ transport_mode_1.TransportMode | None, Field( description='Transportation mode for isochrone calculation. Required when travel_time is provided.' ), + NoExplicitNull, ] = None radius: Annotated[ Radius | None, Field( description='Simple radius from the point. The platform draws a circle of this distance around the coordinates.' ), + NoExplicitNull, ] = None geometry: Annotated[ Geometry | None, Field( description='Pre-computed GeoJSON geometry defining the proximity boundary. Use when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom boundaries. When geometry is provided, lat/lng are not required.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class TrustedMatch(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - providers: Annotated[list[Provider] | None, Field(min_length=1)] = None - response_types: Annotated[list[response_type.TmpResponseType] | None, Field(min_length=1)] = ( - None - ) + providers: Annotated[list[Provider] | None, Field(min_length=1), NoExplicitNull] = None + response_types: Annotated[ + list[response_type.TmpResponseType] | None, Field(min_length=1), NoExplicitNull + ] = None class RequiredPerformanceStandard(AdCPBaseModel): @@ -195,7 +202,7 @@ class RequiredPerformanceStandard(AdCPBaseModel): ) metric: performance_standard_metric.PerformanceStandardMetric threshold: Annotated[StrictFloat, Field(ge=0.0, le=1.0)] - standard: viewability_standard.ViewabilityStandard | None = None + standard: Annotated[viewability_standard.ViewabilityStandard | None, NoExplicitNull] = None vendor: brand_key.BrandKey @@ -203,57 +210,64 @@ class RequiredVendorMetric(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - vendor: brand_key.BrandKey | None = None - metric_id: vendor_metric_id.VendorMetricId | None = None + vendor: Annotated[brand_key.BrandKey | None, NoExplicitNull] = None + metric_id: Annotated[vendor_metric_id.VendorMetricId | None, NoExplicitNull] = None class ProductOfferFilters(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - delivery_type: delivery_type_1.DeliveryType | None = None - exclusivity: exclusivity_1.Exclusivity | None = None + delivery_type: Annotated[delivery_type_1.DeliveryType | None, NoExplicitNull] = None + exclusivity: Annotated[exclusivity_1.Exclusivity | None, NoExplicitNull] = None is_fixed_price: Annotated[ StrictBool | None, Field( description='Filter fixed-price versus auction offers. Contingent pricing matches neither value.' ), + NoExplicitNull, ] = None pricing_structures: Annotated[ - list[pricing_structure.PricingStructure] | None, Field(min_length=1) + list[pricing_structure.PricingStructure] | None, Field(min_length=1), NoExplicitNull ] = None - pricing_currencies: Annotated[list[PricingCurrency] | None, Field(min_length=1)] = None - format_kinds: Annotated[list[str] | None, Field(min_length=1)] = None + pricing_currencies: Annotated[ + list[PricingCurrency] | None, Field(min_length=1), NoExplicitNull + ] = None + format_kinds: Annotated[list[str] | None, Field(min_length=1), NoExplicitNull] = None format_option_refs: Annotated[ - list[format_option_ref.FormatOptionReference] | None, Field(min_length=1) + list[format_option_ref.FormatOptionReference] | None, Field(min_length=1), NoExplicitNull ] = None - standard_formats_only: StrictBool | None = None - min_exposures: Annotated[SchemaInt | None, Field(ge=1)] = None + standard_formats_only: Annotated[StrictBool | None, NoExplicitNull] = None + min_exposures: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None start_date: Annotated[ date | None, Field( description='Fixed-flight availability filter: with end_date, declares the exact flight the buyer intends to run. Returned products MUST be able to serve that flight, and pricing and forecasts are scoped to it. Mutually exclusive with availability_horizon.' ), + NoExplicitNull, ] = None end_date: Annotated[ date | None, Field( description='Fixed-flight availability filter end. See start_date. Mutually exclusive with availability_horizon.' ), + NoExplicitNull, ] = None availability_horizon: Annotated[ AvailabilityHorizon | None, Field( description="Flexible-window availability discovery: the buyer is open to any bookable window inside [start_time, end_time) and asks the seller to describe when the returned inventory can run, instead of filtering to one exact flight. Sellers that support this field partition the horizon into time-dimensioned forecast rows (forecast-dimension-time) carrying availability_status; sellers that cannot cover the full horizon signal the gap via the response's incomplete[] mechanism. Unlike start_date/end_date this is not an eligibility filter — products remain returnable when only part of the horizon is open. The resulting availability is a snapshot bounded by the forecast's valid_until, never a hold. Mutually exclusive with start_date and end_date, which declare a fixed flight; buyers that already know their dates use those instead." ), + NoExplicitNull, ] = None - budget_range: budget_range_1.BudgetRange | None = None + budget_range: Annotated[budget_range_1.BudgetRange | None, NoExplicitNull] = None countries: Annotated[ list[Country] | None, Field( description="Filter by country coverage using ISO 3166-1 alpha-2 codes (e.g., ['US', 'CA', 'GB']). Returns products whose geographic coverage includes at least one of the specified countries. This is a product attribute filter, not a delivery-targeting instruction.", min_length=1, ), + NoExplicitNull, ] = None regions: Annotated[ list[Region] | None, @@ -261,6 +275,7 @@ class ProductOfferFilters(AdCPBaseModel): description='Inventory coverage in the specified ISO 3166-2 subdivisions: OR within this field, AND across offer filters. Does not add delivery targeting, require targeting support, or rescope pricing or forecasts.', min_length=1, ), + NoExplicitNull, ] = None metros: Annotated[ list[Metro] | None, @@ -268,6 +283,7 @@ class ProductOfferFilters(AdCPBaseModel): description='Inventory coverage in the specified metro areas (system and code): OR within this field, AND across offer filters. Does not add delivery targeting, require targeting support, or rescope pricing or forecasts.', min_length=1, ), + NoExplicitNull, ] = None postal_areas: Annotated[ list[postal_area.PostalArea] | None, @@ -275,6 +291,7 @@ class ProductOfferFilters(AdCPBaseModel): description='Inventory coverage in the specified postal areas: OR within this field, AND across offer filters. Does not add delivery targeting, require targeting support, or rescope pricing or forecasts.', min_length=1, ), + NoExplicitNull, ] = None geo_proximity: Annotated[ list[GeoProximityItem] | None, @@ -282,41 +299,53 @@ class ProductOfferFilters(AdCPBaseModel): description='Inventory coverage in the specified proximity boundaries: OR within this field, AND across offer filters. Does not add delivery targeting, require targeting support, or rescope pricing or forecasts.', min_length=1, ), + NoExplicitNull, ] = None property_list: Annotated[ property_list_ref.PropertyListReference | None, Field( description='Reference to an externally managed property list. When provided, the seller filters products to only those available on properties in the list. This narrows which publisher inventory is returned; it is a product attribute filter, not a delivery-targeting instruction.' ), + NoExplicitNull, + ] = None + channels: Annotated[ + list[channels_1.MediaChannel] | None, Field(min_length=1), NoExplicitNull ] = None - channels: Annotated[list[channels_1.MediaChannel] | None, Field(min_length=1)] = None video_placement_types: Annotated[ - list[video_placement_type.VideoPlacementType] | None, Field(min_length=1) + list[video_placement_type.VideoPlacementType] | None, Field(min_length=1), NoExplicitNull ] = None audio_distribution_types: Annotated[ - list[audio_distribution_type.AudioDistributionType] | None, Field(min_length=1) + list[audio_distribution_type.AudioDistributionType] | None, + Field(min_length=1), + NoExplicitNull, ] = None sponsored_placement_types: Annotated[ - list[sponsored_placement_type.SponsoredPlacementType] | None, Field(min_length=1) + list[sponsored_placement_type.SponsoredPlacementType] | None, + Field(min_length=1), + NoExplicitNull, ] = None social_placement_surfaces: Annotated[ - list[social_placement_surface.SocialPlacementSurface] | None, Field(min_length=1) + list[social_placement_surface.SocialPlacementSurface] | None, + Field(min_length=1), + NoExplicitNull, ] = None - trusted_match: TrustedMatch | None = None + trusted_match: Annotated[TrustedMatch | None, NoExplicitNull] = None required_features: Annotated[ canonical_media_buy_features.CanonicalMediaBuyFeatures | None, Field(description='Canonical protocol features the seller must support.'), + NoExplicitNull, ] = None required_performance_standards: Annotated[ - list[RequiredPerformanceStandard] | None, Field(min_length=1) + list[RequiredPerformanceStandard] | None, Field(min_length=1), NoExplicitNull ] = None required_metrics: Annotated[ - list[available_metric.AvailableMetric] | None, Field(min_length=1) + list[available_metric.AvailableMetric] | None, Field(min_length=1), NoExplicitNull ] = None - required_vendor_metrics: Annotated[list[RequiredVendorMetric] | None, Field(min_length=1)] = ( - None - ) - audience_evidence_requirements: ( - product_audience_evidence_requirements.ProductAudienceEvidenceRequirements | None - ) = None - ext: ext_1.ExtensionObject | None = None + required_vendor_metrics: Annotated[ + list[RequiredVendorMetric] | None, Field(min_length=1), NoExplicitNull + ] = None + audience_evidence_requirements: Annotated[ + product_audience_evidence_requirements.ProductAudienceEvidenceRequirements | None, + NoExplicitNull, + ] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/product_signal_targeting_option.py b/src/adcp/types/domains/core/product_signal_targeting_option.py index fe0551364..ca76f3938 100644 --- a/src/adcp/types/domains/core/product_signal_targeting_option.py +++ b/src/adcp/types/domains/core/product_signal_targeting_option.py @@ -1,12 +1,13 @@ # generated by datamodel-codegen: # filename: core/product_signal_targeting_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import signal_ref, vendor_pricing_option @@ -32,12 +33,14 @@ class ProductSignalTargetingOption(SignalListing): Field( description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the seller exposes a distinct runtime or activation handle that buyers must echo in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].signal_agent_segment_id. Buyers SHOULD echo this handle verbatim rather than reconstructing identity from categorical values; providers MAY namespace handles so cross-provider identity stays legible without a shared taxonomy registry.' ), + NoExplicitNull, ] = None activation_status: Annotated[ ActivationStatus | None, Field( description="Whether this signal option is ready to select on create_media_buy for the requesting account. 'ready' means the buyer can select it directly. 'requires_activation' means the buyer must activate the signal first or include an activation_key the seller accepts." ), + NoExplicitNull, ] = ActivationStatus.ready allowed_targeting_modes: Annotated[ list[AllowedTargetingMode] | None, @@ -45,18 +48,21 @@ class ProductSignalTargetingOption(SignalListing): description="How this signal may be used when composing package-level signal targeting groups. 'include' means the signal may appear in an 'any' child group. 'exclude' means the signal may appear in a 'none' child group. Omit when the signal is include-only. This field declares the allowed buy-time group operator; binary package signal entries still use value=true in both include and exclude groups.", min_length=1, ), + NoExplicitNull, ] = [AllowedTargetingMode.include] default_selected: Annotated[ StrictBool | None, Field( description="Whether the seller recommends or preselects this signal when composing this product. Buyers may remove it unless signal_targeting_rules.selection_mode is 'fixed'. When selection_mode is 'fixed', sellers apply default_selected signals even if the buyer omits signal_targeting_groups and MUST echo the applied entries on the resulting package state." ), + NoExplicitNull, ] = False selection_group: Annotated[ str | None, Field( description='Optional product-defined composability bucket for signal options, such as alternative audience tiers, a key-value targeting plane, or an audience-segment targeting plane. Signals in the same selection_group are expected to be OR-combinable inside one child group for a given targeting mode, subject to signal_targeting_rules. Use different selection_group values when the product requires separate ANDed clauses, such as signal sets backed by different platform targeting primitives that cannot be collapsed into one child group. selection_group is a product-option grouping key, not a reference to one child object in packages[].targeting_overlay.signal_targeting_groups.groups[]. Sellers can use signal_targeting_rules.max_selected_per_group and signal_targeting_rules.selection_group_rules with selection_group to guide and validate storefront composition.' ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -64,6 +70,7 @@ class ProductSignalTargetingOption(SignalListing): description='Signal pricing options available when this signal is selected on this product. Product-scoped pricing is authoritative for this product; if get_signals exposes a different default rate card, use this product-scoped price when composing the buy. Buyers pass the selected pricing_option_id in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].pricing_option_id. Omit when the signal is bundled into the product price or has no incremental cost.', min_length=1, ), + NoExplicitNull, ] = None signal_ref: Annotated[ signal_ref.SignalRef, diff --git a/src/adcp/types/domains/core/product_targeting_resolution.py b/src/adcp/types/domains/core/product_targeting_resolution.py index 43d742ffc..1c0819610 100644 --- a/src/adcp/types/domains/core/product_targeting_resolution.py +++ b/src/adcp/types/domains/core/product_targeting_resolution.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/product_targeting_resolution.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -30,5 +30,6 @@ class ProductTargetingResolution(AdCPBaseModel): description='Optional digest of the canonical effective targeting overlay. Until AdCP defines a cross-implementation canonicalization algorithm, this value is an opaque seller audit handle and buyers MUST NOT attempt to reproduce or compare it across sellers.', pattern='^sha256:[0-9a-f]{64}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/property.py b/src/adcp/types/domains/core/property.py index 76dbc6284..073d4f3b3 100644 --- a/src/adcp/types/domains/core/property.py +++ b/src/adcp/types/domains/core/property.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/property.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import channels, identifier_types @@ -40,6 +40,7 @@ class Property(AdCPBaseModel): Field( description='Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.' ), + NoExplicitNull, ] = None property_type: Annotated[ property_type_1.PropertyType, Field(description='Type of advertising property') @@ -53,16 +54,19 @@ class Property(AdCPBaseModel): Field( description='Tags for categorization and grouping (e.g., network membership, content categories)' ), + NoExplicitNull, ] = None supported_channels: Annotated[ list[channels.MediaChannel] | None, Field( description="Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, Field( description='Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/property_delivery_metrics.py b/src/adcp/types/domains/core/property_delivery_metrics.py index c6987ce92..66782be00 100644 --- a/src/adcp/types/domains/core/property_delivery_metrics.py +++ b/src/adcp/types/domains/core/property_delivery_metrics.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/property_delivery_metrics.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any +from adcp.types.base import NoExplicitNull from pydantic import Field from . import identifier as identifier_1 @@ -32,12 +33,14 @@ class PropertyDeliveryMetrics(DeliveryMetrics): Field( description="Canonical publisher-scoped catalog identity when the delivered property resolves to adagents.json. Its publisher_domain MUST equal the row's publisher_domain." ), + NoExplicitNull, ] = None property_name: Annotated[ str | None, Field( description='Current human-readable property name. Convenience metadata only; property_ref is stable identity.' ), + NoExplicitNull, ] = None impressions: Any spend: Any diff --git a/src/adcp/types/domains/core/property_list_ref.py b/src/adcp/types/domains/core/property_list_ref.py index 9373a7a11..9c2de6935 100644 --- a/src/adcp/types/domains/core/property_list_ref.py +++ b/src/adcp/types/domains/core/property_list_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/property_list_ref.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -23,4 +23,5 @@ class PropertyListReference(AdCPBaseModel): Field( description='JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/proposal.py b/src/adcp/types/domains/core/proposal.py index 04b10b2aa..c88896b85 100644 --- a/src/adcp/types/domains/core/proposal.py +++ b/src/adcp/types/domains/core/proposal.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/proposal.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import pacing as pacing_1 @@ -22,16 +22,22 @@ class TotalBudgetGuidance(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - min: Annotated[StrictFloat | None, Field(description='Minimum recommended budget', ge=0.0)] = ( - None - ) + min: Annotated[ + StrictFloat | None, Field(description='Minimum recommended budget', ge=0.0), NoExplicitNull + ] = None recommended: Annotated[ - StrictFloat | None, Field(description='Recommended budget for optimal performance', ge=0.0) + StrictFloat | None, + Field(description='Recommended budget for optimal performance', ge=0.0), + NoExplicitNull, ] = None max: Annotated[ - StrictFloat | None, Field(description='Maximum budget before diminishing returns', ge=0.0) + StrictFloat | None, + Field(description='Maximum budget before diminishing returns', ge=0.0), + NoExplicitNull, ] = None - currency: Annotated[str | None, Field(description='ISO 4217 currency code')] = None + currency: Annotated[str | None, Field(description='ISO 4217 currency code'), NoExplicitNull] = ( + None + ) class Proposal(AdCPBaseModel): @@ -53,6 +59,7 @@ class Proposal(AdCPBaseModel): Field( description='Explanation of the proposal strategy and what it achieves', max_length=2000 ), + NoExplicitNull, ] = None allocations: Annotated[ list[product_allocation.ProductAllocation], @@ -66,39 +73,47 @@ class Proposal(AdCPBaseModel): Field( description='How the executed total budget is allocated across proposal products. Omit for legacy fixed proposals.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Recommended aggregate pacing for the executed media-buy budget. On a committed proposal this is part of the firm delivery terms.' ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Aggregate cap bound into this legacy proposal. It is authoritative when the proposal is executed and uses one counter across its packages.' ), + NoExplicitNull, ] = None proposal_status: Annotated[ proposal_status_1.ProposalStatus | None, Field( description="Lifecycle status of this proposal and the per-proposal source of truth for whether finalization is required before create_media_buy. When absent, the proposal is ready to buy (backward compatible). 'draft' means indicative pricing — finalize via refine before purchasing. 'committed' means firm pricing with inventory reserved until expires_at and executable via create_media_buy." ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='When this proposal expires and can no longer be executed. For draft proposals, indicates when indicative pricing becomes stale. For committed proposals, indicates when the inventory hold lapses — the buyer must call create_media_buy before this time.' ), + NoExplicitNull, ] = None insertion_order: Annotated[ insertion_order_1.InsertionOrder | None, Field( description='Formal insertion order attached to a committed proposal. Present when the seller requires a signed agreement before the media buy can proceed. The buyer references the io_id in io_acceptance on create_media_buy.' ), + NoExplicitNull, ] = None total_budget_guidance: Annotated[ - TotalBudgetGuidance | None, Field(description='Optional budget guidance for this proposal') + TotalBudgetGuidance | None, + Field(description='Optional budget guidance for this proposal'), + NoExplicitNull, ] = None brief_alignment: Annotated[ str | None, @@ -106,11 +121,13 @@ class Proposal(AdCPBaseModel): description='Explanation of how this proposal aligns with the campaign brief', max_length=2000, ), + NoExplicitNull, ] = None forecast: Annotated[ delivery_forecast.DeliveryForecast | None, Field( description='Aggregate forecasted delivery metrics for the entire proposal. When both proposal-level and allocation-level forecasts are present, the proposal-level forecast is authoritative for total delivery estimation.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/protocol_envelope.py b/src/adcp/types/domains/core/protocol_envelope.py index 7f75dfd17..35498ba36 100644 --- a/src/adcp/types/domains/core/protocol_envelope.py +++ b/src/adcp/types/domains/core/protocol_envelope.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/protocol_envelope.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import task_status @@ -24,18 +24,21 @@ class ProtocolEnvelope(AdCPBaseModel): Field( description='Transport-managed conversation identifier. On A2A, this maps to the native Message/Task `contextId` used to associate messages with a conversation; it is not carried inside the AdCP DataPart. On MCP, a request-body `context_id`, where admitted by the selected request schema, is a compatibility-only field: servers MUST ignore it, callers MUST NOT rely on it for continuity, and it MUST NOT select session state, identity, account, authorization, task continuation, or idempotency scope. MCP continuity, if provided, comes from the transport session. Distinct from `context` (per-request opaque echo, see below) and from `task_id` (AdCP operation tracking).' ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (transport-managed A2A conversation correlation or MCP compatibility metadata) — `context` is caller-owned echo and never selects transport state. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root — they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.' ), + NoExplicitNull, ] = None task_id: Annotated[ str | None, Field( description='Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.' ), + NoExplicitNull, ] = None status: Annotated[ task_status.TaskStatus, @@ -48,30 +51,35 @@ class ProtocolEnvelope(AdCPBaseModel): Field( description='Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response.' ), + NoExplicitNull, ] = None timestamp: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress.' ), + NoExplicitNull, ] = None replayed: Annotated[ StrictBool | None, Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False adcp_error: Annotated[ error.Error | None, Field( description="Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array — the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` — the envelope MUST NOT carry `adcp_error` for non-failures." ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='AdCP application-layer webhook configuration for async task updates over MCP, A2A, or REST. Echoed from the request to confirm webhook settings. It is distinct from transport-native progress or A2A TaskPushNotificationConfig delivery and can outlive the originating transport session.' ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -81,10 +89,12 @@ class ProtocolEnvelope(AdCPBaseModel): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None payload: Annotated[ dict[str, Any] | None, Field( description='Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct — it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/provenance.py b/src/adcp/types/domains/core/provenance.py index c02f2c0b4..b22fee04a 100644 --- a/src/adcp/types/domains/core/provenance.py +++ b/src/adcp/types/domains/core/provenance.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/provenance.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..enums import c2pa_watermark_action @@ -33,12 +33,14 @@ class AiTool(AdCPBaseModel): Field( description="Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version." ), + NoExplicitNull, ] = None provider: Annotated[ str | None, Field( description="Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" ), + NoExplicitNull, ] = None @@ -62,6 +64,7 @@ class DeclaredBy(AdCPBaseModel): agent_url: Annotated[ AnyUrl | None, Field(description='URL of the agent or service that declared this provenance'), + NoExplicitNull, ] = None role: Annotated[Role, Field(description='Role of the declaring party in the supply chain')] @@ -87,6 +90,7 @@ class VerifyAgent(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -100,6 +104,7 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" ), + NoExplicitNull, ] = None provider: Annotated[ str, @@ -112,9 +117,12 @@ class EmbeddedProvenanceItem(AdCPBaseModel): Field( description="Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts)." ), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the provenance data was embedded (ISO 8601)') + AwareDatetime | None, + Field(description='When the provenance data was embedded (ISO 8601)'), + NoExplicitNull, ] = None @@ -133,6 +141,7 @@ class VerifyAgent10(AdCPBaseModel): Field( description="Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog." ), + NoExplicitNull, ] = None @@ -142,6 +151,7 @@ class RenderGuidance(AdCPBaseModel): Field( description='How long the disclosure must persist during content playback or display' ), + NoExplicitNull, ] = None min_duration_ms: Annotated[ SchemaInt | None, @@ -149,6 +159,7 @@ class RenderGuidance(AdCPBaseModel): description="Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.", ge=1, ), + NoExplicitNull, ] = None positions: Annotated[ list[disclosure_position.DisclosurePosition] | None, @@ -156,8 +167,9 @@ class RenderGuidance(AdCPBaseModel): description='Preferred disclosure positions in priority order. The first position a format supports should be used.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Jurisdiction(AdCPBaseModel): @@ -167,6 +179,7 @@ class Jurisdiction(AdCPBaseModel): region: Annotated[ str | None, Field(description="Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)"), + NoExplicitNull, ] = None regulation: Annotated[ str, @@ -179,12 +192,14 @@ class Jurisdiction(AdCPBaseModel): Field( description='Required disclosure label text for this jurisdiction, in the local language' ), + NoExplicitNull, ] = None render_guidance: Annotated[ RenderGuidance | None, Field( description="How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed." ), + NoExplicitNull, ] = None @@ -198,6 +213,7 @@ class Disclosure(AdCPBaseModel): jurisdictions: Annotated[ list[Jurisdiction] | None, Field(description='Jurisdictions where disclosure obligations apply', min_length=1), + NoExplicitNull, ] = None @@ -216,7 +232,9 @@ class VerificationItem(AdCPBaseModel): ), ] verified_time: Annotated[ - AwareDatetime | None, Field(description='When the verification was performed (ISO 8601)') + AwareDatetime | None, + Field(description='When the verification was performed (ISO 8601)'), + NoExplicitNull, ] = None result: Annotated[Result, Field(description='Verification outcome')] confidence: Annotated[ @@ -224,9 +242,10 @@ class VerificationItem(AdCPBaseModel): Field( description='Confidence score of the verification result (0.0 to 1.0)', ge=0.0, le=1.0 ), + NoExplicitNull, ] = None details_url: Annotated[ - AnyUrl | None, Field(description='URL to the full verification report') + AnyUrl | None, Field(description='URL to the full verification report'), NoExplicitNull ] = None @@ -246,13 +265,17 @@ class Watermark(AdCPBaseModel): Field( description="Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist)." ), + NoExplicitNull, ] = None c2pa_action: Annotated[ c2pa_watermark_action.C2PaWatermarkAction | None, Field(description='C2PA action classification for this watermark'), + NoExplicitNull, ] = None embedded_at: Annotated[ - AwareDatetime | None, Field(description='When the watermark was applied (ISO 8601)') + AwareDatetime | None, + Field(description='When the watermark was applied (ISO 8601)'), + NoExplicitNull, ] = None @@ -262,46 +285,54 @@ class Provenance(AdCPBaseModel): Field( description='IPTC-aligned classification of AI involvement in producing this content' ), + NoExplicitNull, ] = None synthetic_depiction: Annotated[ StrictBool | None, Field( description='Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.' ), + NoExplicitNull, ] = None ai_tool: Annotated[ AiTool | None, Field( description='AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.' ), + NoExplicitNull, ] = None human_oversight: Annotated[ HumanOversight | None, Field( description='Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.' ), + NoExplicitNull, ] = None declared_by: Annotated[ DeclaredBy | None, Field( description='Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.' ), + NoExplicitNull, ] = None declared_at: Annotated[ AwareDatetime | None, Field( description='When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.' ), + NoExplicitNull, ] = None created_time: Annotated[ AwareDatetime | None, Field(description='When this content was created or generated (ISO 8601)'), + NoExplicitNull, ] = None c2pa: Annotated[ C2pa | None, Field( description='C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.' ), + NoExplicitNull, ] = None embedded_provenance: Annotated[ list[EmbeddedProvenanceItem] | None, @@ -309,6 +340,7 @@ class Provenance(AdCPBaseModel): description='Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None watermarks: Annotated[ list[Watermark] | None, @@ -316,12 +348,14 @@ class Provenance(AdCPBaseModel): description='Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).', min_length=1, ), + NoExplicitNull, ] = None disclosure: Annotated[ Disclosure | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), + NoExplicitNull, ] = None verification: Annotated[ list[VerificationItem] | None, @@ -329,5 +363,6 @@ class Provenance(AdCPBaseModel): description='Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/publisher_property_selector.py b/src/adcp/types/domains/core/publisher_property_selector.py index 5b4cc5862..141cb6f7d 100644 --- a/src/adcp/types/domains/core/publisher_property_selector.py +++ b/src/adcp/types/domains/core/publisher_property_selector.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/publisher_property_selector.json -# timestamp: 2026-10-04T00:53:27+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -30,6 +30,7 @@ class PublisherPropertySelector1(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -37,6 +38,7 @@ class PublisherPropertySelector1(AdCPBaseModel): description="Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['all'], @@ -77,6 +79,7 @@ class PublisherPropertySelector3(AdCPBaseModel): description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None publisher_domains: Annotated[ list[PublisherDomain] | None, @@ -84,6 +87,7 @@ class PublisherPropertySelector3(AdCPBaseModel): description="Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.", min_length=1, ), + NoExplicitNull, ] = None selection_type: Annotated[ Literal['by_tag'], Field(description='Discriminator indicating selection by property tags') diff --git a/src/adcp/types/domains/core/push_notification_config.py b/src/adcp/types/domains/core/push_notification_config.py index 1dd3b1cd7..11d7fbde8 100644 --- a/src/adcp/types/domains/core/push_notification_config.py +++ b/src/adcp/types/domains/core/push_notification_config.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/push_notification_config.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import auth_scheme @@ -48,6 +48,7 @@ class PushNotificationConfig(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None token: Annotated[ str | None, @@ -56,6 +57,7 @@ class PushNotificationConfig(AdCPBaseModel): max_length=4096, min_length=16, ), + NoExplicitNull, ] = None authentication: Annotated[ Authentication | None, @@ -63,4 +65,5 @@ class PushNotificationConfig(AdCPBaseModel): deprecated=True, description='Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt \'try 9421 first, fall back to HMAC\' verification — signature mode is determined solely by whether this block was present at registration time. The seller\'s baseline 9421 webhook key is published at its brand.json `agents[]` `jwks_uri` using `adcp_use: "request-signing"` (deprecated `webhook-signing` keys remain accepted during the compatibility window); it does not override this selector and is only used when `authentication` is omitted. See docs/building/by-layer/L1/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook\'s signing mode does not match the registered mode).', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/real_estate_item.py b/src/adcp/types/domains/core/real_estate_item.py index 338963531..45b5ed6e3 100644 --- a/src/adcp/types/domains/core/real_estate_item.py +++ b/src/adcp/types/domains/core/real_estate_item.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/real_estate_item.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -19,12 +19,18 @@ class Address(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - street: Annotated[str | None, Field(description='Street address.')] = None - city: Annotated[str | None, Field(description='City name.')] = None - region: Annotated[str | None, Field(description='State, province, or region.')] = None - postal_code: Annotated[str | None, Field(description='Postal or ZIP code.')] = None + street: Annotated[str | None, Field(description='Street address.'), NoExplicitNull] = None + city: Annotated[str | None, Field(description='City name.'), NoExplicitNull] = None + region: Annotated[ + str | None, Field(description='State, province, or region.'), NoExplicitNull + ] = None + postal_code: Annotated[str | None, Field(description='Postal or ZIP code.'), NoExplicitNull] = ( + None + ) country: Annotated[ - str | None, Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$') + str | None, + Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$'), + NoExplicitNull, ] = None @@ -80,35 +86,52 @@ class RealEstateItem(AdCPBaseModel): price: Annotated[ price_1.Price | None, Field(description="Property price or rental rate. Use period 'month' for rentals."), + NoExplicitNull, + ] = None + property_type: Annotated[ + PropertyType | None, Field(description='Type of property.'), NoExplicitNull ] = None - property_type: Annotated[PropertyType | None, Field(description='Type of property.')] = None listing_type: Annotated[ - ListingType | None, Field(description='Whether the property is for sale or rent.') + ListingType | None, + Field(description='Whether the property is for sale or rent.'), + NoExplicitNull, + ] = None + bedrooms: Annotated[ + SchemaInt | None, Field(description='Number of bedrooms.', ge=0), NoExplicitNull ] = None - bedrooms: Annotated[SchemaInt | None, Field(description='Number of bedrooms.', ge=0)] = None bathrooms: Annotated[ StrictFloat | None, Field( description='Number of bathrooms (e.g., 2.5 for two full and one half bath).', ge=0.0 ), + NoExplicitNull, + ] = None + area: Annotated[Area | None, Field(description='Property size.'), NoExplicitNull] = None + description: Annotated[ + str | None, Field(description='Property description.'), NoExplicitNull ] = None - area: Annotated[Area | None, Field(description='Property size.')] = None - description: Annotated[str | None, Field(description='Property description.')] = None location: Annotated[ - Location | None, Field(description='Geographic coordinates of the property.') + Location | None, + Field(description='Geographic coordinates of the property.'), + NoExplicitNull, + ] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Primary property image URL.'), NoExplicitNull + ] = None + url: Annotated[AnyUrl | None, Field(description='Listing page URL.'), NoExplicitNull] = None + neighborhood: Annotated[ + str | None, Field(description='Neighborhood or area name.'), NoExplicitNull + ] = None + year_built: Annotated[ + SchemaInt | None, Field(description='Year the property was built.'), NoExplicitNull ] = None - image_url: Annotated[AnyUrl | None, Field(description='Primary property image URL.')] = None - url: Annotated[AnyUrl | None, Field(description='Listing page URL.')] = None - neighborhood: Annotated[str | None, Field(description='Neighborhood or area name.')] = None - year_built: Annotated[SchemaInt | None, Field(description='Year the property was built.')] = ( - None - ) tags: Annotated[ list[str] | None, Field( description="Tags for filtering (e.g., 'garden', 'parking', 'renovated', 'waterfront').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -116,5 +139,6 @@ class RealEstateItem(AdCPBaseModel): description="Typed creative asset pools for this property listing. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (exterior/interior hero), 'images_vertical' (9:16 for Stories), 'images_square' (1:1). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reference_asset.py b/src/adcp/types/domains/core/reference_asset.py index 877e9bfa4..9c952e0c4 100644 --- a/src/adcp/types/domains/core/reference_asset.py +++ b/src/adcp/types/domains/core/reference_asset.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reference_asset.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -39,4 +39,5 @@ class ReferenceAsset(AdCPBaseModel): Field( description='Human-readable description of the asset and how it should inform creative generation' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reference_renderer.py b/src/adcp/types/domains/core/reference_renderer.py index f9b6a454d..da0ab1e98 100644 --- a/src/adcp/types/domains/core/reference_renderer.py +++ b/src/adcp/types/domains/core/reference_renderer.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reference_renderer.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, field_validator @@ -75,6 +75,7 @@ class ReferenceRenderer(AdCPBaseModel): description="Deprecated compatibility annotation retained for previously published registry entries. Renderer package versions and community format revisions have independent lifecycles, so consumers MUST NOT require this value to equal the enclosing entry's format_revision or use matching values as evidence of compatibility. Registry review and contract fixtures bind the named export to the enclosing format's input contract. New entries SHOULD omit this field.", pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$', ), + NoExplicitNull, ] = None integrity: Annotated[ str, diff --git a/src/adcp/types/domains/core/registry_event.py b/src/adcp/types/domains/core/registry_event.py index 6dcdbb0cd..5943fa871 100644 --- a/src/adcp/types/domains/core/registry_event.py +++ b/src/adcp/types/domains/core/registry_event.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/registry_event.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Any, Literal from uuid import UUID -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel, StrictBool @@ -66,6 +66,7 @@ class Payload(AdCPBaseModel): Field( description='Registry evidence source for the merge, such as adagents_json or manual_review.' ), + NoExplicitNull, ] = None @@ -114,8 +115,8 @@ class Payload1(AdCPBaseModel): extra='allow', ) property_rid: UUID - last_resolved_at: AwareDatetime | None = None - reason: str | None = None + last_resolved_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + reason: Annotated[str | None, NoExplicitNull] = None class RegistryEvent4(AdCPBaseModel): @@ -173,6 +174,7 @@ class Payload3(AdCPBaseModel): Field( description='Registry evidence source for the merge, such as adagents_json or manual_review.' ), + NoExplicitNull, ] = None @@ -329,13 +331,13 @@ class AgentProfilePayload(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - agent_url: AnyUrl | None = None - name: str | None = None - type: Type | None = None - channels: StringArray | None = None - property_types: StringArray | None = None - markets: list[Market] | None = None - categories: StringArray | None = None + agent_url: Annotated[AnyUrl | None, NoExplicitNull] = None + name: Annotated[str | None, NoExplicitNull] = None + type: Annotated[Type | None, NoExplicitNull] = None + channels: Annotated[StringArray | None, NoExplicitNull] = None + property_types: Annotated[StringArray | None, NoExplicitNull] = None + markets: Annotated[list[Market] | None, NoExplicitNull] = None + categories: Annotated[StringArray | None, NoExplicitNull] = None category_taxonomy: str | None = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -343,14 +345,15 @@ class AgentProfilePayload(AdCPBaseModel): deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Named-format profile projection. Use canonical `format_kinds` and `format_option_refs`.', ), + NoExplicitNull, ] = None - format_kinds: list[str] | None = None - tags: StringArray | None = None - delivery_types: StringArray | None = None - property_count: Annotated[SchemaInt | None, Field(ge=0)] = None - publisher_count: Annotated[SchemaInt | None, Field(ge=0)] = None - has_tmp: StrictBool | None = None - updated_at: AwareDatetime | None = None + format_kinds: Annotated[list[str] | None, NoExplicitNull] = None + tags: Annotated[StringArray | None, NoExplicitNull] = None + delivery_types: Annotated[StringArray | None, NoExplicitNull] = None + property_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + publisher_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + has_tmp: Annotated[StrictBool | None, NoExplicitNull] = None + updated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class Country(Market): @@ -423,7 +426,9 @@ class Payload5(AdCPBaseModel): extra='allow', ) agent_url: AnyUrl - removed_from_publishers: Annotated[list[Domain] | None, Field(min_length=1)] = None + removed_from_publishers: Annotated[list[Domain] | None, Field(min_length=1), NoExplicitNull] = ( + None + ) class RegistryEvent11(AdCPBaseModel): @@ -467,7 +472,7 @@ class RegistryEvent11(AdCPBaseModel): class Payload6(AgentProfilePayload): - changed_fields: ChangedFields | None = None + changed_fields: Annotated[ChangedFields | None, NoExplicitNull] = None agent_url: Any @@ -517,8 +522,8 @@ class Storyboard(AdCPBaseModel): ) storyboard_id: str status: StoryboardStatus - steps_passed: Annotated[SchemaInt | None, Field(ge=0)] = None - steps_total: Annotated[SchemaInt | None, Field(ge=0)] = None + steps_passed: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + steps_total: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class Payload7(AdCPBaseModel): @@ -534,7 +539,7 @@ class Payload7(AdCPBaseModel): ] storyboards_passing: Annotated[SchemaInt, Field(ge=0)] storyboards_total: Annotated[SchemaInt, Field(ge=0)] - storyboards: list[Storyboard] | None = None + storyboards: Annotated[list[Storyboard] | None, NoExplicitNull] = None class RegistryEvent13(AdCPBaseModel): @@ -584,12 +589,13 @@ class Payload8(AdCPBaseModel): agent_url: AnyUrl role: BadgeRole verified_specialisms: Annotated[list[str], Field(min_length=1)] - adcp_version: str | None = None + adcp_version: Annotated[str | None, NoExplicitNull] = None grading_profile: Annotated[ GradingProfile | None, Field( description='Grading profile that produced the badge. Historical events emitted before profile selection may omit this field and are Legacy.' ), + NoExplicitNull, ] = None @@ -640,12 +646,13 @@ class Payload9(AdCPBaseModel): agent_url: AnyUrl role: BadgeRole reason: str - adcp_version: str | None = None + adcp_version: Annotated[str | None, NoExplicitNull] = None grading_profile: Annotated[ GradingProfile | None, Field( description='Grading profile in force when the badge was lost. Historical events emitted before profile selection may omit this field and are Legacy.' ), + NoExplicitNull, ] = None @@ -693,35 +700,38 @@ class Payload10(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - publisher_domain: Domain | None = None + publisher_domain: Annotated[Domain | None, NoExplicitNull] = None domain: Annotated[ Domain | None, Field( deprecated=True, description='Legacy alias for publisher_domain retained for early feed examples.', ), + NoExplicitNull, ] = None - properties_added: Annotated[SchemaInt | None, Field(ge=0)] = None - properties_removed: Annotated[SchemaInt | None, Field(ge=0)] = None - agents_added: list[AnyUrl] | None = None - agents_removed: list[AnyUrl] | None = None - agent_count: Annotated[SchemaInt | None, Field(ge=0)] = None - property_count: Annotated[SchemaInt | None, Field(ge=0)] = None - collection_count: Annotated[SchemaInt | None, Field(ge=0)] = None + properties_added: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + properties_removed: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + agents_added: Annotated[list[AnyUrl] | None, NoExplicitNull] = None + agents_removed: Annotated[list[AnyUrl] | None, NoExplicitNull] = None + agent_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + property_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + collection_count: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None format_count: Annotated[ SchemaInt | None, Field(description='Number of top-level formats[] declarations after this revision.', ge=0), + NoExplicitNull, ] = None placement_count: Annotated[ SchemaInt | None, Field( description='Number of top-level placements[] declarations after this revision.', ge=0 ), + NoExplicitNull, ] = None - changed_fields: ChangedFields | None = None - discovery_method: str | None = None + changed_fields: Annotated[ChangedFields | None, NoExplicitNull] = None + discovery_method: Annotated[str | None, NoExplicitNull] = None manager_domain: str | None = None - source: str | None = None + source: Annotated[str | None, NoExplicitNull] = None class RegistryEvent16(AdCPBaseModel): @@ -773,11 +783,13 @@ class Payload12(AdCPBaseModel): Field( description='Registry authorization row id when the event is backed by a materialized effective authorization row.' ), + NoExplicitNull, ] = None agent_url: AnyUrl agent_url_canonical: Annotated[ str | None, Field(description='Registry-canonicalized form of agent_url for equality checks.'), + NoExplicitNull, ] = None publisher_domain: Domain authorization_type: Annotated[ @@ -785,13 +797,22 @@ class Payload12(AdCPBaseModel): Field( description='When present, identifies the adagents.json authorization variant represented by this payload.' ), + NoExplicitNull, ] = None authorized_for: str | None = None - property_ids: Annotated[list[property_id.PropertyId] | None, Field(min_length=1)] = None - property_tags: Annotated[list[property_tag.PropertyTag] | None, Field(min_length=1)] = None - properties: Annotated[list[property_1.Property] | None, Field(min_length=1)] = None + property_ids: Annotated[ + list[property_id.PropertyId] | None, Field(min_length=1), NoExplicitNull + ] = None + property_tags: Annotated[ + list[property_tag.PropertyTag] | None, Field(min_length=1), NoExplicitNull + ] = None + properties: Annotated[list[property_1.Property] | None, Field(min_length=1), NoExplicitNull] = ( + None + ) publisher_properties: Annotated[ - list[publisher_property_selector.PublisherPropertySelector] | None, Field(min_length=1) + list[publisher_property_selector.PublisherPropertySelector] | None, + Field(min_length=1), + NoExplicitNull, ] = None property_rid: Annotated[ UUID | None, @@ -805,34 +826,36 @@ class Payload12(AdCPBaseModel): description='Publisher-local property id for materialized per-property authorization rows.' ), ] = None - placement_ids: Annotated[list[str] | None, Field(min_length=1)] = None - placement_tags: Annotated[list[str] | None, Field(min_length=1)] = None + placement_ids: Annotated[list[str] | None, Field(min_length=1), NoExplicitNull] = None + placement_tags: Annotated[list[str] | None, Field(min_length=1), NoExplicitNull] = None collections: Annotated[ list[collection_selector.CollectionSelector] | None, Field( description='Collection constraints from authorized_agents[*].collections. When set, the event does NOT authorize the property unqualified — consumers building a local authorization index MUST scope the grant to these selectors and fail closed when a query carries no collection scope. A selector without collection_ids is a bulk grant for all collections declared at that publisher_domain.', min_length=1, ), + NoExplicitNull, ] = None - countries: Countries | None = None - delegation_type: DelegationType | None = None - exclusive: StrictBool | None = None + countries: Annotated[Countries | None, NoExplicitNull] = None + delegation_type: Annotated[DelegationType | None, NoExplicitNull] = None + exclusive: Annotated[StrictBool | None, NoExplicitNull] = None signing_keys: Annotated[ list[agent_signing_key.AgentSigningKey] | None, Field( description='Publisher-attested signing keys copied from adagents.json when the registry has them. Advisory in feed events; verifiers MUST re-fetch the authoritative publisher artifact before treating keys as a trust anchor.', min_length=1, ), + NoExplicitNull, ] = None - effective_from: AwareDatetime | None = None - effective_until: AwareDatetime | None = None - evidence: Evidence | None = None - disputed: StrictBool | None = None + effective_from: Annotated[AwareDatetime | None, NoExplicitNull] = None + effective_until: Annotated[AwareDatetime | None, NoExplicitNull] = None + evidence: Annotated[Evidence | None, NoExplicitNull] = None + disputed: Annotated[StrictBool | None, NoExplicitNull] = None created_by: str | None = None expires_at: AwareDatetime | None = None created_at: AwareDatetime | None = None updated_at: AwareDatetime | None = None - override_applied: StrictBool | None = None + override_applied: Annotated[StrictBool | None, NoExplicitNull] = None override_reason: str | None = None @@ -972,16 +995,19 @@ class PropertyPayload(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - property_rid: UUID | None = None - classification: Classification | None = None - source: PropertySource | None = None - identifiers: Annotated[list[identifier.Identifier] | None, Field(min_length=1)] = None - publisher_domain: Domain | None = None + property_rid: Annotated[UUID | None, NoExplicitNull] = None + classification: Annotated[Classification | None, NoExplicitNull] = None + source: Annotated[PropertySource | None, NoExplicitNull] = None + identifiers: Annotated[ + list[identifier.Identifier] | None, Field(min_length=1), NoExplicitNull + ] = None + publisher_domain: Annotated[Domain | None, NoExplicitNull] = None property: Annotated[ property_1.Property | None, Field(description='Optional full post-change property object when available.'), + NoExplicitNull, ] = None - changed_fields: ChangedFields | None = None + changed_fields: Annotated[ChangedFields | None, NoExplicitNull] = None class AuthorizationPayload(Payload12): @@ -1073,7 +1099,7 @@ class RegistryEvent2(AdCPBaseModel): class Payload2(PropertyPayload): - reactivated_at: AwareDatetime | None = None + reactivated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None property_rid: Any @@ -1118,11 +1144,11 @@ class RegistryEvent5(AdCPBaseModel): class Payload11(PublisherAdagentsPayload): - properties_added: Annotated[SchemaInt | None, Field(ge=0)] = None - properties_removed: Annotated[SchemaInt | None, Field(ge=0)] = None - agents_added: list[AnyUrl] | None = None - agents_removed: list[AnyUrl] | None = None - changed_fields: ChangedFields | None = None + properties_added: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + properties_removed: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + agents_added: Annotated[list[AnyUrl] | None, NoExplicitNull] = None + agents_removed: Annotated[list[AnyUrl] | None, NoExplicitNull] = None + changed_fields: Annotated[ChangedFields | None, NoExplicitNull] = None class RegistryEvent17(AdCPBaseModel): @@ -1169,25 +1195,27 @@ class CollectionPayload(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - collection_rid: UUID | None = None - publisher_domain: Domain | None = None + collection_rid: Annotated[UUID | None, NoExplicitNull] = None + publisher_domain: Annotated[Domain | None, NoExplicitNull] = None collection_id: Annotated[ str | None, Field(description='Publisher-local collection_id from the authoritative adagents.json.'), ] = None name: str | None = None kind: collection_kind.CollectionKind | None = None - source: PropertySource | None = None - status: Status | None = None + source: Annotated[PropertySource | None, NoExplicitNull] = None + status: Annotated[Status | None, NoExplicitNull] = None identifiers: Annotated[ list[CollectionIdentifier] | None, Field(description='Distribution identifiers that alias this collection across platforms.'), + NoExplicitNull, ] = None collection: Annotated[ collection_1.Collection | None, Field(description='Optional full post-change collection object when available.'), + NoExplicitNull, ] = None - changed_fields: ChangedFields | None = None + changed_fields: Annotated[ChangedFields | None, NoExplicitNull] = None class RegistryEvent6(AdCPBaseModel): diff --git a/src/adcp/types/domains/core/reporting_adjustment.py b/src/adcp/types/domains/core/reporting_adjustment.py index b0bc57966..071cac2d2 100644 --- a/src/adcp/types/domains/core/reporting_adjustment.py +++ b/src/adcp/types/domains/core/reporting_adjustment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_adjustment.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import reporting_control_total @@ -57,6 +57,7 @@ class ReportingAdjustment(AdCPBaseModel): max_length=1024, min_length=1, ), + NoExplicitNull, ] = None accounting_period: Annotated[ AccountingPeriod, @@ -77,6 +78,7 @@ class ReportingAdjustment(AdCPBaseModel): description='SHA-256 of the RFC 8785 JCS serialization of this adjustment with canonical_adjustment_sha256 omitted. Reconciled Billing consumers recompute this digest before accepting or rejecting the adjustment.', pattern='^[A-Fa-f0-9]{64}$', ), + NoExplicitNull, ] = None correction_observed_at: AwareDatetime created_at: AwareDatetime diff --git a/src/adcp/types/domains/core/reporting_adjustment_receipt.py b/src/adcp/types/domains/core/reporting_adjustment_receipt.py index 4efbdd996..371f3a110 100644 --- a/src/adcp/types/domains/core/reporting_adjustment_receipt.py +++ b/src/adcp/types/domains/core/reporting_adjustment_receipt.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_adjustment_receipt.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -43,6 +43,7 @@ class ReportingAdjustmentReceipt(AdCPBaseModel): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None status: Status observed_adjustment_sha256: Annotated[ @@ -53,7 +54,7 @@ class ReportingAdjustmentReceipt(AdCPBaseModel): ), ] rejection_codes: Annotated[ - list[ReportingAdjustmentRejectionCode] | None, Field(min_length=1) + list[ReportingAdjustmentRejectionCode] | None, Field(min_length=1), NoExplicitNull ] = None observed_at: AwareDatetime - received_at: AwareDatetime | None = None + received_at: Annotated[AwareDatetime | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_canonicalization_contract.py b/src/adcp/types/domains/core/reporting_canonicalization_contract.py index c33e43797..b75c69882 100644 --- a/src/adcp/types/domains/core/reporting_canonicalization_contract.py +++ b/src/adcp/types/domains/core/reporting_canonicalization_contract.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reporting_canonicalization_contract.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -56,7 +56,7 @@ class GoldenVectors(AdCPBaseModel): ) empty_report: Annotated[EmptyReport, Field(title='EmptyReportGoldenVector')] ordering_encoding: Annotated[OrderingEncoding, Field(title='OrderingEncodingGoldenVector')] - additional: list[AdditionalItem] | None = None + additional: Annotated[list[AdditionalItem] | None, NoExplicitNull] = None class ReportingPrimaryKey(ScalarStr): diff --git a/src/adcp/types/domains/core/reporting_capabilities.py b/src/adcp/types/domains/core/reporting_capabilities.py index 36f5a4fa2..ba5467593 100644 --- a/src/adcp/types/domains/core/reporting_capabilities.py +++ b/src/adcp/types/domains/core/reporting_capabilities.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_capabilities.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ..enums import available_metric, reporting_frequency @@ -49,6 +49,7 @@ class VendorMetric(AdCPBaseModel): Field( description="Relationship between this product's seller and `vendor` — `first_party` (the seller or its own measurement agent produced the number), `affiliated` (distinct legal entity under common ownership or control: parent/subsidiary, controlling stake, joint venture, disclosed board overlap), or `third_party` (independent vendor). Defined on structural facts only — a commercial dependency does not make a vendor `affiliated`; see the enum. A property of the (seller, vendor) pair, which is why it is declared here rather than on the vendor's catalog. REQUIRED whenever `vendor` resolves to the seller's own `brand.json` or to an entity under common ownership or control with the seller — the self-measurement case is permitted only when it is disclosed, so a seller MUST NOT leave the field absent and let a buyer guess. RECOMMENDED for unrelated third-party vendors. Absence means *undeclared*: buyers MUST treat an absent value as unknown, never as `third_party`, and MAY exclude undeclared rows from reconciliation. This is a relationship disposition, not a trust ranking — quality and independence-of-incentive questions belong to the vendor catalog's `accreditations[]` and `standard_reference`, not to this field. Methodology (sensor, panel, model, instrument) is NOT declared here — it lives at the vendor catalog's `description`, `methodology_url`, `methodology_version`, `standard_reference`, and `accreditations[]`. MAY be echoed on `vendor_metric_values[].vendor_relationship` in delivery." ), + NoExplicitNull, ] = None @@ -84,6 +85,7 @@ class ReportingCapabilities(AdCPBaseModel): Field( description='Product-scoped subset of get_adcp_capabilities.media_buy.reporting_delivery.offerings[].offering_id that packages using this product can satisfy. This binds seller-wide managed-delivery offerings to product/package eligibility. An empty array explicitly declares no managed offering; absence means product-level applicability is unknown and MUST NOT be inferred from the seller-wide list. Account, seat, credential, or provider constraints may narrow support further during sync_accounts validation.' ), + NoExplicitNull, ] = None available_metrics: Annotated[ list[available_metric.AvailableMetric], @@ -100,102 +102,119 @@ class ReportingCapabilities(AdCPBaseModel): Field( description="Vendor-defined metrics this product can report, beyond the closed `available_metrics` enum. Each entry is a pointer (`{ vendor, metric_id }`) into the vendor's metric catalog — the canonical definition (standard alignment, accreditations, methodology, unit, human-readable description) lives at the vendor's `get_adcp_capabilities.measurement.metrics[]`, queried once per vendor when needed. Use this for proprietary metrics like attention scores, emissions, panel-based demographics, or platform-native social metrics not yet in the standard enum. Sellers populate values in delivery via `delivery-metrics.json#/properties/vendor_metric_values`. The metric is identified by the tuple `(vendor, metric_id)`; identifiers are namespaced by the vendor, so the same `metric_id` may mean different things in different vendors' vocabularies. Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST de-duplicate before emission and MUST NOT declare the same vendor metric twice. Buyers MAY treat duplicate `(vendor, metric_id)` rows as a seller-side conformance bug. (JSON Schema `uniqueItems` is not used here because BrandRef carries optional fields whose absence/presence would defeat deep-equal — uniqueness is on the semantic key, enforced at build/validation time on the seller side.) Promotion path: when the industry converges on a metric via a published standard, the spec adds it to the closed `available_metrics` enum and the vendor extensions become historical aliases. The `vendor` MAY resolve to the selling party's own `brand.json` — a seller MAY be its own measurement vendor (DOOH sensor networks, retail-media closed loops, walled gardens) provided it publishes the metric in an `agents[type='measurement']` catalog like any other vendor and declares the relationship via `vendor_relationship`; the catalog contract is not relaxed for first-party measurement." ), + NoExplicitNull, ] = None supports_creative_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports creative-level metric breakdowns in delivery reporting (by_creative within by_package)' ), + NoExplicitNull, ] = None supports_format_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports canonical creative-format breakdowns in GET delivery reporting (by_format within by_package, keyed by format_kind). This is independent from supports_creative_breakdown because a seller may expose aggregate format-grain reporting without exposing individual creative performance.' ), + NoExplicitNull, ] = None supports_keyword_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports keyword-level metric breakdowns in delivery reporting (by_keyword within by_package)' ), + NoExplicitNull, ] = None supports_geo_breakdown: Annotated[ geo_breakdown_support.GeographicBreakdownSupport | None, Field( description='Geographic breakdown support for this product. Declares which geo levels and systems are available for by_geo reporting within by_package.' ), + NoExplicitNull, ] = None supports_device_type_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports device type breakdowns in delivery reporting (by_device_type within by_package)' ), + NoExplicitNull, ] = None supports_device_platform_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports device platform breakdowns in delivery reporting (by_device_platform within by_package)' ), + NoExplicitNull, ] = None supports_audience_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports audience segment breakdowns in delivery reporting (by_audience within by_package)' ), + NoExplicitNull, ] = None supports_demographic_breakdown: Annotated[ demographic_reporting_capability.DemographicReportingCapability | None, Field( description='Product-scoped demographic breakdown support for by_demographic reporting. Declares reportable age ranges and measurement systems independently from demographic targeting execution.' ), + NoExplicitNull, ] = None supports_placement_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports placement breakdowns in delivery reporting (by_placement within by_package)' ), + NoExplicitNull, ] = None supports_property_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports property breakdowns in delivery reporting (by_property within by_package).' ), + NoExplicitNull, ] = None supports_collection_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports collection breakdowns in delivery reporting (by_collection within by_package).' ), + NoExplicitNull, ] = None supports_installment_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports installment breakdowns in delivery reporting (by_installment within by_package).' ), + NoExplicitNull, ] = None supports_collection_property_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports collection × property intersection reporting (by_collection_property within by_package).' ), + NoExplicitNull, ] = None supports_installment_property_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports installment × property intersection reporting (by_installment_property within by_package).' ), + NoExplicitNull, ] = None supports_placement_property_breakdown: Annotated[ StrictBool | None, Field( description='Whether this product supports placement × property intersection reporting (by_placement_property within by_package).' ), + NoExplicitNull, ] = None supports_spot_breakdown: Annotated[ spot_reporting_capability.SpotReportingCapability | None, Field( description='Spot-level as-run airing-log support and metrics available at spot grain for broadcast TV, radio, and other scheduled inventory.' ), + NoExplicitNull, ] = None date_range_support: Annotated[ DateRangeSupport, @@ -209,6 +228,7 @@ class ReportingCapabilities(AdCPBaseModel): description='Granularities at which this product honors per-window pulls on get_media_buy_delivery (via request `time_granularity` + `include_window_breakdown: true`). Closes the GET-side half of the snapshot/log two-paths-parity contract for data-bearing events: a buyer who missed a webhook fire at any granularity listed here can reconstruct an identical payload by polling. Capability-scoped MUST — sellers MUST honor pulls at any granularity declared here, and MUST return UNSUPPORTED_GRANULARITY for pulls outside the set. Sellers MAY emit higher-frequency webhooks than they expose for pull (common where the webhook is a Kafka tap and historical reads go through a warehouse with coarser granularity); buyers see the gap up front via this capability and treat the webhook as primary for those frequencies. Absent or empty means the product only supports cumulative date-range pulls and full per-window recovery via GET is unavailable — see snapshot-and-log Rule 4.', examples=[['daily'], ['hourly', 'daily'], ['hourly', 'daily', 'monthly']], ), + NoExplicitNull, ] = None measurement_windows: Annotated[ list[measurement_window.MeasurementWindow] | None, @@ -216,4 +236,5 @@ class ReportingCapabilities(AdCPBaseModel): description='Measurement maturation stages available for this product. Used by any channel where billing-grade data is produced in phases rather than arriving final on day one. Examples: broadcast/linear TV (Live → C3 → C7 DVR accumulation), DOOH (tentative plays → post-IVT/fraud-check final), digital with IVT filtering (raw → GIVT filtered → SIVT filtered), podcast (7-day downloads → 30-day downloads). Each window defines an accumulation period and expected data availability. When present, delivery reports reference a specific window_id. Sellers whose data is final on first delivery typically omit this.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_consumer_status.py b/src/adcp/types/domains/core/reporting_consumer_status.py index b8f56908b..fde8eed4e 100644 --- a/src/adcp/types/domains/core/reporting_consumer_status.py +++ b/src/adcp/types/domains/core/reporting_consumer_status.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_consumer_status.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field @@ -66,6 +66,7 @@ class ReportingConsumerStatus(AdCPBaseModel): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None delivery_config_id: Annotated[ str, Field(max_length=64, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$') @@ -94,6 +95,7 @@ class ReportingConsumerStatus(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None reporting_revision_id: Annotated[ str | None, @@ -103,6 +105,7 @@ class ReportingConsumerStatus(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None observed_revision_content_sha256: Annotated[ str | None, @@ -110,6 +113,7 @@ class ReportingConsumerStatus(AdCPBaseModel): description='revision_content_sha256 independently recomputed from the exact consumed Core revision binding. Required for received and content_mismatch, where it proves which exact revision content the consumer read; unlike a Reconciled Billing receipt it carries no materialization evidence, row totals, canonical digest, or billing acceptance.', pattern='^[A-Fa-f0-9]{64}$', ), + NoExplicitNull, ] = None consumer_status: Annotated[ ConsumerStatus, @@ -128,12 +132,14 @@ class ReportingConsumerStatus(AdCPBaseModel): Field( description="Closed reason the consumed revision contradicts the accepted configuration generation. Each value is decidable from the obligation, the pinned report definition, and the revision itself, with no reference to either party's own measurement. scope_media_buy_missing: a media buy frozen in the obligation's media_buy_ids denominator is absent from the revision and is not represented by an explicit zero row, so the revision cannot distinguish zero delivery from an omitted buy. coverage_short: the revision covers fewer packages than the obligation's frozen coverage.covered_package_ids claims. metric_missing: a metric named in the pinned report definition's metrics[].name is absent from the revision. schema_nonconformant: rows do not validate against the reporting profile's pinned schema_uri and schema_sha256. currency_mismatch: a value's unit disagrees with the unit the pinned report definition fixed for that metric, or a control total's unit disagrees with the profile-defined unit for that name. period_mismatch: the revision carries a time dimension declared by the pinned grain whose values fall outside the obligation's half-open period. Precedence when more than one applies: schema_nonconformant is used only when the failure is structural validation against the pinned schema; a metric that is simply absent uses metric_missing even when the pinned schema declares it required. Each names a contract fact already fixed by the accepted generation, never a difference of opinion about counts. Agents dispatch on this value, not on prose." ), + NoExplicitNull, ] = None failure_code: Annotated[ FailureCode | None, Field( description='Typed reason a named revision was unreadable. Agents dispatch on this value, not prose or provider response bodies.' ), + NoExplicitNull, ] = None consumer_commit_ref: Annotated[ str | None, @@ -143,6 +149,7 @@ class ReportingConsumerStatus(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,512}$', ), + NoExplicitNull, ] = None seller_ledger_snapshot_id: Annotated[ str | None, @@ -151,14 +158,17 @@ class ReportingConsumerStatus(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None seller_ledger_as_of: Annotated[ AwareDatetime | None, Field( description='ledger_as_of echoed from seller_ledger_snapshot_id. Present if and only if seller_ledger_snapshot_id is present.' ), + NoExplicitNull, ] = None recorded_at: Annotated[ AwareDatetime | None, Field(description='When the seller durably recorded this immutable statement.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_control_total.py b/src/adcp/types/domains/core/reporting_control_total.py index fdbd69365..70e9602ec 100644 --- a/src/adcp/types/domains/core/reporting_control_total.py +++ b/src/adcp/types/domains/core/reporting_control_total.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reporting_control_total.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -32,6 +32,7 @@ class ReportingControlTotal1(AdCPBaseModel): max_length=32, min_length=1, ), + NoExplicitNull, ] = None @@ -57,6 +58,7 @@ class ReportingControlTotal2(AdCPBaseModel): max_length=32, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_coverage.py b/src/adcp/types/domains/core/reporting_coverage.py index b3ada0b0c..9491e1b06 100644 --- a/src/adcp/types/domains/core/reporting_coverage.py +++ b/src/adcp/types/domains/core/reporting_coverage.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_coverage.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -45,7 +45,9 @@ class Limitation(AdCPBaseModel): ) reason: Reason media_buy_id: ReportingMediaBuyId - package_ids: Annotated[list[ReportingPackageId] | None, Field(min_length=1)] = None + package_ids: Annotated[list[ReportingPackageId] | None, Field(min_length=1), NoExplicitNull] = ( + None + ) class ReportingCoverage(AdCPBaseModel): diff --git a/src/adcp/types/domains/core/reporting_dataset_share_destination.py b/src/adcp/types/domains/core/reporting_dataset_share_destination.py index c67138f1b..b8b6c6fb4 100644 --- a/src/adcp/types/domains/core/reporting_dataset_share_destination.py +++ b/src/adcp/types/domains/core/reporting_dataset_share_destination.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_dataset_share_destination.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -46,8 +46,8 @@ class Recipient(AdCPBaseModel): extra='forbid', ) identity: Annotated[str, Field(max_length=512, min_length=1)] - cloud: ReportingCloud | None = None - region: Annotated[str | None, Field(max_length=128, min_length=1)] = None + cloud: Annotated[ReportingCloud | None, NoExplicitNull] = None + region: Annotated[str | None, Field(max_length=128, min_length=1), NoExplicitNull] = None class ReportingDatasetShareDestination2(AdCPBaseModel): diff --git a/src/adcp/types/domains/core/reporting_delivery_capabilities.py b/src/adcp/types/domains/core/reporting_delivery_capabilities.py index 6770cc64f..11a95495f 100644 --- a/src/adcp/types/domains/core/reporting_delivery_capabilities.py +++ b/src/adcp/types/domains/core/reporting_delivery_capabilities.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/reporting_delivery_capabilities.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, EmailStr, Field, StrictBool from . import reporting_delivery_offering, reporting_reliability_statistics @@ -25,6 +25,7 @@ class OperationsContact(AdCPBaseModel): Field( description='HTTPS page a human uses to open or track a reporting issue, such as a support portal or status page. Same hardened origin shape as the offering document URIs: never an IP literal, userinfo URL, loopback host, AdCP task endpoint, webhook target, or credentialed link.' ), + NoExplicitNull, ] = None email: Annotated[ EmailStr | None, @@ -32,6 +33,7 @@ class OperationsContact(AdCPBaseModel): description='Monitored operations mailbox for reporting escalations. A role address, not an individual.', max_length=254, ), + NoExplicitNull, ] = None @@ -51,12 +53,14 @@ class ReportingDeliveryCapabilities(AdCPBaseModel): Field( description='Tier flag: this seller supports managed file, dataset-share, or warehouse delivery. Offerings whose method names a delivery pattern require this tier. When false or absent, every offering is API-delivered and Core-only.' ), + NoExplicitNull, ] = None reconciled_billing: Annotated[ StrictBool | None, Field( description='Tier flag: this seller supports canonical-digest verification and authenticated consumer receipts for both report materializations and post-official adjustments through receipt_task. Offerings with reconciliation_mode consumer_receipt and billing-grade canonicalization require this tier.' ), + NoExplicitNull, ] = None configuration_task: Literal['sync_accounts'] | None = None status_task: Literal['get_reporting_status'] | None = None @@ -123,18 +127,21 @@ class ReportingDeliveryCapabilities(AdCPBaseModel): description="Maximum interval after a CONSUMER_STATUS_MISMATCH issue's opened_at during which the seller may keep that issue at a non-escalated recommended_action. After it, the issue MUST be action_required with a contact_ recommended_action naming the diagnosed responsible_party. Declaring it requires operations_contact so the escalation has a destination. Absence means the seller publishes no escalation commitment; it never means an unbounded one.", ge=0, ), + NoExplicitNull, ] = None operations_contact: Annotated[ OperationsContact | None, Field( description='Optional non-secret human escalation path for reporting issues the protocol cannot resolve. It is display metadata for an operator, not an AdCP endpoint: agents MUST NOT dereference, probe, or send protocol traffic to these values, and they carry no authorization. Required when consumer_mismatch_escalation_seconds is advertised.' ), + NoExplicitNull, ] = None reliability_statistics: Annotated[ list[reporting_reliability_statistics.ReportingReliabilityStatistics] | None, Field( description='Optional evidence-scoped observed performance for advertised offerings. offering_id values MUST be unique and name offerings in this capability block.' ), + NoExplicitNull, ] = None resource_retention_days: Annotated[ SchemaInt | None, @@ -142,14 +149,16 @@ class ReportingDeliveryCapabilities(AdCPBaseModel): description='Minimum period after publication for which at least one verified exact materialization remains readable to every still-authorized intended consumer.', ge=1, ), + NoExplicitNull, ] = None - supports_webhook_activity: StrictBool | None = None + supports_webhook_activity: Annotated[StrictBool | None, NoExplicitNull] = None authorization_revocation_seconds: Annotated[ SchemaInt | None, Field( description="Maximum delay after caller/account authorization ends before seller-controlled transport access, provider grants, and write credentials are revoked. It cannot revoke a buyer's access to data already written into a buyer-owned destination.", ge=0, ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/reporting_delivery_config.py b/src/adcp/types/domains/core/reporting_delivery_config.py index 44ab8c951..04a60c6b7 100644 --- a/src/adcp/types/domains/core/reporting_delivery_config.py +++ b/src/adcp/types/domains/core/reporting_delivery_config.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/reporting_delivery_config.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import reporting_finality @@ -28,9 +28,9 @@ class Scope(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - all_media_buys: Literal[True] | None = None + all_media_buys: Annotated[Literal[True] | None, NoExplicitNull] = None media_buy_ids: Annotated[ - list[reporting_coverage.ReportingMediaBuyId] | None, Field(min_length=1) + list[reporting_coverage.ReportingMediaBuyId] | None, Field(min_length=1), NoExplicitNull ] = None @model_validator(mode='before') @@ -153,12 +153,16 @@ class ReportingDeliveryConfiguration(AdCPBaseModel): Field( description="Reserved: which party's count of this feed is authoritative. seller (the default, and the only value any 3.2 seller accepts) means the seller produces every revision and the consumer may only attest to what it consumed. consumer is reserved for the buyer-deposited billing revision task scoped to a later minor; until that task exists sellers MUST reject it with UNSUPPORTED_FEATURE. Reserving the field now keeps a future buyer-basis billing feed additive instead of breaking the billing-feed constraints. See https://github.com/adcontextprotocol/adcp/issues/7440." ), + NoExplicitNull, ] = AuthoritativeParty.seller schedule: reporting_schedule.ReportingSchedule - method: reporting_delivery_method.ReportingDeliveryMethod | None = None + method: Annotated[reporting_delivery_method.ReportingDeliveryMethod | None, NoExplicitNull] = ( + None + ) revocation_effective_at: Annotated[ AwareDatetime | None, Field( description='Optional requested cutoff for deactivation. No new publication may begin after the applied cutoff; historical access is limited to the contracted recovery window.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_delivery_config_state.py b/src/adcp/types/domains/core/reporting_delivery_config_state.py index dfe7b6cd4..4209ddd31 100644 --- a/src/adcp/types/domains/core/reporting_delivery_config_state.py +++ b/src/adcp/types/domains/core/reporting_delivery_config_state.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_delivery_config_state.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from . import reporting_coverage, reporting_delivery_config, reporting_status_issue @@ -33,8 +33,8 @@ class Setup(AdCPBaseModel): min_length=1, ), ] - url: AnyUrl | None = None - expires_at: AwareDatetime | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + expires_at: Annotated[AwareDatetime | None, NoExplicitNull] = None class ReportingDeliveryConfigLifecycleState(StrEnum): @@ -58,34 +58,41 @@ class ReportingDeliveryConfigurationState(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None - validated_at: AwareDatetime | None = None - activated_at: AwareDatetime | None = None - deactivated_at: AwareDatetime | None = None + validated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + activated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + deactivated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None publication_stopped_at: Annotated[ AwareDatetime | None, Field( description='Applied schedule boundary at or after deactivation. No obligation whose period starts at or after this cutoff is created; earlier obligations remain owed through their SLA and recovery lifecycle.' ), + NoExplicitNull, ] = None seller_managed_access_ends_at: Annotated[ AwareDatetime | None, Field( description='End of historical access to a producer-hosted share/resource for a still-authorized principal after voluntary deactivation. Inapplicable to data already written into a buyer-owned destination.' ), + NoExplicitNull, ] = None current_coverage: Annotated[ reporting_coverage.ReportingCoverage | None, Field( description='Current effective product/package coverage for the selected offering and resolved account. This setup-time view may change as media buys or provider capabilities change; each period obligation later freezes its own authoritative coverage.' ), + NoExplicitNull, ] = None setup: Annotated[ Setup | None, Field( description='Secret-free next step when provider-side authorization or recipient activation cannot be completed automatically.' ), + NoExplicitNull, ] = None issues: Annotated[ - list[reporting_status_issue.ReportingStatusIssue] | None, Field(min_length=1) + list[reporting_status_issue.ReportingStatusIssue] | None, + Field(min_length=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_delivery_offering.py b/src/adcp/types/domains/core/reporting_delivery_offering.py index 02ad6a866..8123b8d84 100644 --- a/src/adcp/types/domains/core/reporting_delivery_offering.py +++ b/src/adcp/types/domains/core/reporting_delivery_offering.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_delivery_offering.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import reporting_finality @@ -71,6 +71,7 @@ class ReportingProfile(AdCPBaseModel): max_length=128, min_length=1, ), + NoExplicitNull, ] = None canonicalization_contract_version: Literal['1.0'] = '1.0' canonicalization_media_type: Literal['application/vnd.adcp.reporting-canonicalization+json'] = ( @@ -81,6 +82,7 @@ class ReportingProfile(AdCPBaseModel): Field( description='Retrievable exact canonicalization contract on the authenticated seller/provider or AdCP-registry origin. SDKs apply the same bounded, redirect-free SSRF controls as schema_uri and verify canonicalization_sha256 before use.' ), + NoExplicitNull, ] = None canonicalization_sha256: Annotated[ str | None, @@ -88,6 +90,7 @@ class ReportingProfile(AdCPBaseModel): description='Digest of the exact canonicalization contract identified by canonicalization_id.', pattern='^[A-Fa-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -119,8 +122,10 @@ class ProducerIdentity(AdCPBaseModel): ) provider: Provider identity: Annotated[str, Field(max_length=512, min_length=1)] - cloud: reporting_dataset_share_destination.ReportingCloud | None = None - region: Annotated[str | None, Field(max_length=128, min_length=1)] = None + cloud: Annotated[reporting_dataset_share_destination.ReportingCloud | None, NoExplicitNull] = ( + None + ) + region: Annotated[str | None, Field(max_length=128, min_length=1), NoExplicitNull] = None class ReportingFeedPurpose(StrEnum): @@ -143,18 +148,23 @@ class Method(AdCPBaseModel): transport: Annotated[str, Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$')] orchestration: reporting_delivery_method.ReportingOrchestration destination_modes: Annotated[list[DestinationMode], Field(min_length=1)] - provider: Provider | None = None - format: Format | None = None + provider: Annotated[Provider | None, NoExplicitNull] = None + format: Annotated[Format | None, NoExplicitNull] = None access_mode: Annotated[ - str | None, Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$') + str | None, + Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$'), + NoExplicitNull, ] = None producer_identity: Annotated[ ProducerIdentity | None, Field( description='Seller principal a buyer grants access to for this exact buyer-hosted destination offering.' ), + NoExplicitNull, + ] = None + reader_compatibility: Annotated[ + list[reporting_resource.ReportingReaderCompatibilityItem] | None, NoExplicitNull ] = None - reader_compatibility: list[reporting_resource.ReportingReaderCompatibilityItem] | None = None class ReportingDeliveryOffering(AdCPBaseModel): @@ -218,4 +228,5 @@ class ReportingDeliveryOffering(AdCPBaseModel): Field( description='Managed delivery method for this offering. Omit for a Core (API-delivered) offering: rows flow through existing get_media_buy_delivery and reporting_webhook transports and no destination is involved. Present only when the seller advertises managed_delivery.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_file_entry.py b/src/adcp/types/domains/core/reporting_file_entry.py index e8e092cc5..6aa299b80 100644 --- a/src/adcp/types/domains/core/reporting_file_entry.py +++ b/src/adcp/types/domains/core/reporting_file_entry.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reporting_file_entry.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from . import reporting_file_object_ref, reporting_native_version_ref @@ -17,8 +17,10 @@ class ReportingFileEntry(AdCPBaseModel): extra='forbid', ) object_ref: reporting_file_object_ref.ReportingFileObjectReference - native_version_ref: reporting_native_version_ref.ReportingNativeVersionReference | None = None + native_version_ref: Annotated[ + reporting_native_version_ref.ReportingNativeVersionReference | None, NoExplicitNull + ] = None size_bytes: Annotated[SchemaInt, Field(ge=0)] sha256: Annotated[str, Field(pattern='^[A-Fa-f0-9]{64}$')] row_count: Annotated[SchemaInt, Field(ge=0)] - partition: Annotated[dict[str, str] | None, Field(max_length=32)] = None + partition: Annotated[dict[str, str] | None, Field(max_length=32), NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_ledger_changed_webhook.py b/src/adcp/types/domains/core/reporting_ledger_changed_webhook.py index 8976a6482..55fb958a4 100644 --- a/src/adcp/types/domains/core/reporting_ledger_changed_webhook.py +++ b/src/adcp/types/domains/core/reporting_ledger_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_ledger_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import reporting_finality @@ -49,16 +49,24 @@ class ReportingLedgerChangedWebhook(AdCPBaseModel): account_id: Annotated[str, Field(min_length=1)] change_kind: ChangeKind reporting_revision_id: Annotated[ - str | None, Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$') + str | None, + Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$'), + NoExplicitNull, ] = None supersedes_reporting_revision_id: Annotated[ - str | None, Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$') + str | None, + Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$'), + NoExplicitNull, ] = None - finality: reporting_finality.ReportingFinality | None = None + finality: Annotated[reporting_finality.ReportingFinality | None, NoExplicitNull] = None reporting_adjustment_id: Annotated[ - str | None, Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$') + str | None, + Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$'), + NoExplicitNull, ] = None adjusts_reporting_revision_id: Annotated[ - str | None, Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$') + str | None, + Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_materialization.py b/src/adcp/types/domains/core/reporting_materialization.py index 995f13884..85762a0c6 100644 --- a/src/adcp/types/domains/core/reporting_materialization.py +++ b/src/adcp/types/domains/core/reporting_materialization.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_materialization.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import reporting_delivery_offering, reporting_resource, reporting_verification @@ -66,7 +66,9 @@ class ReportingMaterialization(AdCPBaseModel): feed_purpose: reporting_delivery_offering.ReportingFeedPurpose method: Method transport: Annotated[ - str | None, Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$') + str | None, + Field(max_length=64, min_length=1, pattern='^[a-z][a-z0-9_.-]*$'), + NoExplicitNull, ] = None attempt: Annotated[SchemaInt, Field(ge=1)] status: Annotated[ @@ -78,8 +80,9 @@ class ReportingMaterialization(AdCPBaseModel): ready_at: Annotated[ AwareDatetime | None, Field(description='When consumer-path or destination verification completed.'), + NoExplicitNull, ] = None - failed_at: AwareDatetime | None = None + failed_at: Annotated[AwareDatetime | None, NoExplicitNull] = None failure_code: Annotated[ str | None, Field( @@ -88,7 +91,10 @@ class ReportingMaterialization(AdCPBaseModel): min_length=1, pattern='^[A-Z][A-Z0-9_]*$', ), + NoExplicitNull, ] = None - resource: reporting_resource.ReportingResource | None = None - verification: reporting_verification.ReportingVerification | None = None + resource: Annotated[reporting_resource.ReportingResource | None, NoExplicitNull] = None + verification: Annotated[reporting_verification.ReportingVerification | None, NoExplicitNull] = ( + None + ) created_at: AwareDatetime diff --git a/src/adcp/types/domains/core/reporting_obligation.py b/src/adcp/types/domains/core/reporting_obligation.py index bc2a9fa25..22993ed06 100644 --- a/src/adcp/types/domains/core/reporting_obligation.py +++ b/src/adcp/types/domains/core/reporting_obligation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_obligation.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from ..enums import reporting_finality, reporting_health @@ -91,6 +91,7 @@ class ReportingObligation(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None required_finality: reporting_finality.ReportingFinality reconciliation_mode: reporting_reconciliation_mode.ReportingReconciliationMode @@ -120,6 +121,7 @@ class ReportingObligation(AdCPBaseModel): description='Complete number of immutable authenticated consumer status statements associated with this obligation in the ledger snapshot, whether originally joined by reporting_obligation_id or by the exact configuration-generation, report-definition, and period key before the obligation existed. Core status-sync history is counted independently from Reconciled Billing receipts.', ge=0, ), + NoExplicitNull, ] = None current_consumer_status_id: Annotated[ str | None, @@ -129,6 +131,7 @@ class ReportingObligation(AdCPBaseModel): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None adjustment_count: Annotated[ SchemaInt | None, @@ -136,6 +139,7 @@ class ReportingObligation(AdCPBaseModel): description='Number of immutable post-official reporting adjustment records for this obligation in the consistent ledger snapshot.', ge=0, ), + NoExplicitNull, ] = None materialization_count: Annotated[ SchemaInt | None, @@ -143,6 +147,7 @@ class ReportingObligation(AdCPBaseModel): description="Number of materialization records for this obligation's revisions in the consistent ledger snapshot. Present iff the obligation is managed-delivery (destination_ref present).", ge=0, ), + NoExplicitNull, ] = None successful_materialization_count: Annotated[ SchemaInt | None, @@ -150,6 +155,7 @@ class ReportingObligation(AdCPBaseModel): description='Number of available/delivered verified materializations in the consistent ledger snapshot. Present iff the obligation is managed-delivery (destination_ref present).', ge=0, ), + NoExplicitNull, ] = None receipt_count: Annotated[ SchemaInt | None, @@ -157,6 +163,7 @@ class ReportingObligation(AdCPBaseModel): description='Complete number of authenticated receipts associated with this obligation in the ledger snapshot. Present iff reconciliation_mode is consumer_receipt.', ge=0, ), + NoExplicitNull, ] = None accepted_receipt_count: Annotated[ SchemaInt | None, @@ -164,6 +171,7 @@ class ReportingObligation(AdCPBaseModel): description='Number of accepted receipts. At most one current accepted receipt per consumer and revision contributes to reconciliation_status. Present iff reconciliation_mode is consumer_receipt.', ge=0, ), + NoExplicitNull, ] = None adjustment_receipt_count: Annotated[ SchemaInt | None, @@ -171,6 +179,7 @@ class ReportingObligation(AdCPBaseModel): description="Number of authenticated receipts for adjustments targeting this obligation's official revision. Reconciled Billing only.", ge=0, ), + NoExplicitNull, ] = None accepted_adjustment_receipt_count: Annotated[ SchemaInt | None, @@ -178,6 +187,7 @@ class ReportingObligation(AdCPBaseModel): description='Number of accepted adjustment receipts. Reconciled Billing buyers do not post an adjustment until their exact digest is accepted.', ge=0, ), + NoExplicitNull, ] = None pending_adjustment_count: Annotated[ SchemaInt | None, @@ -185,6 +195,7 @@ class ReportingObligation(AdCPBaseModel): description='Number of applicable adjustments without an accepted receipt. Reconciled Billing complete/healthy requires zero.', ge=0, ), + NoExplicitNull, ] = None issues: list[reporting_status_issue.ReportingStatusIssue] resource_retained_until: Annotated[ @@ -192,4 +203,5 @@ class ReportingObligation(AdCPBaseModel): Field( description='Minimum time through which at least one verified materialization for a completed obligation remains readable. Managed-delivery only; Core revisions are retained per status_retention_days and readable through the existing API transports.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_receipt.py b/src/adcp/types/domains/core/reporting_receipt.py index 4deba8528..17703e45c 100644 --- a/src/adcp/types/domains/core/reporting_receipt.py +++ b/src/adcp/types/domains/core/reporting_receipt.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_receipt.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -53,18 +53,21 @@ class ReportingReceipt(AdCPBaseModel): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None status: Status verification_profile: reporting_verification_profile.ReportingVerificationProfile observed_row_count: Annotated[SchemaInt, Field(ge=0)] observed_control_totals: list[reporting_control_total.ReportingControlTotal] - observed_canonical_content_digest: ( - reporting_canonical_content_digest.ReportingCanonicalContentDigest | None - ) = None - observed_manifest_sha256: Annotated[str | None, Field(pattern='^[A-Fa-f0-9]{64}$')] = None - observed_native_version_ref: ( - reporting_native_version_ref.ReportingNativeVersionReference | None - ) = None + observed_canonical_content_digest: Annotated[ + reporting_canonical_content_digest.ReportingCanonicalContentDigest | None, NoExplicitNull + ] = None + observed_manifest_sha256: Annotated[ + str | None, Field(pattern='^[A-Fa-f0-9]{64}$'), NoExplicitNull + ] = None + observed_native_version_ref: Annotated[ + reporting_native_version_ref.ReportingNativeVersionReference | None, NoExplicitNull + ] = None consumer_commit_ref: Annotated[ str | None, Field( @@ -72,7 +75,10 @@ class ReportingReceipt(AdCPBaseModel): max_length=512, min_length=1, ), + NoExplicitNull, ] = None - rejection_codes: Annotated[list[RejectionCode] | None, Field(min_length=1)] = None + rejection_codes: Annotated[list[RejectionCode] | None, Field(min_length=1), NoExplicitNull] = ( + None + ) observed_at: AwareDatetime - received_at: AwareDatetime | None = None + received_at: Annotated[AwareDatetime | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_reliability_statistics.py b/src/adcp/types/domains/core/reporting_reliability_statistics.py index 53b9e1e70..8faa6a66a 100644 --- a/src/adcp/types/domains/core/reporting_reliability_statistics.py +++ b/src/adcp/types/domains/core/reporting_reliability_statistics.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_reliability_statistics.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -34,8 +34,10 @@ class Evidence(AdCPBaseModel): ) basis: Basis generated_at: AwareDatetime - attestation_uri: AnyUrl | None = None - attestation_sha256: Annotated[str | None, Field(pattern='^[A-Fa-f0-9]{64}$')] = None + attestation_uri: Annotated[AnyUrl | None, NoExplicitNull] = None + attestation_sha256: Annotated[ + str | None, Field(pattern='^[A-Fa-f0-9]{64}$'), NoExplicitNull + ] = None class LatencyPercentiles(AdCPBaseModel): @@ -93,11 +95,13 @@ class ReportingReliabilityStatistics(AdCPBaseModel): Field( description='Observed seconds from official finalized_at to the first adjustment, required when official_revisions_adjusted is nonzero.' ), + NoExplicitNull, ] = None adjustment_magnitude: Annotated[ list[AdjustmentMagnitudeItem] | None, Field( description='Optional absolute adjustment-magnitude percentiles for comparable control totals such as spend.' ), + NoExplicitNull, ] = None evidence: Evidence diff --git a/src/adcp/types/domains/core/reporting_report_definition.py b/src/adcp/types/domains/core/reporting_report_definition.py index 29d118e15..527b9128e 100644 --- a/src/adcp/types/domains/core/reporting_report_definition.py +++ b/src/adcp/types/domains/core/reporting_report_definition.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_report_definition.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -59,7 +59,7 @@ class Metric(AdCPBaseModel): name: Annotated[str, Field(max_length=128, min_length=1)] source_expression: Annotated[str, Field(max_length=2048, min_length=1)] aggregation: Aggregation - unit: Annotated[str | None, Field(max_length=64, min_length=1)] = None + unit: Annotated[str | None, Field(max_length=64, min_length=1), NoExplicitNull] = None class Dimension(ScalarStr): @@ -150,7 +150,7 @@ class Calendar(AdCPBaseModel): extra='forbid', ) timezone_basis: ReportCalendarTimezoneBasis - timezone: Annotated[str | None, Field(max_length=255, min_length=1)] = None + timezone: Annotated[str | None, Field(max_length=255, min_length=1), NoExplicitNull] = None class ReportingReportDefinition(AdCPBaseModel): diff --git a/src/adcp/types/domains/core/reporting_resource.py b/src/adcp/types/domains/core/reporting_resource.py index 8cd4072f7..0393eaa3c 100644 --- a/src/adcp/types/domains/core/reporting_resource.py +++ b/src/adcp/types/domains/core/reporting_resource.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_resource.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -55,7 +55,9 @@ class ReportingResource(AdCPBaseModel): min_length=1, ), ] - native_version_ref: reporting_native_version_ref.ReportingNativeVersionReference | None = None + native_version_ref: Annotated[ + reporting_native_version_ref.ReportingNativeVersionReference | None, NoExplicitNull + ] = None manifest_version: Annotated[ Literal['1.0'], Field(description='Version of reporting-file-manifest.json used by a manifest resource.'), @@ -66,6 +68,7 @@ class ReportingResource(AdCPBaseModel): description='SHA-256 over the exact manifest bytes. Consumers verify this before parsing the manifest.', pattern='^[A-Fa-f0-9]{64}$', ), + NoExplicitNull, ] = None immutability: Annotated[ Immutability, @@ -82,4 +85,5 @@ class ReportingResource(AdCPBaseModel): Field( description='Reader features or format constraints required to consume this resource. Readiness verification MUST use a representative supported reader.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_revision.py b/src/adcp/types/domains/core/reporting_revision.py index fd3e216c6..3114f0d3a 100644 --- a/src/adcp/types/domains/core/reporting_revision.py +++ b/src/adcp/types/domains/core/reporting_revision.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_revision.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..enums import reporting_finality @@ -114,6 +114,7 @@ class ReportingRevision(AdCPBaseModel): Field( description='Why an official revision is considered final: an authoritative source signal, a versioned contractual cutoff, or a versioned stabilization rule.' ), + NoExplicitNull, ] = None finality_policy_id: Annotated[ str | None, @@ -123,12 +124,14 @@ class ReportingRevision(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None finalized_at: Annotated[ AwareDatetime | None, Field( description='When the producer applied the declared finality basis to this official revision.' ), + NoExplicitNull, ] = None observed_at: Annotated[ AwareDatetime, @@ -149,6 +152,7 @@ class ReportingRevision(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None row_count: Annotated[ SchemaInt, @@ -163,7 +167,7 @@ class ReportingRevision(AdCPBaseModel): description='Profile-defined totals computed from the canonical logical revision. Names MUST be unique.' ), ] - canonical_content_digest: ( - reporting_canonical_content_digest.ReportingCanonicalContentDigest | None - ) = None + canonical_content_digest: Annotated[ + reporting_canonical_content_digest.ReportingCanonicalContentDigest | None, NoExplicitNull + ] = None created_at: AwareDatetime diff --git a/src/adcp/types/domains/core/reporting_schedule.py b/src/adcp/types/domains/core/reporting_schedule.py index 8226a2ba3..28a05ae60 100644 --- a/src/adcp/types/domains/core/reporting_schedule.py +++ b/src/adcp/types/domains/core/reporting_schedule.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_schedule.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field @@ -36,6 +36,7 @@ class ReportingSchedule(AdCPBaseModel): Field( description='Required for billing_cycle alignment. This immutable instant anchors the recurring half-open billing periods so producer and consumer derive the same month, quarter, or other contractual cycle.' ), + NoExplicitNull, ] = None period_timezone: Annotated[ str | None, @@ -44,6 +45,7 @@ class ReportingSchedule(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None delivery_sla: Annotated[ str, diff --git a/src/adcp/types/domains/core/reporting_schedule_offering.py b/src/adcp/types/domains/core/reporting_schedule_offering.py index 26d0a633a..b52b0aced 100644 --- a/src/adcp/types/domains/core/reporting_schedule_offering.py +++ b/src/adcp/types/domains/core/reporting_schedule_offering.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_schedule_offering.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import reporting_schedule @@ -40,15 +40,19 @@ class ReportingScheduleOffering(AdCPBaseModel): Field( description='For billing_cycle only. fixed requires the advertised anchor and timezone; configurable lets each authorized account configuration select them.' ), + NoExplicitNull, ] = None period_timezone_policy: Annotated[ PeriodTimezonePolicy | None, Field( description="For source_timezone only. fixed advertises one exact upstream IANA timezone; account_resolved requires the seller to resolve and echo the account's upstream reporting timezone during configuration." ), + NoExplicitNull, ] = None - period_anchor: AwareDatetime | None = None - period_timezone: Annotated[str | None, Field(max_length=255, min_length=1)] = None + period_anchor: Annotated[AwareDatetime | None, NoExplicitNull] = None + period_timezone: Annotated[str | None, Field(max_length=255, min_length=1), NoExplicitNull] = ( + None + ) delivery_sla: Annotated[ str, Field( diff --git a/src/adcp/types/domains/core/reporting_status_changed_webhook.py b/src/adcp/types/domains/core/reporting_status_changed_webhook.py index 3378c62cc..cc98bb9d0 100644 --- a/src/adcp/types/domains/core/reporting_status_changed_webhook.py +++ b/src/adcp/types/domains/core/reporting_status_changed_webhook.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reporting_status_changed_webhook.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -67,17 +67,19 @@ class ReportingStatusChangedWebhook(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None health: Annotated[ reporting_health.ReportingHealth, Field(description='The health state after this transition.'), ] - previous_health: reporting_health.ReportingHealth | None = None + previous_health: Annotated[reporting_health.ReportingHealth | None, NoExplicitNull] = None issue_ids: Annotated[ list[IssueId] | None, Field( description='Stable identifiers of the open issues that caused or survived this transition, matching issues[].issue_id on get_reporting_status, so consumers can project AdCP reporting issues into durable work items. Empty or absent on recovery transitions.', max_length=16, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_status_issue.py b/src/adcp/types/domains/core/reporting_status_issue.py index 129fd2cbf..873a23820 100644 --- a/src/adcp/types/domains/core/reporting_status_issue.py +++ b/src/adcp/types/domains/core/reporting_status_issue.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_status_issue.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import reporting_coverage, reporting_delivery_offering @@ -80,12 +80,14 @@ class ReportingStatusIssue(AdCPBaseModel): Field( description='When the seller first observed this logical condition, carried unchanged across every re-emission until the issue is retired. It anchors the escalation clock advertised as consumer_mismatch_escalation_seconds and lets a consumer age an issue without keeping its own first-seen table. Required when code is CONSUMER_STATUS_MISMATCH.' ), + NoExplicitNull, ] = None issue_state: Annotated[ IssueState | None, Field( description='Optional seller-maintained lifecycle for this issue_id. open is the default when omitted. acknowledged means a human on responsible_party has taken it up but the condition persists. resolved means the underlying condition no longer holds; a recurrence uses a new issue_id. waived means the parties agreed off-protocol to disregard this exact issue even though its underlying condition may still hold. Only open and acknowledged issues appear in issues[]; retiring an issue removes it from the projection rather than publishing it at resolved or waived, so a reader that treats a nonempty issues[] as degradation stays correct. A CONSUMER_STATUS_MISMATCH waiver follows the bilateral, exact-scope requirements in consumer_mismatch_lifecycle.' ), + NoExplicitNull, ] = None external_ref: Annotated[ str | None, @@ -95,6 +97,7 @@ class ReportingStatusIssue(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,128}$', ), + NoExplicitNull, ] = None responsible_party: ResponsibleParty recommended_action: RecommendedAction @@ -104,9 +107,12 @@ class ReportingStatusIssue(AdCPBaseModel): description='Untrusted display text only. SDKs and agents dispatch exclusively on closed code/recommended_action values and never execute embedded links or instructions.', max_length=500, ), + NoExplicitNull, ] = None reporting_obligation_id: Annotated[ - str | None, Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$') + str | None, + Field(max_length=255, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$'), + NoExplicitNull, ] = None reporting_revision_id: Annotated[ str | None, @@ -116,6 +122,7 @@ class ReportingStatusIssue(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None reporting_status_id: Annotated[ str | None, @@ -125,18 +132,23 @@ class ReportingStatusIssue(AdCPBaseModel): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None delivery_config_id: Annotated[ - str | None, Field(max_length=64, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$') + str | None, + Field(max_length=64, min_length=1, pattern='^[A-Za-z0-9_.:-]{1,64}$'), + NoExplicitNull, + ] = None + delivery_config_version: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + feed_purpose: Annotated[ + reporting_delivery_offering.ReportingFeedPurpose | None, NoExplicitNull ] = None - delivery_config_version: Annotated[SchemaInt | None, Field(ge=1)] = None - feed_purpose: reporting_delivery_offering.ReportingFeedPurpose | None = None media_buy_ids: Annotated[ - list[reporting_coverage.ReportingMediaBuyId] | None, Field(min_length=1) + list[reporting_coverage.ReportingMediaBuyId] | None, Field(min_length=1), NoExplicitNull ] = None package_ids: Annotated[ - list[reporting_coverage.ReportingPackageId] | None, Field(min_length=1) + list[reporting_coverage.ReportingPackageId] | None, Field(min_length=1), NoExplicitNull ] = None - period_start: AwareDatetime | None = None - period_end: AwareDatetime | None = None - expected_at: AwareDatetime | None = None + period_start: Annotated[AwareDatetime | None, NoExplicitNull] = None + period_end: Annotated[AwareDatetime | None, NoExplicitNull] = None + expected_at: Annotated[AwareDatetime | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/reporting_verification.py b/src/adcp/types/domains/core/reporting_verification.py index d10af955d..2aad46853 100644 --- a/src/adcp/types/domains/core/reporting_verification.py +++ b/src/adcp/types/domains/core/reporting_verification.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_verification.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from . import ( @@ -86,19 +86,21 @@ class ReportingVerification(AdCPBaseModel): description='Profile-defined totals recomputed through verification_path. Names MUST be unique.' ), ] - canonical_content_digest: ( - reporting_canonical_content_digest.ReportingCanonicalContentDigest | None - ) = None + canonical_content_digest: Annotated[ + reporting_canonical_content_digest.ReportingCanonicalContentDigest | None, NoExplicitNull + ] = None physical_checksums: Annotated[ list[PhysicalChecksums | PhysicalChecksums1] | None, Field( description='Method-specific byte/object checksums. Different encodings of the same logical revision normally have different values.', min_length=1, ), + NoExplicitNull, ] = None native_commit_evidence: Annotated[ NativeCommitEvidence | None, Field( description='Provider-native immutable version evidence observed through the named consumer or destination path.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_webhook.py b/src/adcp/types/domains/core/reporting_webhook.py index 75a031218..58d23feac 100644 --- a/src/adcp/types/domains/core/reporting_webhook.py +++ b/src/adcp/types/domains/core/reporting_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/reporting_webhook.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import auth_scheme, available_metric @@ -51,6 +51,7 @@ class ReportingWebhook(AdCPBaseModel): description='Optional client-provided token for webhook validation. Echoed back in webhook payload to validate request authenticity.', min_length=16, ), + NoExplicitNull, ] = None authentication: Annotated[ Authentication, @@ -70,4 +71,5 @@ class ReportingWebhook(AdCPBaseModel): Field( description="Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included; an empty array has the same meaning as omission (it does not narrow to impressions and spend only). impressions and spend are always included regardless of this list. Must be a subset of the product's available_metrics. Subset evaluation and leaf resolution follow `enums/available-metric.json`: a numeric leaf may be covered by its container, while each structured distribution must be explicitly available. Requesting any nested identity selects its canonical carrier inside the payload's nested object. Same narrowing semantics as get_media_buy_delivery's requested_metrics (which additionally requires at least one entry when present)." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/reporting_write_destination.py b/src/adcp/types/domains/core/reporting_write_destination.py index e14248287..d6c58d90c 100644 --- a/src/adcp/types/domains/core/reporting_write_destination.py +++ b/src/adcp/types/domains/core/reporting_write_destination.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/reporting_write_destination.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -56,6 +56,7 @@ class ReportingWriteDestination2(AdCPBaseModel): min_length=1, pattern='^[a-z][a-z0-9_.-]*$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/representation_destination.py b/src/adcp/types/domains/core/representation_destination.py index b811381a9..e68ca9ac8 100644 --- a/src/adcp/types/domains/core/representation_destination.py +++ b/src/adcp/types/domains/core/representation_destination.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/representation_destination.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import daast_version, vast_version @@ -32,16 +32,19 @@ class RepresentationDestination(AdCPBaseModel): description='Optional placements the derived manifest must satisfy. Omission means every placement in the intended product route, not an unconstrained destination.', min_length=1, ), + NoExplicitNull, ] = None execution_vast_version: Annotated[ vast_version.VastVersion | None, Field( description='Exact VAST execution version selected for seller-assembled first-class VAST trackers when no sibling exact-version VAST document supplies it. Must be accepted by format_option and is preserved in selection lineage.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST execution version selected for seller-assembled first-class DAAST trackers when no sibling exact-version DAAST document supplies it. Must be accepted by format_option and is preserved in selection lineage.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/representation_rejection.py b/src/adcp/types/domains/core/representation_rejection.py index 8af9d760f..c25dc64aa 100644 --- a/src/adcp/types/domains/core/representation_rejection.py +++ b/src/adcp/types/domains/core/representation_rejection.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/representation_rejection.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -29,4 +29,4 @@ class RepresentationRejection(AdCPBaseModel): representation_id: Annotated[str, Field(min_length=1)] code: Code message: Annotated[str, Field(min_length=1)] - details: dict[str, Any] | None = None + details: Annotated[dict[str, Any] | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/representation_selection.py b/src/adcp/types/domains/core/representation_selection.py index 0778984a0..f7b92afd6 100644 --- a/src/adcp/types/domains/core/representation_selection.py +++ b/src/adcp/types/domains/core/representation_selection.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/representation_selection.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import daast_version, representation_selection_strategy, vast_version @@ -49,12 +49,14 @@ class RepresentationSelection(AdCPBaseModel): Field( description='Exact VAST version verified for seller-assembled first-class VAST trackers. This is selection lineage and part of the execution review identity; it is not included in selected_output_digest.' ), + NoExplicitNull, ] = None execution_daast_version: Annotated[ daast_version.DaastVersion | None, Field( description='Exact DAAST version verified for seller-assembled first-class DAAST trackers. This is selection lineage and part of the execution review identity; it is not included in selected_output_digest.' ), + NoExplicitNull, ] = None resolved_by: Annotated[ ResolvedBy, diff --git a/src/adcp/types/domains/core/requirements/audio_asset_requirements.py b/src/adcp/types/domains/core/requirements/audio_asset_requirements.py index 673dd0b39..f181e7fa0 100644 --- a/src/adcp/types/domains/core/requirements/audio_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/audio_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/audio_asset_requirements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictFloat @@ -35,38 +35,53 @@ class AudioAssetRequirements(AdCPBaseModel): extra='allow', ) min_duration_ms: Annotated[ - SchemaInt | None, Field(description='Minimum duration in milliseconds', ge=1) + SchemaInt | None, + Field(description='Minimum duration in milliseconds', ge=1), + NoExplicitNull, ] = None max_duration_ms: Annotated[ - SchemaInt | None, Field(description='Maximum duration in milliseconds', ge=1) + SchemaInt | None, + Field(description='Maximum duration in milliseconds', ge=1), + NoExplicitNull, + ] = None + formats: Annotated[ + list[Format] | None, Field(description='Accepted audio file formats'), NoExplicitNull ] = None - formats: Annotated[list[Format] | None, Field(description='Accepted audio file formats')] = None max_file_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1) + SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1), NoExplicitNull ] = None sample_rates: Annotated[ list[SampleRate] | None, Field(description='Accepted sample rates in Hz (e.g., [44100, 48000])'), + NoExplicitNull, ] = None channels: Annotated[ - list[Channel] | None, Field(description='Accepted audio channel configurations') + list[Channel] | None, + Field(description='Accepted audio channel configurations'), + NoExplicitNull, ] = None min_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Minimum audio bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Minimum audio bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None max_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Maximum audio bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Maximum audio bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None loudness_lufs: Annotated[ StrictFloat | None, Field( description='Target integrated loudness in LUFS. LKFS is the equivalent unit name under ITU-R BS.1770; values expressed in LUFS and LKFS are directly comparable without conversion.' ), + NoExplicitNull, ] = None loudness_tolerance_db: Annotated[ StrictFloat | None, Field(description='Acceptable deviation from the loudness_lufs target in dB.', ge=0.0), + NoExplicitNull, ] = None true_peak_dbfs: Annotated[ - StrictFloat | None, Field(description='Maximum true-peak level in dBFS.') + StrictFloat | None, Field(description='Maximum true-peak level in dBFS.'), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/core/requirements/catalog_field_binding.py b/src/adcp/types/domains/core/requirements/catalog_field_binding.py index c3d6bfcd6..94c2757bb 100644 --- a/src/adcp/types/domains/core/requirements/catalog_field_binding.py +++ b/src/adcp/types/domains/core/requirements/catalog_field_binding.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/catalog_field_binding.json -# timestamp: 2026-10-03T00:36:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from .. import ext as ext_1 @@ -29,7 +29,7 @@ class ScalarBinding(AdCPBaseModel): description="Dot-notation path to the field on the catalog item (e.g., 'name', 'price.amount', 'location.city')." ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AssetPoolBinding(AdCPBaseModel): @@ -49,7 +49,7 @@ class AssetPoolBinding(AdCPBaseModel): description="The asset_group_id on the catalog item's assets array to pull from (e.g., 'images_landscape', 'images_vertical', 'logo')." ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None PerItemBindings = Annotated[ @@ -79,8 +79,9 @@ class CatalogFieldBinding1(AdCPBaseModel): description='Scalar and asset pool bindings that apply within each repetition of the group. Nested catalog_group bindings are not permitted.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None CatalogFieldBinding = Annotated[ diff --git a/src/adcp/types/domains/core/requirements/catalog_requirements.py b/src/adcp/types/domains/core/requirements/catalog_requirements.py index 81471366d..86505c335 100644 --- a/src/adcp/types/domains/core/requirements/catalog_requirements.py +++ b/src/adcp/types/domains/core/requirements/catalog_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/catalog_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ...enums import catalog_type as catalog_type_1 @@ -27,6 +27,7 @@ class CatalogRequirements(AdCPBaseModel): Field( description='Whether this catalog type must be present. When true, creatives using this format must reference a synced catalog of this type.' ), + NoExplicitNull, ] = True min_items: Annotated[ SchemaInt | None, @@ -34,6 +35,7 @@ class CatalogRequirements(AdCPBaseModel): description='Minimum number of items the catalog must contain for this format to render properly (e.g., a carousel might require at least 3 products)', ge=1, ), + NoExplicitNull, ] = None max_items: Annotated[ SchemaInt | None, @@ -41,6 +43,7 @@ class CatalogRequirements(AdCPBaseModel): description='Maximum number of items the format can render. Items beyond this limit are ignored. Useful for fixed-slot layouts (e.g., a 3-product card) or feed-size constraints.', ge=1, ), + NoExplicitNull, ] = None required_fields: Annotated[ list[str] | None, @@ -48,6 +51,7 @@ class CatalogRequirements(AdCPBaseModel): description="Fields that must be present and non-empty on every item in the catalog. Field names are catalog-type-specific (e.g., 'title', 'price', 'image_url' for product catalogs; 'store_id', 'quantity' for inventory feeds).", min_length=1, ), + NoExplicitNull, ] = None feed_formats: Annotated[ list[feed_format.FeedFormat] | None, @@ -55,6 +59,7 @@ class CatalogRequirements(AdCPBaseModel): description='Accepted feed formats for this catalog type. When specified, the synced catalog must use one of these formats. When omitted, any format is accepted.', min_length=1, ), + NoExplicitNull, ] = None offering_asset_constraints: Annotated[ list[offering_asset_constraint.OfferingAssetConstraint] | None, @@ -62,6 +67,7 @@ class CatalogRequirements(AdCPBaseModel): description="Per-item creative asset requirements. Declares what asset groups (headlines, images, videos) each catalog item must provide in its assets array, along with count bounds and per-asset technical constraints. Applicable to 'offering' and all vertical catalog types (hotel, flight, job, etc.) whose items carry typed assets.", min_length=1, ), + NoExplicitNull, ] = None field_bindings: Annotated[ list[catalog_field_binding.CatalogFieldBinding] | None, @@ -69,4 +75,5 @@ class CatalogRequirements(AdCPBaseModel): description='Explicit mappings from format template slots to catalog item fields or typed asset pools. Optional — creative agents can infer mappings without them, but bindings make the relationship self-describing and enable validation. Covers scalar fields (asset_id → catalog_field), asset pools (asset_id → asset_group_id on the catalog item), and repeatable groups that iterate over catalog items.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/css_asset_requirements.py b/src/adcp/types/domains/core/requirements/css_asset_requirements.py index 96bdaef76..488eb60c1 100644 --- a/src/adcp/types/domains/core/requirements/css_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/css_asset_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/css_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -15,5 +15,5 @@ class CssAssetRequirements(AdCPBaseModel): extra='allow', ) max_file_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1) + SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1), NoExplicitNull ] = None diff --git a/src/adcp/types/domains/core/requirements/daast_asset_requirements.py b/src/adcp/types/domains/core/requirements/daast_asset_requirements.py index 1b591a10b..75d7da0ae 100644 --- a/src/adcp/types/domains/core/requirements/daast_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/daast_asset_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/daast_asset_requirements.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ...enums import daast_version as daast_version_1 @@ -23,8 +23,10 @@ class DaastAssetRequirements(AdCPBaseModel): deprecated=True, description='Deprecated one-element alias for `daast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None daast_versions: Annotated[ daast_tracker_constraints.DaastVersions | None, Field(description='Accepted DAAST version set for this asset requirement.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/html_asset_requirements.py b/src/adcp/types/domains/core/requirements/html_asset_requirements.py index 303810f4c..b2fd842fd 100644 --- a/src/adcp/types/domains/core/requirements/html_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/html_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/html_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -25,22 +25,26 @@ class HtmlAssetRequirements(AdCPBaseModel): max_file_size_kb: Annotated[ SchemaInt | None, Field(description='Maximum file size in kilobytes for the HTML asset', ge=1), + NoExplicitNull, ] = None sandbox: Annotated[ Sandbox | None, Field( description="Sandbox environment the HTML must be compatible with. 'none' = direct DOM access, 'iframe' = standard iframe isolation, 'safeframe' = IAB SafeFrame container, 'fencedframe' = Privacy Sandbox fenced frame" ), + NoExplicitNull, ] = Sandbox.none external_resources_allowed: Annotated[ StrictBool | None, Field( description='Whether the HTML creative can load external resources (scripts, images, fonts, etc.). When false, all resources must be inlined or bundled.' ), + NoExplicitNull, ] = None allowed_external_domains: Annotated[ list[str] | None, Field( description='List of domains the HTML creative may reference for external resources. Only applicable when external_resources_allowed is true.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/image_asset_requirements.py b/src/adcp/types/domains/core/requirements/image_asset_requirements.py index f0a643f4e..9ea7e77d0 100644 --- a/src/adcp/types/domains/core/requirements/image_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/image_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/image_asset_requirements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -65,6 +65,7 @@ class ImageAssetRequirements(AdCPBaseModel): description='Minimum width. Interpretation depends on unit (default: pixels). For exact dimensions, set min_width = max_width.', gt=0.0, ), + NoExplicitNull, ] = None max_width: Annotated[ StrictFloat | None, @@ -72,6 +73,7 @@ class ImageAssetRequirements(AdCPBaseModel): description='Maximum width. Interpretation depends on unit (default: pixels). For exact dimensions, set min_width = max_width.', gt=0.0, ), + NoExplicitNull, ] = None min_height: Annotated[ StrictFloat | None, @@ -79,6 +81,7 @@ class ImageAssetRequirements(AdCPBaseModel): description='Minimum height. Interpretation depends on unit (default: pixels). For exact dimensions, set min_height = max_height.', gt=0.0, ), + NoExplicitNull, ] = None max_height: Annotated[ StrictFloat | None, @@ -86,6 +89,7 @@ class ImageAssetRequirements(AdCPBaseModel): description='Maximum height. Interpretation depends on unit (default: pixels). For exact dimensions, set min_height = max_height.', gt=0.0, ), + NoExplicitNull, ] = None pixel_ratios: Annotated[ list[PixelRatio] | None, @@ -93,18 +97,21 @@ class ImageAssetRequirements(AdCPBaseModel): description='Accepted intrinsic-pixel densities for this image slot. One submitted image matching any listed ratio satisfies the slot; the array does not require one asset per ratio. Absence imposes no density constraint beyond the existing dimension requirements.', min_length=1, ), + NoExplicitNull, ] = None parameters_from_format_id: Annotated[ StrictBool | None, Field( description="When true on a legacy template format, logical width, logical height, and pixel ratio are supplied by the parameterized format_id. The image asset's intrinsic dimensions MUST equal logical dimensions multiplied by pixel_ratio (which defaults to 1 when omitted)." ), + NoExplicitNull, ] = None unit: Annotated[ dimension_unit.DimensionUnit | None, Field( description="Unit of measurement for width/height values. Defaults to 'px' when absent. Print formats use 'inches' or 'cm'." ), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -112,36 +119,45 @@ class ImageAssetRequirements(AdCPBaseModel): description="Required aspect ratio (e.g., '16:9', '1:1', '1.91:1')", pattern='^\\d+(\\.\\d+)?:\\d+(\\.\\d+)?$', ), + NoExplicitNull, + ] = None + formats: Annotated[ + list[Format] | None, Field(description='Accepted image file formats'), NoExplicitNull ] = None - formats: Annotated[list[Format] | None, Field(description='Accepted image file formats')] = None min_dpi: Annotated[ SchemaInt | None, Field( description='Minimum resolution in dots per inch. Always in DPI regardless of the dimension unit. Standard print requires 300 DPI, newspaper 150 DPI.', ge=1, ), + NoExplicitNull, ] = None bleed: Annotated[ Bleed | Bleed1 | None, Field( description='Required bleed area beyond the trim size. The submitted image must be larger than the declared dimensions: total width = trim width + left bleed + right bleed, total height = trim height + top bleed + bottom bleed. For uniform bleed: total = trim + (2 * uniform). Uses the same unit as the parent dimensions.' ), + NoExplicitNull, ] = None color_space: Annotated[ - ColorSpace | None, Field(description='Required color space. Print typically requires CMYK.') + ColorSpace | None, + Field(description='Required color space. Print typically requires CMYK.'), + NoExplicitNull, ] = None max_file_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1) + SchemaInt | None, Field(description='Maximum file size in kilobytes', ge=1), NoExplicitNull ] = None transparency_required: Annotated[ StrictBool | None, Field( description='Whether the image must support transparency (requires PNG, WebP, or GIF)' ), + NoExplicitNull, ] = None animation_allowed: Annotated[ StrictBool | None, Field(description='Whether animated images (GIF, animated WebP) are accepted'), + NoExplicitNull, ] = None max_animation_duration_ms: Annotated[ SchemaInt | None, @@ -149,6 +165,7 @@ class ImageAssetRequirements(AdCPBaseModel): description='Maximum animation duration in milliseconds (if animation_allowed is true)', ge=0, ), + NoExplicitNull, ] = None max_weight_grams: Annotated[ SchemaInt | None, @@ -156,4 +173,5 @@ class ImageAssetRequirements(AdCPBaseModel): description='Maximum weight in grams for the finished physical piece (print inserts, flyers). Affects postage calculations and production constraints. Only applicable to print channels.', gt=0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/javascript_asset_requirements.py b/src/adcp/types/domains/core/requirements/javascript_asset_requirements.py index a2757e7e9..f0f6a5efe 100644 --- a/src/adcp/types/domains/core/requirements/javascript_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/javascript_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/javascript_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -24,23 +24,29 @@ class JavascriptAssetRequirements(AdCPBaseModel): max_file_size_kb: Annotated[ SchemaInt | None, Field(description='Maximum file size in kilobytes for the JavaScript asset', ge=1), + NoExplicitNull, ] = None module_type: Annotated[ ModuleType | None, Field( description="Required JavaScript module format. 'script' = classic script, 'module' = ES modules, 'iife' = immediately invoked function expression" ), + NoExplicitNull, ] = None strict_mode_required: Annotated[ - StrictBool | None, Field(description='Whether the JavaScript must use strict mode') + StrictBool | None, + Field(description='Whether the JavaScript must use strict mode'), + NoExplicitNull, ] = None external_resources_allowed: Annotated[ StrictBool | None, Field(description='Whether the JavaScript can load external resources dynamically'), + NoExplicitNull, ] = None allowed_external_domains: Annotated[ list[str] | None, Field( description='List of domains the JavaScript may reference for external resources. Only applicable when external_resources_allowed is true.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/markdown_asset_requirements.py b/src/adcp/types/domains/core/requirements/markdown_asset_requirements.py index ff057afdc..d842d30b1 100644 --- a/src/adcp/types/domains/core/requirements/markdown_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/markdown_asset_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/markdown_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -14,6 +14,6 @@ class MarkdownAssetRequirements(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - max_length: Annotated[SchemaInt | None, Field(description='Maximum character length', ge=1)] = ( - None - ) + max_length: Annotated[ + SchemaInt | None, Field(description='Maximum character length', ge=1), NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/core/requirements/offering_asset_constraint.py b/src/adcp/types/domains/core/requirements/offering_asset_constraint.py index 752c31af6..f9392afeb 100644 --- a/src/adcp/types/domains/core/requirements/offering_asset_constraint.py +++ b/src/adcp/types/domains/core/requirements/offering_asset_constraint.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/offering_asset_constraint.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ...enums import asset_content_type @@ -33,17 +33,23 @@ class OfferingAssetConstraint(AdCPBaseModel): Field( description='Whether this asset group must be present in each offering. Defaults to true.' ), + NoExplicitNull, ] = True min_count: Annotated[ - SchemaInt | None, Field(description='Minimum number of items required in this group.', ge=1) + SchemaInt | None, + Field(description='Minimum number of items required in this group.', ge=1), + NoExplicitNull, ] = None max_count: Annotated[ - SchemaInt | None, Field(description='Maximum number of items allowed in this group.', ge=1) + SchemaInt | None, + Field(description='Maximum number of items allowed in this group.', ge=1), + NoExplicitNull, ] = None asset_requirements: Annotated[ asset_requirements_1.AssetRequirements | None, Field( description='Technical requirements for each item in this group (e.g., max_length for text, min_width/aspect_ratio for images). Applies uniformly to all items in the group.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/requirements/text_asset_requirements.py b/src/adcp/types/domains/core/requirements/text_asset_requirements.py index 9ae90b27e..071a7965e 100644 --- a/src/adcp/types/domains/core/requirements/text_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/text_asset_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/text_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -14,26 +14,27 @@ class TextAssetRequirements(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - min_length: Annotated[SchemaInt | None, Field(description='Minimum character length', ge=0)] = ( - None - ) - max_length: Annotated[SchemaInt | None, Field(description='Maximum character length', ge=1)] = ( - None - ) - min_lines: Annotated[SchemaInt | None, Field(description='Minimum number of lines', ge=1)] = ( - None - ) - max_lines: Annotated[SchemaInt | None, Field(description='Maximum number of lines', ge=1)] = ( - None - ) + min_length: Annotated[ + SchemaInt | None, Field(description='Minimum character length', ge=0), NoExplicitNull + ] = None + max_length: Annotated[ + SchemaInt | None, Field(description='Maximum character length', ge=1), NoExplicitNull + ] = None + min_lines: Annotated[ + SchemaInt | None, Field(description='Minimum number of lines', ge=1), NoExplicitNull + ] = None + max_lines: Annotated[ + SchemaInt | None, Field(description='Maximum number of lines', ge=1), NoExplicitNull + ] = None character_pattern: Annotated[ str | None, Field( description="Regex pattern defining allowed characters (e.g., '^[a-zA-Z0-9 .,!?-]+$')" ), + NoExplicitNull, ] = None prohibited_terms: Annotated[ - list[str] | None, Field(description='List of prohibited words or phrases') + list[str] | None, Field(description='List of prohibited words or phrases'), NoExplicitNull ] = None allowed_values: Annotated[ list[str] | None, @@ -41,4 +42,5 @@ class TextAssetRequirements(AdCPBaseModel): description='Closed set of permitted string values for this text slot. When present, a conformant implementation MUST reject any submitted content not in this list with `CREATIVE_VALUE_NOT_ALLOWED` (echoing the offending field path in `error.field` and the allowed list in `error.details.allowed_values`). Matching is case-sensitive; producers MUST supply exact casing. If the submitted value contains unresolved template tokens (e.g., {{product_name}}), validation against allowed_values MUST be deferred until interpolation is complete. All declared constraints (allowed_values, character_pattern, max_length, etc.) are conjunctive — submitted content must satisfy every applicable constraint simultaneously.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/url_asset_requirements.py b/src/adcp/types/domains/core/requirements/url_asset_requirements.py index 32f541574..4b1c06daf 100644 --- a/src/adcp/types/domains/core/requirements/url_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/url_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/url_asset_requirements.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -34,20 +34,25 @@ class UrlAssetRequirements(AdCPBaseModel): Field( description="Purpose this URL slot serves in the format — distinct from `url_type` on the manifest-side asset (which declares the receiver's invocation mechanism). A slot can be `click_tracker` (purpose) and accept a `tracker_pixel` (mechanism) URL, or `clickthrough` (purpose) and accept a `clickthrough` (mechanism) URL. Complements `asset_role` (human-readable label) by providing a machine-readable enum and serves as the receiver's fallback signal when a manifest URL asset omits `url_type`." ), + NoExplicitNull, ] = None protocols: Annotated[ list[Protocol] | None, Field(description='Allowed URL protocols. HTTPS is recommended for all ad URLs.'), + NoExplicitNull, ] = None allowed_domains: Annotated[ - list[str] | None, Field(description='List of allowed domains for the URL') + list[str] | None, Field(description='List of allowed domains for the URL'), NoExplicitNull ] = None max_length: Annotated[ - SchemaInt | None, Field(description='Maximum URL length in characters', ge=1) + SchemaInt | None, + Field(description='Maximum URL length in characters', ge=1), + NoExplicitNull, ] = None macro_support: Annotated[ StrictBool | None, Field( description="Coarse slot gate for macro-bearing URLs. `false` is a hard prohibition on macro tokens. `true` permits tokens but does not prove any dialect semantic, resolver, context, or encoding capability; exact support comes from the selected format option's macro_resolution_capabilities." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/vast_asset_requirements.py b/src/adcp/types/domains/core/requirements/vast_asset_requirements.py index f778813a2..23e7d5d89 100644 --- a/src/adcp/types/domains/core/requirements/vast_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/vast_asset_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/requirements/vast_asset_requirements.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ...enums import vast_version as vast_version_1 @@ -23,14 +23,17 @@ class VastAssetRequirements(AdCPBaseModel): deprecated=True, description='Deprecated one-element alias for `vast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None vast_versions: Annotated[ list[vast_version_1.VastVersion] | None, Field(description='Accepted VAST version set for this asset requirement.', min_length=1), + NoExplicitNull, ] = None media_file_requirements: Annotated[ vast_media_file_requirements.VastMediafileRequirements | None, Field( description='Technical acceptance constraints for the alternative MediaFile renditions in each applicable resolved InLine linear creative.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/video_asset_requirements.py b/src/adcp/types/domains/core/requirements/video_asset_requirements.py index 9bb6b1477..730be18fa 100644 --- a/src/adcp/types/domains/core/requirements/video_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/video_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/video_asset_requirements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat, ScalarInt from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -60,67 +60,89 @@ class VideoAssetRequirements(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - min_width: Annotated[SchemaInt | None, Field(description='Minimum width in pixels', ge=1)] = ( - None - ) - max_width: Annotated[SchemaInt | None, Field(description='Maximum width in pixels', ge=1)] = ( - None - ) - min_height: Annotated[SchemaInt | None, Field(description='Minimum height in pixels', ge=1)] = ( - None - ) - max_height: Annotated[SchemaInt | None, Field(description='Maximum height in pixels', ge=1)] = ( - None - ) + min_width: Annotated[ + SchemaInt | None, Field(description='Minimum width in pixels', ge=1), NoExplicitNull + ] = None + max_width: Annotated[ + SchemaInt | None, Field(description='Maximum width in pixels', ge=1), NoExplicitNull + ] = None + min_height: Annotated[ + SchemaInt | None, Field(description='Minimum height in pixels', ge=1), NoExplicitNull + ] = None + max_height: Annotated[ + SchemaInt | None, Field(description='Maximum height in pixels', ge=1), NoExplicitNull + ] = None aspect_ratio: Annotated[ str | None, Field(description="Required aspect ratio (e.g., '16:9', '9:16')", pattern='^\\d+:\\d+$'), + NoExplicitNull, ] = None min_duration_ms: Annotated[ - SchemaInt | None, Field(description='Minimum duration in milliseconds', ge=1) + SchemaInt | None, + Field(description='Minimum duration in milliseconds', ge=1), + NoExplicitNull, ] = None max_duration_ms: Annotated[ - SchemaInt | None, Field(description='Maximum duration in milliseconds', ge=1) + SchemaInt | None, + Field(description='Maximum duration in milliseconds', ge=1), + NoExplicitNull, ] = None containers: Annotated[ - list[Container] | None, Field(description='Accepted video container formats') + list[Container] | None, + Field(description='Accepted video container formats'), + NoExplicitNull, + ] = None + codecs: Annotated[ + list[Codec] | None, Field(description='Accepted video codecs'), NoExplicitNull ] = None - codecs: Annotated[list[Codec] | None, Field(description='Accepted video codecs')] = None max_file_size_kb: Annotated[ SchemaInt | None, Field(description='Maximum file size, where 1 KB is exactly 1,000 bytes', ge=1), + NoExplicitNull, ] = None min_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Minimum video bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Minimum video bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None max_bitrate_kbps: Annotated[ - SchemaInt | None, Field(description='Maximum video bitrate in kilobits per second', ge=1) + SchemaInt | None, + Field(description='Maximum video bitrate in kilobits per second', ge=1), + NoExplicitNull, ] = None frame_rates: Annotated[ list[FrameRate] | None, Field(description='Accepted frame rates in frames per second (e.g., [24, 30, 60])'), + NoExplicitNull, ] = None audio_required: Annotated[ - StrictBool | None, Field(description='Whether the video must include an audio track') + StrictBool | None, + Field(description='Whether the video must include an audio track'), + NoExplicitNull, ] = None frame_rate_type: Annotated[ frame_rate_type_1.FrameRateType | None, Field( description='Required frame rate type. Broadcast and SSAI require constant frame rate for seamless splicing.' ), + NoExplicitNull, ] = None scan_type: Annotated[ scan_type_1.ScanType | None, Field(description='Required scan type. Modern delivery requires progressive scan.'), + NoExplicitNull, ] = None gop_type: Annotated[ gop_type_1.GopType | None, Field( description='Required GOP structure. SSAI and broadcast require closed GOPs for clean splice points.' ), + NoExplicitNull, ] = None min_gop_interval_seconds: Annotated[ - StrictFloat | None, Field(description='Minimum keyframe interval in seconds', ge=0.0) + StrictFloat | None, + Field(description='Minimum keyframe interval in seconds', ge=0.0), + NoExplicitNull, ] = None max_gop_interval_seconds: Annotated[ StrictFloat | None, @@ -128,30 +150,36 @@ class VideoAssetRequirements(AdCPBaseModel): description='Maximum keyframe interval in seconds. SSAI typically requires 1-2 second intervals.', ge=0.0, ), + NoExplicitNull, ] = None moov_atom_position: Annotated[ moov_atom_position_1.MoovAtomPosition | None, Field( description="Required moov atom position in MP4 container. 'start' enables progressive download without buffering the entire file." ), + NoExplicitNull, ] = None audio_codecs: Annotated[ list[AudioCodec] | None, Field(description="Accepted audio codecs (e.g., ['aac', 'pcm', 'ac3'])"), + NoExplicitNull, ] = None audio_sample_rates: Annotated[ list[AudioSampleRate] | None, Field(description='Accepted audio sample rates in Hz (e.g., [44100, 48000])'), + NoExplicitNull, ] = None audio_channels: Annotated[ list[audio_channel_layout.AudioChannelLayout] | None, Field(description='Accepted audio channel configurations'), + NoExplicitNull, ] = None loudness_lufs: Annotated[ StrictFloat | None, Field( description='Target integrated loudness in LUFS (e.g., -24 for broadcast, -16 for streaming)' ), + NoExplicitNull, ] = None loudness_tolerance_db: Annotated[ StrictFloat | None, @@ -159,8 +187,10 @@ class VideoAssetRequirements(AdCPBaseModel): description='Acceptable deviation from loudness_lufs target in dB (e.g., 2 means -22 to -26 LUFS for a -24 target)', ge=0.0, ), + NoExplicitNull, ] = None true_peak_dbfs: Annotated[ StrictFloat | None, Field(description='Maximum true peak level in dBFS (e.g., -2 for broadcast)'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/requirements/webhook_asset_requirements.py b/src/adcp/types/domains/core/requirements/webhook_asset_requirements.py index bd1e92bab..9095cc63c 100644 --- a/src/adcp/types/domains/core/requirements/webhook_asset_requirements.py +++ b/src/adcp/types/domains/core/requirements/webhook_asset_requirements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/requirements/webhook_asset_requirements.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -20,4 +20,6 @@ class WebhookAssetRequirements(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - methods: Annotated[list[Method] | None, Field(description='Allowed HTTP methods')] = None + methods: Annotated[ + list[Method] | None, Field(description='Allowed HTTP methods'), NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/core/response.py b/src/adcp/types/domains/core/response.py index 4898fb1a2..9ec163c18 100644 --- a/src/adcp/types/domains/core/response.py +++ b/src/adcp/types/domains/core/response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/response.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -20,6 +20,7 @@ class ProtocolResponse(AdCPBaseModel): Field( description='Transport-managed conversation identifier. Maps to native contextId on A2A; compatibility metadata only on MCP and not a continuation or authorization mechanism.' ), + NoExplicitNull, ] = None data: Annotated[ Any | None, diff --git a/src/adcp/types/domains/core/rights_attestation_evaluation.py b/src/adcp/types/domains/core/rights_attestation_evaluation.py index ebd0bc596..74c7452d4 100644 --- a/src/adcp/types/domains/core/rights_attestation_evaluation.py +++ b/src/adcp/types/domains/core/rights_attestation_evaluation.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/rights_attestation_evaluation.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from . import brand_ref @@ -24,7 +24,7 @@ class Subject(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject19(AdCPBaseModel): @@ -59,7 +59,7 @@ class Subject19(AdCPBaseModel): pattern='^sha256:[a-f0-9]{64}$', ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ActionBinding(AdCPBaseModel): @@ -106,7 +106,7 @@ class Issuer(AdCPBaseModel): Literal['brand'], Field(description='The issuer is identified by an AdCP BrandRef.') ] = 'brand' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Issuer5(AdCPBaseModel): @@ -126,7 +126,7 @@ class Issuer5(AdCPBaseModel): description='Canonical HTTPS endpoint of the issuing agent. Evaluators compare it using AdCP URL canonicalization rules.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Issuer6(AdCPBaseModel): @@ -146,7 +146,7 @@ class Issuer6(AdCPBaseModel): description='Canonical HTTPS origin with no path, query, fragment, or userinfo. This identifies the issuer; it does not authorize a fetch.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject10(AdCPBaseModel): @@ -156,7 +156,7 @@ class Subject10(AdCPBaseModel): type: Literal['brand'] = 'brand' resource_type: Literal['https://adcontextprotocol.org/claims/subjects/rights-grant'] = 'https://adcontextprotocol.org/claims/subjects/rights-grant' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Reference(AttestationReference): @@ -175,6 +175,7 @@ class Reference(AttestationReference): ], title='Attestation Issuer', ), + NoExplicitNull, ] = None claim_type: Annotated[ Literal['https://adcontextprotocol.org/claims/rights/grant'], @@ -201,6 +202,7 @@ class Reference(AttestationReference): ], title='Attestation Subject', ), + NoExplicitNull, ] = None @@ -224,4 +226,4 @@ class RightsAttestationEvaluation(AdCPBaseModel): ] reference: Reference evaluation: Evaluation - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/rights_constraint.py b/src/adcp/types/domains/core/rights_constraint.py index d5838dd94..6f335c198 100644 --- a/src/adcp/types/domains/core/rights_constraint.py +++ b/src/adcp/types/domains/core/rights_constraint.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/rights_constraint.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool @@ -54,7 +54,7 @@ class Disclosure(AdCPBaseModel): extra='forbid', ) required: StrictBool - text: Annotated[str | None, Field(min_length=1)] = None + text: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class Subject(AdCPBaseModel): @@ -66,7 +66,7 @@ class Subject(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject29(AdCPBaseModel): @@ -101,7 +101,7 @@ class Subject29(AdCPBaseModel): pattern='^sha256:[a-f0-9]{64}$', ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Issuer(AdCPBaseModel): @@ -112,7 +112,7 @@ class Issuer(AdCPBaseModel): Literal['brand'], Field(description='The issuer is identified by an AdCP BrandRef.') ] = 'brand' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Issuer8(AdCPBaseModel): @@ -132,7 +132,7 @@ class Issuer8(AdCPBaseModel): description='Canonical HTTPS endpoint of the issuing agent. Evaluators compare it using AdCP URL canonicalization rules.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Issuer9(AdCPBaseModel): @@ -152,7 +152,7 @@ class Issuer9(AdCPBaseModel): description='Canonical HTTPS origin with no path, query, fragment, or userinfo. This identifies the issuer; it does not authorize a fetch.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject20(AdCPBaseModel): @@ -162,7 +162,7 @@ class Subject20(AdCPBaseModel): type: Literal['brand'] = 'brand' resource_type: Literal['https://adcontextprotocol.org/claims/subjects/rights-grant'] = 'https://adcontextprotocol.org/claims/subjects/rights-grant' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class AttestationRef(AttestationReference): @@ -181,6 +181,7 @@ class AttestationRef(AttestationReference): ], title='Attestation Issuer', ), + NoExplicitNull, ] = None claim_type: Annotated[ Literal['https://adcontextprotocol.org/claims/rights/grant'], @@ -207,6 +208,7 @@ class AttestationRef(AttestationReference): ], title='Attestation Subject', ), + NoExplicitNull, ] = None @@ -223,15 +225,19 @@ class RightsConstraint(AdCPBaseModel): Field( description='Canonical BrandRef for the rights holder that issued the grant through an authorized rights agent. Required whenever attestation_refs is present. Evaluators first match this exact BrandRef against local issuer policy, then resolve the authoritative brand.json and bind the credential verification key to exactly one matching agents[] entry with type rights. Sibling-brand agents are never authority. Legacy unattested constraints may omit this field but remain machine-unverified.' ), + NoExplicitNull, ] = None valid_from: Annotated[ - AwareDatetime | None, Field(description='Start of the rights validity period') + AwareDatetime | None, + Field(description='Start of the rights validity period'), + NoExplicitNull, ] = None valid_until: Annotated[ AwareDatetime | None, Field( description='End of the rights validity period. Creative should not be served after this time.' ), + NoExplicitNull, ] = None uses: Annotated[ list[right_use.RightUse], @@ -242,12 +248,14 @@ class RightsConstraint(AdCPBaseModel): Field( description='Countries where this creative may be served under these rights (ISO 3166-1 alpha-2). If omitted, no country restriction. When both countries and excluded_countries are present, the effective set is countries minus excluded_countries.' ), + NoExplicitNull, ] = None excluded_countries: Annotated[ list[ExcludedCountry] | None, Field( description='Countries excluded from rights availability (ISO 3166-1 alpha-2). Use when the grant is worldwide except specific markets.' ), + NoExplicitNull, ] = None impression_cap: Annotated[ SchemaInt | None, @@ -255,24 +263,28 @@ class RightsConstraint(AdCPBaseModel): description='Maximum total impressions allowed for the full validity period (valid_from to valid_until). This is the absolute cap across all creatives using this rights grant, not a per-creative or per-period limit.', ge=1, ), + NoExplicitNull, ] = None right_type: Annotated[ right_type_1.RightType | None, Field( description='Type of rights (talent, music, etc.). Helps identify constraints when a creative combines multiple rights types.' ), + NoExplicitNull, ] = None approval_status: Annotated[ ApprovalStatus | None, Field( description='Approval status from the rights holder at manifest creation time (snapshot, not a live value)' ), + NoExplicitNull, ] = None grant_status: Annotated[ GrantStatus | None, Field( description='Authoritative lifecycle state bound into the grant digest and credential status evidence. Only active can evaluate to verified. paused and revoked credentials are ineligible for serving; resumption issues a fresh active credential and reference.' ), + NoExplicitNull, ] = None restrictions: Annotated[ list[Restriction] | None, @@ -280,18 +292,21 @@ class RightsConstraint(AdCPBaseModel): description='Normalized enforceable content and usage restrictions issued with the grant. When present these values are part of content_digest and cannot be omitted or changed without invalidating the attestation.', min_length=1, ), + NoExplicitNull, ] = None disclosure: Annotated[ Disclosure | None, Field( description='Disclosure obligation issued with the grant and bound by content_digest.' ), + NoExplicitNull, ] = None creative_approval_required: Annotated[ StrictBool | None, Field( description='Whether each creative produced under this grant requires holder approval before distribution. This enforceable term is bound by content_digest.' ), + NoExplicitNull, ] = None verification_url: Annotated[ AnyUrl | None, @@ -299,6 +314,7 @@ class RightsConstraint(AdCPBaseModel): deprecated=True, description='DEPRECATED legacy informational locator. It is not proof of grant status, a revocation source, a credential locator, or permission to make a network request. Receivers MUST NOT fetch it during rights evaluation, and HTTP status codes have no AdCP authorization meaning. Rights authorization uses attestation_refs and a verifier-of-record evaluation under evaluator-owned policy.', ), + NoExplicitNull, ] = None content_digest: Annotated[ str | None, @@ -306,6 +322,7 @@ class RightsConstraint(AdCPBaseModel): description='SHA-256 of UTF8(JCS(rights_constraint minus content_digest, attestation_refs, and verification_url)), formatted as sha256:. This binds all serving-relevant grant fields, including future extension fields, while deliberately excluding the legacy non-authoritative URL.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None attestation_refs: Annotated[ list[AttestationRef] | None, @@ -314,5 +331,6 @@ class RightsConstraint(AdCPBaseModel): max_length=4, min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/seller_agent_ref.py b/src/adcp/types/domains/core/seller_agent_ref.py index a7cf517a5..c1b0e0e1a 100644 --- a/src/adcp/types/domains/core/seller_agent_ref.py +++ b/src/adcp/types/domains/core/seller_agent_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/seller_agent_ref.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -27,4 +27,5 @@ class SellerAgentReference(AdCPBaseModel): min_length=1, pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_coverage_forecast.py b/src/adcp/types/domains/core/signal_coverage_forecast.py index 41a01b7c3..5a1b4ea92 100644 --- a/src/adcp/types/domains/core/signal_coverage_forecast.py +++ b/src/adcp/types/domains/core/signal_coverage_forecast.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_coverage_forecast.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Dict, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat @@ -21,14 +21,17 @@ class CoverageRate(forecast_point.CoverageRate): low: Annotated[ StrictFloat | None, Field(description='Conservative (low-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None mid: Annotated[ StrictFloat | None, Field(description='Expected (most likely) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None high: Annotated[ StrictFloat | None, Field(description='Optimistic (high-end) forecast value', ge=0.0, le=1.0), + NoExplicitNull, ] = None @@ -37,22 +40,22 @@ class Metrics(forecast_point.Metrics): model_config = ConfigDict( extra='allow', ) - audience_size: forecast_range.ForecastRange | None = None - reach: forecast_range.ForecastRange | None = None - frequency: forecast_range.ForecastRange | None = None - impressions: forecast_range.ForecastRange | None = None - clicks: forecast_range.ForecastRange | None = None - spend: forecast_range.ForecastRange | None = None - views: forecast_range.ForecastRange | None = None - completed_views: forecast_range.ForecastRange | None = None - grps: forecast_range.ForecastRange | None = None - engagements: forecast_range.ForecastRange | None = None - follows: forecast_range.ForecastRange | None = None - saves: forecast_range.ForecastRange | None = None - profile_visits: forecast_range.ForecastRange | None = None - measured_impressions: forecast_range.ForecastRange | None = None - downloads: forecast_range.ForecastRange | None = None - plays: forecast_range.ForecastRange | None = None + audience_size: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + reach: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + frequency: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + clicks: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + spend: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + views: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + completed_views: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + grps: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + engagements: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + follows: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + saves: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + profile_visits: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + measured_impressions: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + downloads: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None + plays: Annotated[forecast_range.ForecastRange | None, NoExplicitNull] = None coverage_rate: Annotated[ CoverageRate, Field( @@ -87,7 +90,7 @@ class Scope(AdCPBaseModel): ), ] product_id: Annotated[ - str | None, Field(description="Product denominator when kind is 'product'.") + str | None, Field(description="Product denominator when kind is 'product'."), NoExplicitNull ] = None countries: Annotated[ list[Country] | None, @@ -95,6 +98,7 @@ class Scope(AdCPBaseModel): description='Countries included in the denominator, as ISO 3166-1 alpha-2 codes.', min_length=1, ), + NoExplicitNull, ] = None line_item_types: Annotated[ list[str] | None, @@ -102,10 +106,12 @@ class Scope(AdCPBaseModel): description='Seller or ad-server line item types included in the denominator.', min_length=1, ), + NoExplicitNull, ] = None date_range: Annotated[ date_range_1.DateRange | None, Field(description='Historical or planned date window used to compute the denominator.'), + NoExplicitNull, ] = None @@ -174,9 +180,13 @@ class SignalCoverageForecast(AdCPBaseModel): ), ] generated_at: Annotated[ - AwareDatetime | None, Field(description='When this coverage forecast was computed.') + AwareDatetime | None, + Field(description='When this coverage forecast was computed.'), + NoExplicitNull, ] = None valid_until: Annotated[ - AwareDatetime | None, Field(description='When this coverage forecast expires.') + AwareDatetime | None, + Field(description='When this coverage forecast expires.'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/signal_definition.py b/src/adcp/types/domains/core/signal_definition.py index 1aedabd51..68ccb87eb 100644 --- a/src/adcp/types/domains/core/signal_definition.py +++ b/src/adcp/types/domains/core/signal_definition.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_definition.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, StrictBool, StrictFloat @@ -30,7 +30,9 @@ class Range(AdCPBaseModel): min: Annotated[StrictFloat, Field(description='Minimum value')] max: Annotated[StrictFloat, Field(description='Maximum value')] unit: Annotated[ - str | None, Field(description="Unit of measurement (e.g., 'score', 'dollars', 'years')") + str | None, + Field(description="Unit of measurement (e.g., 'score', 'dollars', 'years')"), + NoExplicitNull, ] = None @@ -44,10 +46,12 @@ class Value(AdCPBaseModel): Field( description='Optional human-readable or taxonomy-native path for display and review.' ), + NoExplicitNull, ] = None modifiers: Annotated[ list[str] | None, Field(description='Optional taxonomy-specific modifiers that qualify the node.'), + NoExplicitNull, ] = None @@ -64,10 +68,12 @@ class ValueMapping(AdCPBaseModel): Field( description='Optional human-readable or taxonomy-native path for display and review.' ), + NoExplicitNull, ] = None modifiers: Annotated[ list[str] | None, Field(description='Optional taxonomy-specific modifiers that qualify the mapped value.'), + NoExplicitNull, ] = None @@ -89,6 +95,7 @@ class Taxonomy(AdCPBaseModel): Field( description='Version identifier for the taxonomy when the taxonomy has versioned definitions.' ), + NoExplicitNull, ] = None segtax: Annotated[ SchemaInt | None, @@ -96,12 +103,14 @@ class Taxonomy(AdCPBaseModel): description='OpenRTB segtax code when the taxonomy maps to an OpenRTB segment taxonomy.', ge=1, ), + NoExplicitNull, ] = None etag: Annotated[ str | None, Field( description='Optional validator for custom taxonomy definitions so consumers can detect drift between signal publication and taxonomy resolution.' ), + NoExplicitNull, ] = None values: Annotated[ list[Value], @@ -116,12 +125,14 @@ class Taxonomy(AdCPBaseModel): description="For categorical signals, maps package-targeting allowed_values[] strings to stable taxonomy node identifiers. The allowed_values[] strings remain the wire values buyers submit; this mapping explains how those strings resolve into the published taxonomy. Each value_mappings[].value SHOULD match one of the signal's allowed_values[] entries; JSON Schema draft-07 cannot enforce this cross-array constraint.", min_length=1, ), + NoExplicitNull, ] = None parent_match_behavior: Annotated[ ParentMatchBehavior | None, Field( description="Whether this signal definition supports treating a parent taxonomy node as matching descendant nodes for discovery/filtering or seller-side expansion. 'exact_only' means only explicitly listed node ids match. 'descendants_supported' means the seller can expand known, version-pinned parent nodes to descendants internally, typically by ORing the children in its execution system. 'unknown' means the provider has not declared parent-node behavior. This field is metadata about discovery/translation behavior, not a package targeting operator." ), + NoExplicitNull, ] = None @@ -189,9 +200,11 @@ class Onboarder(AdCPBaseModel): extra='forbid', ) match_keys: Annotated[list[MatchKey], Field(min_length=1)] - pre_onboarding_audience_expansion: StrictBool | None = None - pre_onboarding_device_expansion: StrictBool | None = None - pre_onboarding_precision_level: PreOnboardingPrecisionLevel | None = None + pre_onboarding_audience_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_device_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_precision_level: Annotated[ + PreOnboardingPrecisionLevel | None, NoExplicitNull + ] = None class IdType(StrEnum): @@ -274,19 +287,26 @@ class Channel(AdCPBaseModel): list[Right], Field(description='Rights supported by this channel.', min_length=1) ] url: Annotated[ - AnyUrl | None, Field(description='HTTPS URL for submitting the rights request.') + AnyUrl | None, + Field(description='HTTPS URL for submitting the rights request.'), + NoExplicitNull, ] = None email: Annotated[ - EmailStr | None, Field(description='Email address for submitting the rights request.') + EmailStr | None, + Field(description='Email address for submitting the rights request.'), + NoExplicitNull, ] = None languages: Annotated[ - list[str] | None, Field(description='BCP 47 language tags supported by this channel.') + list[str] | None, + Field(description='BCP 47 language tags supported by this channel.'), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, Field( description='ISO 3166-1 alpha-2 countries this channel serves, when the provider routes rights by country.' ), + NoExplicitNull, ] = None @@ -301,6 +321,7 @@ class DataSubjectRights(AdCPBaseModel): max_length=253, pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]{0,61}[A-Za-z0-9])$', ), + NoExplicitNull, ] = None channels: Annotated[ list[Channel], @@ -316,10 +337,12 @@ class DataSubjectRights(AdCPBaseModel): ge=1, le=90, ), + NoExplicitNull, ] = None ccpa_opt_out_url: Annotated[ AnyUrl | None, Field(description="US-specific 'Do Not Sell or Share' opt-out URL where required."), + NoExplicitNull, ] = None @@ -347,6 +370,7 @@ class Modeling(AdCPBaseModel): Field( description='Signal/modeling-specific disclosure requirements and jurisdictional notes. This is not creative provenance render guidance.' ), + NoExplicitNull, ] = None @@ -370,6 +394,7 @@ class SignalDefinition(AdCPBaseModel): description="Detailed description of what this signal represents and how it's derived", max_length=2000, ), + NoExplicitNull, ] = None value_type: Annotated[ signal_value_type.SignalValueType, @@ -380,6 +405,7 @@ class SignalDefinition(AdCPBaseModel): Field( description="Tags for grouping and filtering this domain's published signal definitions" ), + NoExplicitNull, ] = None allowed_values: Annotated[ list[str] | None, @@ -387,6 +413,7 @@ class SignalDefinition(AdCPBaseModel): description='For categorical signals, the valid values users can be assigned', min_length=1, ), + NoExplicitNull, ] = None restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, @@ -394,12 +421,14 @@ class SignalDefinition(AdCPBaseModel): description="Restricted attribute categories this signal touches. Data providers SHOULD declare these so governance agents can structurally match signals against a plan's restricted_attributes without relying on semantic inference from the signal name or description.", min_length=1, ), + NoExplicitNull, ] = None demographic_predicate: Annotated[ demographic_predicate_1.DemographicPredicate | None, Field( description='Authoritative machine-readable demographic meaning for this signal. Signal names and taxonomy labels alone never establish exact demographic equivalence. Signals carrying this field MUST also declare restricted_attributes including age.' ), + NoExplicitNull, ] = None policy_categories: Annotated[ list[str] | None, @@ -407,15 +436,19 @@ class SignalDefinition(AdCPBaseModel): description="Policy categories this signal is sensitive for (e.g., a children's interest signal declares ['children_directed']). Governance agents match these against a plan's policy_categories to flag sensitive data usage.", min_length=1, ), + NoExplicitNull, ] = None range: Annotated[ - Range | None, Field(description='For numeric signals, the valid value range') + Range | None, + Field(description='For numeric signals, the valid value range'), + NoExplicitNull, ] = None taxonomy: Annotated[ Taxonomy | None, Field( description='Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy. Taxonomy metadata does not create a new value_type and does not change package targeting grammar: buyers still target the named signal according to value_type. When a taxonomy value is a parent node, parent/descendant expansion is seller behavior and must be declared through parent_match_behavior rather than assumed.' ), + NoExplicitNull, ] = None segmentation_criteria: Annotated[ str | None, @@ -423,12 +456,14 @@ class SignalDefinition(AdCPBaseModel): description='Rules governing inclusion of identifiers in the segment. Aligns with IAB Data Transparency Standard audience criteria disclosure.', max_length=500, ), + NoExplicitNull, ] = None criteria_url: Annotated[ AnyUrl | None, Field( description='Optional URL to a longer-form methodology or criteria document. This is a disclosure pointer; buyers should not branch programmatically on the linked content.' ), + NoExplicitNull, ] = None data_sources: Annotated[ list[DataSource] | None, @@ -436,48 +471,57 @@ class SignalDefinition(AdCPBaseModel): description="Origin categories of the raw data used to compile the signal, aligned with IAB Data Transparency Standard source disclosure. Use 'panel' for respondent-panel or JIC-style audience sources. Offline and public-record sources require onboarder disclosure. Co-viewing projection and reconciliation of seller claims against a JIC or measurement vendor belong in measurement reporting/vendor metrics rather than package signal targeting.", min_length=1, ), + NoExplicitNull, ] = None methodology: Annotated[ Methodology | None, Field( description="How the signal's audience membership or attribute was determined. 'modeled' requires the modeling block." ), + NoExplicitNull, ] = None audience_expansion: Annotated[ StrictBool | None, Field( description='Whether look-alike or similar-audience expansion was used to include additional identifiers. When true, modeling is required.' ), + NoExplicitNull, ] = None device_expansion: Annotated[ StrictBool | None, Field( description="Whether the signal was expanded deterministically across devices of the same user, household, or business. Probabilistic cross-device expansion is modeling and should use methodology 'modeled' or the modeling block." ), + NoExplicitNull, ] = None refresh_cadence: Annotated[ RefreshCadence | None, Field( description="Cadence at which the signal definition's underlying segment membership is refreshed." ), + NoExplicitNull, ] = None lookback_window: Annotated[ RefreshCadence | None, Field(description='Time window in which a qualifying event can occur for inclusion.'), + NoExplicitNull, ] = None onboarder: Annotated[ Onboarder | None, Field( description='Onboarder disclosure. Required when data_sources includes an offline_* or public_record_* source.' ), + NoExplicitNull, ] = None subject_type: Annotated[ audience_subject_type.AudienceSubjectType | None, Field(description='What kind of subject this signal characterizes.'), + NoExplicitNull, ] = None resolution_method: Annotated[ audience_resolution_method.AudienceResolutionMethod | None, Field(description='How the subject is resolved at decision time.'), + NoExplicitNull, ] = None id_types: Annotated[ list[IdType] | None, @@ -485,12 +529,14 @@ class SignalDefinition(AdCPBaseModel): description='Identifier currencies analyzed to determine audience membership or attributes.', min_length=1, ), + NoExplicitNull, ] = None audience_scope: Annotated[ AudienceScope | None, Field( description="Context within which the audience attribute was determined. 'single_domain' requires originating_domain." ), + NoExplicitNull, ] = None originating_domain: Annotated[ str | None, @@ -498,6 +544,7 @@ class SignalDefinition(AdCPBaseModel): description="Domain of the digital property where the audience originates. Required when audience_scope is 'single_domain'.", pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]{0,61}[A-Za-z0-9])$', ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -505,6 +552,7 @@ class SignalDefinition(AdCPBaseModel): description="ISO 3166-1 alpha-2 country codes where the signal is applicable. Sellers must not expose a signal for media buys in countries outside this list. Federating agents that surface a peer's signal MUST treat the peer-published list as an upper bound, re-check it against the buyer's intended deployment countries, and may apply only narrower local policy.", min_length=1, ), + NoExplicitNull, ] = None consent_basis: Annotated[ list[consent_basis_1.ConsentBasis] | None, @@ -512,34 +560,40 @@ class SignalDefinition(AdCPBaseModel): description="Declared GDPR Article 6 lawful basis or consent basis under which this signal's data is processed. For non-GDPR regimes, use countries, policy_categories, and disclosure fields to describe jurisdiction-specific obligations unless a future enum value applies.", min_length=1, ), + NoExplicitNull, ] = None art9_basis: Annotated[ Art9Basis | None, Field( description='GDPR Article 9 basis when restricted_attributes is non-empty and the signal is used in jurisdictions where Article 9 applies. Required by policy for applicable use cases rather than universally required at schema level because sensitivity and lawful basis are jurisdiction-relative.' ), + NoExplicitNull, ] = None modeling: Annotated[ Modeling | None, Field( description="Modeling disclosure for modeled data signals. Required when methodology is 'modeled' or audience_expansion is true. This describes data modeling and intentionally does not reuse creative provenance, which is content/render oriented." ), + NoExplicitNull, ] = None data_subject_rights: Annotated[ DataSubjectRights | None, Field( description='Per-signal data-subject-rights routing. Inline on the signal because upstream source, rights routing, and response commitments can differ by segment or may be unavailable from a public provider document for custom/private signals. This is a contact/routing reference, not a machine-callable AdCP API.' ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.' ), + NoExplicitNull, ] = None dts_compliant_version: Annotated[ str | None, Field( description='IAB Data Transparency Standard version this signal definition self-attests as satisfying, when applicable.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_definition_enrichment.py b/src/adcp/types/domains/core/signal_definition_enrichment.py index 2151a3a24..62b7af392 100644 --- a/src/adcp/types/domains/core/signal_definition_enrichment.py +++ b/src/adcp/types/domains/core/signal_definition_enrichment.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_definition_enrichment.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, StrictBool @@ -22,8 +22,8 @@ class Value(AdCPBaseModel): extra='forbid', ) id: Annotated[str, Field(min_length=1)] - path: str | None = None - modifiers: list[str] | None = None + path: Annotated[str | None, NoExplicitNull] = None + modifiers: Annotated[list[str] | None, NoExplicitNull] = None class ValueMapping(AdCPBaseModel): @@ -32,8 +32,8 @@ class ValueMapping(AdCPBaseModel): ) value: str taxonomy_value_id: str - path: str | None = None - modifiers: list[str] | None = None + path: Annotated[str | None, NoExplicitNull] = None + modifiers: Annotated[list[str] | None, NoExplicitNull] = None class ParentMatchBehavior(StrEnum): @@ -47,12 +47,12 @@ class Taxonomy(AdCPBaseModel): extra='forbid', ) ref: AnyUrl - version: str | None = None - segtax: Annotated[SchemaInt | None, Field(ge=1)] = None - etag: str | None = None + version: Annotated[str | None, NoExplicitNull] = None + segtax: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + etag: Annotated[str | None, NoExplicitNull] = None values: Annotated[list[Value], Field(min_length=1)] - value_mappings: Annotated[list[ValueMapping] | None, Field(min_length=1)] = None - parent_match_behavior: ParentMatchBehavior | None = None + value_mappings: Annotated[list[ValueMapping] | None, Field(min_length=1), NoExplicitNull] = None + parent_match_behavior: Annotated[ParentMatchBehavior | None, NoExplicitNull] = None class DataSource(StrEnum): @@ -119,9 +119,11 @@ class Onboarder(AdCPBaseModel): extra='forbid', ) match_keys: Annotated[list[MatchKey], Field(min_length=1)] - pre_onboarding_audience_expansion: StrictBool | None = None - pre_onboarding_device_expansion: StrictBool | None = None - pre_onboarding_precision_level: PreOnboardingPrecisionLevel | None = None + pre_onboarding_audience_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_device_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_precision_level: Annotated[ + PreOnboardingPrecisionLevel | None, NoExplicitNull + ] = None class Country(ScalarStr): @@ -187,10 +189,10 @@ class Channel(AdCPBaseModel): extra='forbid', ) rights: Annotated[list[Right], Field(min_length=1)] - url: AnyUrl | None = None - email: EmailStr | None = None - languages: list[str] | None = None - countries: list[Country] | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + email: Annotated[EmailStr | None, NoExplicitNull] = None + languages: Annotated[list[str] | None, NoExplicitNull] = None + countries: Annotated[list[Country] | None, NoExplicitNull] = None class DataSubjectRights(AdCPBaseModel): @@ -203,10 +205,11 @@ class DataSubjectRights(AdCPBaseModel): max_length=253, pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]{0,61}[A-Za-z0-9])$', ), + NoExplicitNull, ] = None channels: Annotated[list[Channel], Field(min_length=1)] - response_sla_days: Annotated[SchemaInt | None, Field(ge=1, le=90)] = None - ccpa_opt_out_url: AnyUrl | None = None + response_sla_days: Annotated[SchemaInt | None, Field(ge=1, le=90), NoExplicitNull] = None + ccpa_opt_out_url: Annotated[AnyUrl | None, NoExplicitNull] = None class Modeling(AdCPBaseModel): @@ -217,7 +220,9 @@ class Modeling(AdCPBaseModel): seed_source: SeedSource training_data_jurisdictions: Annotated[list[TrainingDataJurisdiction], Field(min_length=1)] ai_act_risk_class: AiActRiskClass - disclosure: signal_modeling_disclosure.SignalModelingDisclosure | None = None + disclosure: Annotated[ + signal_modeling_disclosure.SignalModelingDisclosure | None, NoExplicitNull + ] = None class SignalDefinitionEnrichment(AdCPBaseModel): @@ -227,57 +232,65 @@ class SignalDefinitionEnrichment(AdCPBaseModel): restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, Field(description='Restricted attribute categories this signal touches.', min_length=1), + NoExplicitNull, ] = None demographic_predicate: Annotated[ demographic_predicate_1.DemographicPredicate | None, Field( description="Projected authoritative demographic meaning for the signal. When projected from another provider, this MUST match the provider's definition exactly. Signal names alone never establish demographic semantics." ), + NoExplicitNull, ] = None policy_categories: Annotated[ list[str] | None, Field(description='Policy categories this signal is sensitive for.', min_length=1), + NoExplicitNull, ] = None taxonomy: Annotated[ Taxonomy | None, Field( description='Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy.' ), + NoExplicitNull, ] = None - segmentation_criteria: Annotated[str | None, Field(max_length=500)] = None - criteria_url: AnyUrl | None = None - data_sources: Annotated[list[DataSource] | None, Field(min_length=1)] = None - methodology: Methodology | None = None - audience_expansion: StrictBool | None = None - device_expansion: StrictBool | None = None - refresh_cadence: RefreshCadence | None = None - lookback_window: RefreshCadence | None = None - onboarder: Onboarder | None = None - countries: Annotated[list[Country] | None, Field(min_length=1)] = None + segmentation_criteria: Annotated[str | None, Field(max_length=500), NoExplicitNull] = None + criteria_url: Annotated[AnyUrl | None, NoExplicitNull] = None + data_sources: Annotated[list[DataSource] | None, Field(min_length=1), NoExplicitNull] = None + methodology: Annotated[Methodology | None, NoExplicitNull] = None + audience_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + device_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + refresh_cadence: Annotated[RefreshCadence | None, NoExplicitNull] = None + lookback_window: Annotated[RefreshCadence | None, NoExplicitNull] = None + onboarder: Annotated[Onboarder | None, NoExplicitNull] = None + countries: Annotated[list[Country] | None, Field(min_length=1), NoExplicitNull] = None consent_basis: Annotated[ list[consent_basis_1.ConsentBasis] | None, Field( description="Data provider's declared GDPR Article 6 lawful basis or consent basis for the underlying signal definition, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own processing basis for the provider-declared basis.", min_length=1, ), + NoExplicitNull, ] = None art9_basis: Annotated[ Art9Basis | None, Field( description="Data provider's declared GDPR Article 9 basis for the underlying signal definition when special-category data is involved and Article 9 applies, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own Article 9 basis for the provider-declared basis." ), + NoExplicitNull, ] = None - modeling: Modeling | None = None + modeling: Annotated[Modeling | None, NoExplicitNull] = None data_subject_rights: Annotated[ DataSubjectRights | None, Field( description='Per-signal data-subject-rights routing. This is a contact/routing reference, not a machine-callable AdCP API.' ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.' ), + NoExplicitNull, ] = None - dts_compliant_version: str | None = None + dts_compliant_version: Annotated[str | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/signal_filters.py b/src/adcp/types/domains/core/signal_filters.py index 1e07697cd..deac4d24c 100644 --- a/src/adcp/types/domains/core/signal_filters.py +++ b/src/adcp/types/domains/core/signal_filters.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/signal_filters.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import signal_catalog_type @@ -20,13 +20,17 @@ class SignalFilters(AdCPBaseModel): catalog_types: Annotated[ list[signal_catalog_type.SignalAvailabilityType] | None, Field(description='Filter by catalog type', min_length=1), + NoExplicitNull, ] = None data_providers: Annotated[ - list[str] | None, Field(description='Filter by specific data providers', min_length=1) + list[str] | None, + Field(description='Filter by specific data providers', min_length=1), + NoExplicitNull, ] = None max_cpm: Annotated[ StrictFloat | None, Field(description="Maximum CPM filter. Applies only to signals with model='cpm'.", ge=0.0), + NoExplicitNull, ] = None max_percent: Annotated[ StrictFloat | None, @@ -35,13 +39,17 @@ class SignalFilters(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None min_coverage_percentage: Annotated[ - StrictFloat | None, Field(description='Minimum coverage requirement', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Minimum coverage requirement', ge=0.0, le=100.0), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Vendor-namespaced extension parameters for seller- or platform-specific signal filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; avoid unbounded logging or labels, and do not interpolate values into caller-visible error strings, LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_listing.py b/src/adcp/types/domains/core/signal_listing.py index c2c4db2db..6e65b568a 100644 --- a/src/adcp/types/domains/core/signal_listing.py +++ b/src/adcp/types/domains/core/signal_listing.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/signal_listing.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictFloat, model_validator from ..enums import restricted_attribute, signal_value_type @@ -35,6 +35,7 @@ class SignalListing(AdCPBaseModel): Field( description="Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined in a data provider's published adagents.json signals[]; use scope 'signal_source' with signal_source_url for a source-native signal." ), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -42,36 +43,42 @@ class SignalListing(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.', ), + NoExplicitNull, ] = None name: Annotated[ str | None, Field( description="Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced definition or source remains authoritative." ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition.' ), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition.' ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time.' ), + NoExplicitNull, ] = None value_type: Annotated[ signal_value_type.SignalValueType | None, Field( description="The data type of this signal's values. Required when signal_ref.scope is 'product'." ), + NoExplicitNull, ] = None categories: Annotated[ list[str] | None, @@ -79,10 +86,12 @@ class SignalListing(AdCPBaseModel): description="Valid values for categorical signals. Present when value_type is 'categorical'.", min_length=1, ), + NoExplicitNull, ] = None range: Annotated[ Range | None, Field(description="Valid range for numeric signals. Present when value_type is 'numeric'."), + NoExplicitNull, ] = None restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, @@ -90,12 +99,14 @@ class SignalListing(AdCPBaseModel): description='Restricted attribute categories this listing touches. Required with demographic_predicate and must include age. For referenced provider/source signals, any projected values must match the authoritative definition.', min_length=1, ), + NoExplicitNull, ] = None demographic_predicate: Annotated[ demographic_predicate_1.DemographicPredicate | None, Field( description='Machine-readable demographic meaning. For product-local signals this listing is authoritative; for data-provider and signal-source refs, any projected value MUST match the referenced authoritative definition. Signal names alone never establish demographic semantics.' ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/signal_modeling_disclosure.py b/src/adcp/types/domains/core/signal_modeling_disclosure.py index 31be34976..ea50f557b 100644 --- a/src/adcp/types/domains/core/signal_modeling_disclosure.py +++ b/src/adcp/types/domains/core/signal_modeling_disclosure.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_modeling_disclosure.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool @@ -30,6 +30,7 @@ class Jurisdiction(AdCPBaseModel): Field( description='Provider-defined sub-national region code or name when the obligation is regional. No global canonical format is implied.' ), + NoExplicitNull, ] = None regulation: Annotated[ str, @@ -40,15 +41,19 @@ class Jurisdiction(AdCPBaseModel): Field( description='Human-readable disclosure text or summary the provider expects buyers or reviewers to see.' ), + NoExplicitNull, ] = None disclosure_url: Annotated[ AnyUrl | None, Field( description="Optional URL to the provider's canonical disclosure or methodology page for this jurisdiction." ), + NoExplicitNull, ] = None audience: Annotated[ - Audience | None, Field(description='Primary audience for this disclosure entry.') + Audience | None, + Field(description='Primary audience for this disclosure entry.'), + NoExplicitNull, ] = None @@ -67,6 +72,7 @@ class SignalModelingDisclosure(AdCPBaseModel): Field( description='Jurisdictions where a modeling or AI-use disclosure applies.', min_length=1 ), + NoExplicitNull, ] = None notes: Annotated[ str | None, @@ -74,4 +80,5 @@ class SignalModelingDisclosure(AdCPBaseModel): description='Optional provider notes on how the disclosure should be interpreted. Informational only; buyers should not branch programmatically on this text.', max_length=2000, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_pricing.py b/src/adcp/types/domains/core/signal_pricing.py index 2c35b6cc7..ad0f66885 100644 --- a/src/adcp/types/domains/core/signal_pricing.py +++ b/src/adcp/types/domains/core/signal_pricing.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_pricing.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -20,7 +20,7 @@ class VendorPricing1(AdCPBaseModel): model: Literal['cpm'] = 'cpm' cpm: Annotated[StrictFloat, Field(description='Cost per thousand impressions', ge=0.0)] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class VendorPricing2(AdCPBaseModel): @@ -37,12 +37,13 @@ class VendorPricing2(AdCPBaseModel): description='Optional CPM cap. When set, the effective charge is min(percent × media_spend_per_mille, max_cpm).', ge=0.0, ), + NoExplicitNull, ] = None currency: Annotated[ str, Field(description='ISO 4217 currency code for the resulting charge', pattern='^[A-Z]{3}$'), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Period(StrEnum): @@ -60,7 +61,7 @@ class VendorPricing3(AdCPBaseModel): amount: Annotated[StrictFloat, Field(description='Fixed charge for the billing period', ge=0.0)] period: Annotated[Period, Field(description='Billing period for the flat fee.')] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class VendorPricing4(AdCPBaseModel): @@ -76,7 +77,7 @@ class VendorPricing4(AdCPBaseModel): ] unit_price: Annotated[StrictFloat, Field(description='Cost per one unit', ge=0.0)] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Metadata(AdCPBaseModel): @@ -89,6 +90,7 @@ class Metadata(AdCPBaseModel): description="One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim — summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", min_length=1, ), + NoExplicitNull, ] = None @@ -116,8 +118,9 @@ class VendorPricing5(AdCPBaseModel): description='ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None VendorPricing = Annotated[ diff --git a/src/adcp/types/domains/core/signal_selection_group_rule.py b/src/adcp/types/domains/core/signal_selection_group_rule.py index 830ae4f84..01dbec9d4 100644 --- a/src/adcp/types/domains/core/signal_selection_group_rule.py +++ b/src/adcp/types/domains/core/signal_selection_group_rule.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_selection_group_rule.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -37,12 +37,14 @@ class SignalSelectionGroupRule(AdCPBaseModel): Field( description="How options in this selection_group are intended to be used in signal_targeting_groups. 'include' maps to child groups with operator 'any'. 'exclude' maps to child groups with operator 'none'. Omit when options in the group may be used according to each option's allowed_targeting_modes." ), + NoExplicitNull, ] = None selection_mode: Annotated[ SelectionMode | None, Field( description="Selection behavior for this selection_group. 'required' means at least min_selected_signals, or 1 when omitted. 'fixed' means default_selected options in this group are seller-applied and read-only." ), + NoExplicitNull, ] = None min_selected_signals: Annotated[ SchemaInt | None, @@ -50,8 +52,10 @@ class SignalSelectionGroupRule(AdCPBaseModel): description="Minimum selected options from this selection_group. If selection_mode is 'required' and omitted, sellers MUST treat the minimum as 1.", ge=0, ), + NoExplicitNull, ] = None max_selected_signals: Annotated[ SchemaInt | None, Field(description='Maximum selected options from this selection_group.', ge=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_targeting.py b/src/adcp/types/domains/core/signal_targeting.py index d9de5babe..f6830a0d7 100644 --- a/src/adcp/types/domains/core/signal_targeting.py +++ b/src/adcp/types/domains/core/signal_targeting.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/signal_targeting.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from . import signal_id as signal_id_1 @@ -20,6 +20,7 @@ class SignalTargeting1(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -27,6 +28,7 @@ class SignalTargeting1(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[Literal['binary'], Field(description='Discriminator for binary signals')] = 'binary' value: Annotated[ @@ -44,6 +46,7 @@ class SignalTargeting2(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -51,6 +54,7 @@ class SignalTargeting2(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['categorical'], Field(description='Discriminator for categorical signals') @@ -71,6 +75,7 @@ class SignalTargeting3(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -78,6 +83,7 @@ class SignalTargeting3(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['numeric'], Field(description='Discriminator for numeric signals') @@ -87,12 +93,14 @@ class SignalTargeting3(AdCPBaseModel): Field( description="Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided. Should be >= signal's range.min if defined." ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description="Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided. Should be <= signal's range.max if defined." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_targeting_expression.py b/src/adcp/types/domains/core/signal_targeting_expression.py index 93a9e8048..5b9035922 100644 --- a/src/adcp/types/domains/core/signal_targeting_expression.py +++ b/src/adcp/types/domains/core/signal_targeting_expression.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/signal_targeting_expression.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from . import signal_ref as signal_ref_1 @@ -56,12 +56,14 @@ class SignalTargetingExpression3(AdCPBaseModel): Field( description="Minimum value, inclusive. Omit for no minimum. Should be within the signal definition's range when declared." ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description="Maximum value, inclusive. Omit for no maximum. Should be within the signal definition's range when declared." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/signal_targeting_rules.py b/src/adcp/types/domains/core/signal_targeting_rules.py index 4d9d6d7be..ce06453e9 100644 --- a/src/adcp/types/domains/core/signal_targeting_rules.py +++ b/src/adcp/types/domains/core/signal_targeting_rules.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/signal_targeting_rules.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from . import signal_selection_group_rule @@ -33,12 +33,14 @@ class SignalTargetingRules(AdCPBaseModel): Field( description="How selected signal_targeting_options are resolved against the product's inventory. 'direct_targeting' means selected signals are applied as targeting predicates to the package inventory. 'seller_planned' means selected signals are planning inputs that the seller resolves against product-specific inventory, timing, availability, reach, or pacing constraints; buyers SHOULD NOT attempt to decompose the signal selection into lower-level inventory or schedule decisions. Use 'seller_planned' for products such as linear broadcast schedules where the audience definition may be portable but the audience-to-avails plan is seller-resolved." ), + NoExplicitNull, ] = ResolutionModel.direct_targeting selection_mode: Annotated[ SelectionMode | None, Field( description="Default selection behavior for selectable signals on this product. 'optional' means the buyer may select zero or more signals. 'required' means the buyer must select at least min_selected_signals, or 1 when min_selected_signals is omitted. 'fixed' means the seller applies the default_selected signals and the buyer cannot add or remove them; buyers SHOULD render those entries as read-only and sellers MUST echo them in package targeting_overlay.signal_targeting_groups. Use selection_group_rules for product-scoped products that need different behavior for different groups, such as fixed suppressions plus a required include tier." ), + NoExplicitNull, ] = SelectionMode.optional min_selected_signals: Annotated[ SchemaInt | None, @@ -46,6 +48,7 @@ class SignalTargetingRules(AdCPBaseModel): description="Minimum number of signals the buyer must select when selection_mode is 'required'. If selection_mode is 'required' and this field is omitted, sellers MUST treat the minimum as 1. Defaults to 0 for optional selection.", ge=0, ), + NoExplicitNull, ] = None max_selected_signals: Annotated[ SchemaInt | None, @@ -53,6 +56,7 @@ class SignalTargetingRules(AdCPBaseModel): description='Maximum number of signals the buyer may select for a package. Omit when there is no declared limit beyond the available options.', ge=1, ), + NoExplicitNull, ] = None max_selected_per_group: Annotated[ SchemaInt | None, @@ -60,6 +64,7 @@ class SignalTargetingRules(AdCPBaseModel): description='Maximum number of signal_targeting_options the buyer may select from the same ProductSignalTargetingOption.selection_group. Use 1 for mutually exclusive alternatives within each option group. This limit applies to product option grouping, not to the number of child groups in packages[].targeting_overlay.signal_targeting_groups.', ge=1, ), + NoExplicitNull, ] = None max_signal_targeting_groups: Annotated[ SchemaInt | None, @@ -67,6 +72,7 @@ class SignalTargetingRules(AdCPBaseModel): description='Maximum number of child groups allowed in packages[].targeting_overlay.signal_targeting_groups.groups. Omit when the seller has no declared limit beyond product terms.', ge=1, ), + NoExplicitNull, ] = None max_signals_per_targeting_group: Annotated[ SchemaInt | None, @@ -74,6 +80,7 @@ class SignalTargetingRules(AdCPBaseModel): description='Maximum number of signals allowed in each packages[].targeting_overlay.signal_targeting_groups.groups[].signals array. Omit when the seller has no declared limit beyond product terms.', ge=1, ), + NoExplicitNull, ] = None selection_group_rules: Annotated[ list[signal_selection_group_rule.SignalSelectionGroupRule] | None, @@ -81,4 +88,5 @@ class SignalTargetingRules(AdCPBaseModel): description='Optional product-scoped overrides for specific ProductSignalTargetingOption.selection_group values. Use this when one product has mixed behavior, such as fixed seller-applied suppressions, a required pick-one include tier, optional buyer-selected exclusions, or heterogeneous targeting planes that must be represented as separate ANDed clauses. Rules apply only to options whose selection_group matches. When selection_group_rules are present, each packages[].targeting_overlay.signal_targeting_groups child group MUST contain signals from exactly one selection_group and one targeting_mode, and buyers MUST send at most one child group for each (selection_group, targeting_mode) pair. Sellers MUST reject duplicate, mixed, or collapsed groups that combine distinct selection_group_rules into the same child group.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/sla_window.py b/src/adcp/types/domains/core/sla_window.py index f6545809d..7a73cc862 100644 --- a/src/adcp/types/domains/core/sla_window.py +++ b/src/adcp/types/domains/core/sla_window.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/sla_window.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -22,6 +22,7 @@ class SlaWindow(AdCPBaseModel): examples=['PT5M', 'PT4H', 'P1D'], pattern='^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$', ), + NoExplicitNull, ] = None completion_max: Annotated[ str | None, @@ -30,4 +31,5 @@ class SlaWindow(AdCPBaseModel): examples=['PT1H', 'PT24H', 'P2D'], pattern='^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/special.py b/src/adcp/types/domains/core/special.py index c2b96e54d..2ec0ed2b6 100644 --- a/src/adcp/types/domains/core/special.py +++ b/src/adcp/types/domains/core/special.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/special.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..enums import special_category @@ -20,12 +20,15 @@ class Special(AdCPBaseModel): str, Field(description="Name of the event (e.g., 'Olympics 2028', 'Super Bowl LXI')") ] category: Annotated[ - special_category.SpecialCategory | None, Field(description='Category of the event') + special_category.SpecialCategory | None, + Field(description='Category of the event'), + NoExplicitNull, ] = None starts: Annotated[ - AwareDatetime | None, Field(description='When the event starts (ISO 8601)') + AwareDatetime | None, Field(description='When the event starts (ISO 8601)'), NoExplicitNull ] = None ends: Annotated[ AwareDatetime | None, Field(description='When the event ends (ISO 8601). Omit for single-day events.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/store_item.py b/src/adcp/types/domains/core/store_item.py index a93a4eb49..2b1999cbe 100644 --- a/src/adcp/types/domains/core/store_item.py +++ b/src/adcp/types/domains/core/store_item.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/store_item.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import catchment @@ -30,17 +30,24 @@ class Address(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - street: Annotated[str | None, Field(description="Street address (e.g., '123 Main St').")] = None - city: Annotated[str | None, Field(description='City name.')] = None + street: Annotated[ + str | None, Field(description="Street address (e.g., '123 Main St')."), NoExplicitNull + ] = None + city: Annotated[str | None, Field(description='City name.'), NoExplicitNull] = None region: Annotated[ str | None, Field( description="State, province, or region. ISO 3166-2 subdivision code preferred (e.g., 'NL-NH', 'US-CA')." ), + NoExplicitNull, ] = None - postal_code: Annotated[str | None, Field(description='Postal or ZIP code.')] = None + postal_code: Annotated[str | None, Field(description='Postal or ZIP code.'), NoExplicitNull] = ( + None + ) country: Annotated[ - str | None, Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$') + str | None, + Field(description='ISO 3166-1 alpha-2 country code.', pattern='^[A-Z]{2}$'), + NoExplicitNull, ] = None @@ -62,7 +69,9 @@ class StoreItem(AdCPBaseModel): ] location: Annotated[Location, Field(description='Geographic coordinates of the store.')] address: Annotated[ - Address | None, Field(description='Structured address for display and geocoding fallback.') + Address | None, + Field(description='Structured address for display and geocoding fallback.'), + NoExplicitNull, ] = None catchments: Annotated[ list[catchment.Catchment] | None, @@ -70,13 +79,17 @@ class StoreItem(AdCPBaseModel): description='Catchment areas for this store. Each defines a reachable area using travel time (isochrone), simple radius, or pre-computed GeoJSON. Multiple catchments allow different modes — e.g., 15-minute drive AND 10-minute walk.', min_length=1, ), + NoExplicitNull, ] = None phone: Annotated[ - str | None, Field(description="Store phone number in E.164 format (e.g., '+31201234567').") + str | None, + Field(description="Store phone number in E.164 format (e.g., '+31201234567')."), + NoExplicitNull, ] = None url: Annotated[ AnyUrl | None, Field(description='Store-specific page URL (e.g., store locator detail page).'), + NoExplicitNull, ] = None hours: Annotated[ dict[ @@ -87,6 +100,7 @@ class StoreItem(AdCPBaseModel): Field( description='Operating hours. Keys are ISO day names (monday–sunday), values are time ranges.' ), + NoExplicitNull, ] = None tags: Annotated[ list[str] | None, @@ -94,5 +108,6 @@ class StoreItem(AdCPBaseModel): description="Tags for filtering stores in targeting and creative selection (e.g., 'flagship', 'pickup', 'pharmacy').", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/talent.py b/src/adcp/types/domains/core/talent.py index 5607dfb08..41bffc402 100644 --- a/src/adcp/types/domains/core/talent.py +++ b/src/adcp/types/domains/core/talent.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/talent.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import talent_role @@ -26,4 +26,5 @@ class Talent(AdCPBaseModel): Field( description="URL to this person's brand.json entry. Enables buyer agents to evaluate the talent's brand identity and associations." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/targeting.py b/src/adcp/types/domains/core/targeting.py index 17707ed15..7308d40f7 100644 --- a/src/adcp/types/domains/core/targeting.py +++ b/src/adcp/types/domains/core/targeting.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/targeting.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from collections.abc import Sequence from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -80,6 +80,7 @@ class AgeRestriction(AdCPBaseModel): verification_required: Annotated[ StrictBool | None, Field(description='Whether verified age (not inferred) is required for compliance'), + NoExplicitNull, ] = False accepted_methods: Annotated[ list[age_verification_method.AgeVerificationMethod] | None, @@ -87,6 +88,7 @@ class AgeRestriction(AdCPBaseModel): description='Accepted verification methods. If omitted, any method the platform supports is acceptable.', min_length=1, ), + NoExplicitNull, ] = None @@ -103,6 +105,7 @@ class StoreCatchment(AdCPBaseModel): description='Filter to specific stores within the catalog. Omit to target all stores.', min_length=1, ), + NoExplicitNull, ] = None catchment_ids: Annotated[ list[str] | None, @@ -110,6 +113,7 @@ class StoreCatchment(AdCPBaseModel): description="Catchment zone IDs to target (e.g., 'walk', 'drive'). Omit to target all catchment zones.", min_length=1, ), + NoExplicitNull, ] = None @@ -151,6 +155,7 @@ class KeywordTarget(AdCPBaseModel): description="Per-keyword bid price, denominated in the same currency as the package's pricing option. Overrides the package-level bid_price for this keyword. Inherits the max_bid interpretation from the pricing option: when max_bid is true, this is the keyword's bid ceiling; when false, this is the exact bid. If omitted, the package bid_price applies.", ge=0.0, ), + NoExplicitNull, ] = None @@ -173,6 +178,7 @@ class GeoProximityItem(AdCPBaseModel): ge=-90.0, le=90.0, ), + NoExplicitNull, ] = None lng: Annotated[ StrictFloat | None, @@ -181,38 +187,44 @@ class GeoProximityItem(AdCPBaseModel): ge=-180.0, le=180.0, ), + NoExplicitNull, ] = None label: Annotated[ str | None, Field( description="Human-readable label for this entry (e.g., 'Düsseldorf', 'Heathrow Airport', 'Primary trade area')." ), + NoExplicitNull, ] = None travel_time: Annotated[ TravelTime | None, Field( description='Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks.' ), + NoExplicitNull, ] = None transport_mode: Annotated[ transport_mode_1.TransportMode | None, Field( description='Transportation mode for isochrone calculation. Required when travel_time is provided.' ), + NoExplicitNull, ] = None radius: Annotated[ Radius | None, Field( description='Simple radius from the point. The platform draws a circle of this distance around the coordinates.' ), + NoExplicitNull, ] = None geometry: Annotated[ Geometry | None, Field( description='Pre-computed GeoJSON geometry defining the proximity boundary. Use when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom boundaries. When geometry is provided, lat/lng are not required.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class TargetingOverlay(AdCPBaseModel): @@ -225,6 +237,7 @@ class TargetingOverlay(AdCPBaseModel): description="Restrict delivery to specific countries. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", min_length=1, ), + NoExplicitNull, ] = None geo_countries_exclude: Annotated[ Sequence[GeoCountriesExcludeItem] | None, @@ -232,6 +245,7 @@ class TargetingOverlay(AdCPBaseModel): description="Exclude specific countries from delivery. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", min_length=1, ), + NoExplicitNull, ] = None geo_regions: Annotated[ list[GeoRegion] | None, @@ -239,6 +253,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to exact canonical ISO 3166-2 subdivisions (states, provinces, regions, departments, or other subdivision categories). Unknown identifiers are invalid. At create or update, sellers MUST reject unsupported identifiers and MUST NOT silently widen, drop, or partially apply the list. During get_products, a seller may instead return a sparse, buyer-reviewable targeting_resolution modification for a valid but unsupported requested outcome. Exact internal translation preserves accepted identifiers in package readback.', min_length=1, ), + NoExplicitNull, ] = None geo_regions_exclude: Annotated[ Sequence[GeoRegionsExcludeItem] | None, @@ -246,6 +261,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude exact canonical ISO 3166-2 subdivisions. Support is independent from geo_regions inclusion support. Unknown identifiers and values also present in geo_regions are invalid. At create or update, sellers MUST reject unsupported identifiers and partial application; during get_products, a seller may instead return a sparse, buyer-reviewable targeting_resolution modification for a valid but unsupported requested outcome.', min_length=1, ), + NoExplicitNull, ] = None geo_metros: Annotated[ list[geo_metro.GeoMetro] | None, @@ -254,6 +270,7 @@ class TargetingOverlay(AdCPBaseModel): min_length=1, title='Targeting Geo Metros', ), + NoExplicitNull, ] = None geo_metros_exclude: Annotated[ Sequence[GeoMetrosExcludeItem] | None, @@ -261,6 +278,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude specific metro areas from delivery. Each entry specifies the classification system and excluded values. Seller must declare supported systems in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None geo_postal_areas: Annotated[ list[postal_area.PostalArea] | None, @@ -268,6 +286,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to specific postal areas. Prefer the native country + postal system form. The deprecated legacy country-fused postal-system tokens remain accepted for compatibility. Seller must declare supported systems in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None geo_postal_areas_exclude: Annotated[ Sequence[postal_area.PostalArea] | None, @@ -275,6 +294,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude specific postal areas from delivery. Prefer the native country + postal system form. The deprecated legacy country-fused postal-system tokens remain accepted for compatibility. Seller must declare supported systems in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None geo_places: Annotated[ list[geo_place_area.GeographicPlaceArea] | None, @@ -282,6 +302,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to catalog-backed named places. Values MUST be stable identifiers in the declared system, not display names. Sellers must declare supported systems, countries, and place types in get_adcp_capabilities and reject unsupported entries rather than silently dropping them.', min_length=1, ), + NoExplicitNull, ] = None geo_places_exclude: Annotated[ list[geo_place_area.GeographicPlaceArea] | None, @@ -289,6 +310,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude catalog-backed named places. Uses the same identifier-based shape as geo_places. Sellers MUST reject overlap with geo_places for the same country, system, place_type, and value.', min_length=1, ), + NoExplicitNull, ] = None daypart_targets: Annotated[ list[daypart_target.DaypartTarget] | None, @@ -296,6 +318,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to specific time windows. Each entry specifies days of week, an hour range, and an optional timezone that defaults to inventory_local. A concrete IANA zone uses one shared civil-time clock, while inventory_local evaluates each inventory unit in its seller-assigned local timezone. Entries are independent and MAY use different clocks.', min_length=1, ), + NoExplicitNull, ] = None axe_include_segment: Annotated[ str | None, @@ -303,6 +326,7 @@ class TargetingOverlay(AdCPBaseModel): deprecated=True, description='Deprecated: Use TMP provider fields instead. AXE segment ID to include for targeting.', ), + NoExplicitNull, ] = None axe_exclude_segment: Annotated[ str | None, @@ -310,6 +334,7 @@ class TargetingOverlay(AdCPBaseModel): deprecated=True, description='Deprecated: Use TMP provider fields instead. AXE segment ID to exclude from targeting.', ), + NoExplicitNull, ] = None audience_include: Annotated[ list[str] | None, @@ -317,6 +342,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to members of these first-party CRM audiences. Only users present in the uploaded lists are eligible. References audience_id values from sync_audiences on the same seller account — audience IDs are not portable across sellers. Not for lookalike expansion — express that intent in the campaign brief. Seller must declare support in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None audience_exclude: Annotated[ list[str] | None, @@ -324,6 +350,7 @@ class TargetingOverlay(AdCPBaseModel): description='Suppress delivery to members of these first-party CRM audiences. Matched users are excluded regardless of other targeting. References audience_id values from sync_audiences on the same seller account — audience IDs are not portable across sellers. Seller must declare support in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None signal_targeting_groups: Annotated[ package_signal_targeting_groups.PackageSignalTargetingGroups | None, @@ -331,6 +358,7 @@ class TargetingOverlay(AdCPBaseModel): description="Basic Boolean grouping for seller-offered signals. v1 supports a required top-level operator 'all' and child groups with operator 'any' for include groups or 'none' for exclusion groups. Example semantics: group 1 any(A, B) plus group 2 none(C, D) means (A OR B) AND NOT (C OR D). Signal entries reference named signal definitions with signal_ref scope 'product' for product-local signal options or scope 'data_provider' for external signals published in adagents.json signals[]. For simple include-only targeting, send one child group with operator 'any'. Sellers SHOULD reject entries that are not available for the product through inline signal_targeting_options or get_signals, are not active for the account, or exceed the product's signal_targeting_allowed/signal_targeting_rules/product terms. Signal targeting limits are product-scoped, not declared in get_adcp_capabilities, because products may be backed by different ad servers. Sellers MUST echo applied signal_targeting_groups on the resulting package state, including fixed/default selections. Sellers MAY return REQUOTE_REQUIRED when a targeting mutation changes commercial terms.", title='Targeting Signal Groups', ), + NoExplicitNull, ] = None signal_targeting: Annotated[ list[signal_targeting_1.SignalTargeting] | None, @@ -339,15 +367,17 @@ class TargetingOverlay(AdCPBaseModel): description='DEPRECATED. Use signal_targeting_groups for package-level signal targeting. Legacy flat signal_targeting remains accepted during the SignalRef migration window but cannot express grouped include/exclude composition or product-scoped pricing.', min_length=1, ), + NoExplicitNull, ] = None demographics: Annotated[ demographic_targeting_intent.DemographicTargetingIntent | None, Field( description='Canonical demographic audience targeting intent with optional constraints on how age may be determined. This is distinct from age_restriction: demographics selects an audience, while age_restriction expresses a legal eligibility or verification floor. Fresh create/update targeting MUST compile exactly or be rejected. During get_products, a seller may offer a different configured predicate only through sparse targeting_resolution modifications on a distinguishable product_id; selecting that product accepts the alternative. Sellers never silently broaden, narrow, default, drop, or substitute the basis.' ), + NoExplicitNull, ] = None frequency_cap: Annotated[ - frequency_cap_1.FrequencyCap | None, Field(title='Targeting Frequency Cap') + frequency_cap_1.FrequencyCap | None, Field(title='Targeting Frequency Cap'), NoExplicitNull ] = None property_list: Annotated[ property_list_ref.PropertyListReference | None, @@ -355,12 +385,14 @@ class TargetingOverlay(AdCPBaseModel): description="Reference to a property list for targeting specific properties within this product. The package runs on the intersection of the product's publisher_properties and this list. Sellers SHOULD return a validation error if the product has property_targeting_allowed: false.", title='Targeting Property List', ), + NoExplicitNull, ] = None property_list_exclude: Annotated[ property_list_ref.PropertyListReference | None, Field( description="Reference to a property list whose properties must not carry the buyer's ads. Matched properties are removed from delivery. Use for brand-safety do-not-run lists (apps, sites). Exclude wins on overlap with property_list, and applies regardless of the product's property_targeting_allowed flag. Seller must declare support in get_adcp_capabilities." ), + NoExplicitNull, ] = None collection_list: Annotated[ collection_list_ref.CollectionListReference | None, @@ -368,18 +400,21 @@ class TargetingOverlay(AdCPBaseModel): description='Reference to a collection list for including specific collections (programs, publications, channels) within this product. The package runs on the intersection of matched collections and this list. Use for inclusion-based collection targeting. Seller must declare support in get_adcp_capabilities.', title='Targeting Collection List', ), + NoExplicitNull, ] = None collection_list_exclude: Annotated[ collection_list_ref.CollectionListReference | None, Field( description="Reference to a collection list for excluding specific collections (programs, publications, channels) from this product. Matched collections must not carry the buyer's ads. Use for brand safety do-not-air lists. Seller must declare support in get_adcp_capabilities." ), + NoExplicitNull, ] = None placement_selection: Annotated[ placement_selection_1.PlacementSelection | None, Field( description='Purchased placement selection within the product. This constrains package inventory; it is distinct from creative_assignments[].placement_refs, which only route individual creatives within the purchased set. On create, mode selected supplies the complete selected set and mode default uses the product default. In request-side Targeting Input, a non-null value replaces this dimension, omission preserves or inherits it, and null clears it when the product permits that broader inventory set.' ), + NoExplicitNull, ] = None collection_selection: Annotated[ collection_selection_1.CollectionSelection | None, @@ -387,12 +422,14 @@ class TargetingOverlay(AdCPBaseModel): description="Purchased collection selection within the product. On create, mode selected supplies the complete selected set and mode default uses the product's full bundle. On package readback this is the committed selection sellers MUST echo as concrete selectors, materializing any collection_list composition; collection_list fields remain the buyer-managed list mechanism. In request-side Targeting Input, a non-null value replaces this dimension, omission preserves or inherits it, and null clears it when the product permits that broader inventory set.", title='Targeting Collection Selection', ), + NoExplicitNull, ] = None age_restriction: Annotated[ AgeRestriction | None, Field( description='Age restriction for compliance. Use for legal requirements (alcohol, gambling), not audience targeting.' ), + NoExplicitNull, ] = None device_platform: Annotated[ list[device_platform_1.DevicePlatform] | None, @@ -400,6 +437,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict to specific platforms. Use for technical compatibility (app only works on iOS). Values from Sec-CH-UA-Platform standard, extended for CTV.', min_length=1, ), + NoExplicitNull, ] = None device_platform_exclude: Annotated[ list[device_platform_1.DevicePlatform] | None, @@ -407,6 +445,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude specific operating-system platforms from delivery. When a platform appears in both device_platform and device_platform_exclude, exclusion wins. Sellers MUST reject a request they cannot enforce rather than silently dropping the exclusion.', min_length=1, ), + NoExplicitNull, ] = None device_type: Annotated[ list[device_type_1.DeviceType] | None, @@ -414,6 +453,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict to specific device form factors. Use for campaigns targeting hardware categories rather than operating systems (e.g., mobile-only promotions, CTV campaigns).', min_length=1, ), + NoExplicitNull, ] = None device_type_exclude: Annotated[ list[device_type_1.DeviceType] | None, @@ -421,6 +461,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude specific device form factors from delivery (e.g., exclude CTV for app-install campaigns).', min_length=1, ), + NoExplicitNull, ] = None browser: Annotated[ list[browser_family.BrowserFamily] | None, @@ -428,6 +469,7 @@ class TargetingOverlay(AdCPBaseModel): description='Restrict delivery to specific canonical browser families in the impression delivery and rendering environment, not the post-click landing-page browser. Values MUST NOT be inferred solely from operating system, device, web/mobile-web inventory, or placement. Values in this array use OR semantics. When browser is supplied, families not listed are ineligible: other includes a seller-recognized family that is not explicitly enumerated, while unknown includes a browser the seller cannot classify into a recognized family. When the same family appears in browser and browser_exclude, exclusion wins. Browser and device constraints intersect; a seller that cannot enforce the exact combination MUST exclude or explicitly reconfigure the product during discovery and MUST reject it at create or update rather than silently widening delivery. Browser versions and seller-native IDs are intentionally unsupported.', min_length=1, ), + NoExplicitNull, ] = None browser_exclude: Annotated[ list[browser_family.BrowserFamily] | None, @@ -435,6 +477,7 @@ class TargetingOverlay(AdCPBaseModel): description='Exclude specific canonical browser families from delivery. other excludes seller-recognized families that are not explicitly enumerated; unknown excludes browsers the seller cannot classify into a recognized family. When the same family appears in browser and browser_exclude, exclusion wins. Sellers MUST reject a request they cannot enforce rather than silently dropping the exclusion.', min_length=1, ), + NoExplicitNull, ] = None store_catchments: Annotated[ list[StoreCatchment] | None, @@ -442,6 +485,7 @@ class TargetingOverlay(AdCPBaseModel): description='Target users within store catchment areas from a synced store catalog. Each entry references a store-type catalog and optionally narrows to specific stores or catchment zones.', min_length=1, ), + NoExplicitNull, ] = None geo_proximity: Annotated[ list[GeoProximityItem] | None, @@ -449,6 +493,7 @@ class TargetingOverlay(AdCPBaseModel): description='Target users within travel time, distance, or a custom boundary around arbitrary geographic points. Multiple entries use OR semantics — a user within range of any listed point is eligible. For campaigns targeting 10+ locations, consider using store_catchments with a location catalog instead. Seller must declare support in get_adcp_capabilities.', min_length=1, ), + NoExplicitNull, ] = None language: Annotated[ list[locale_tag.LanguageTag] | None, @@ -457,6 +502,7 @@ class TargetingOverlay(AdCPBaseModel): min_length=1, title='Targeting Languages', ), + NoExplicitNull, ] = None keyword_targets: Annotated[ list[KeywordTarget] | None, @@ -465,6 +511,7 @@ class TargetingOverlay(AdCPBaseModel): min_length=1, title='Targeting Keywords', ), + NoExplicitNull, ] = None negative_keywords: Annotated[ list[negative_keyword.NegativeKeyword] | None, @@ -473,4 +520,5 @@ class TargetingOverlay(AdCPBaseModel): min_length=1, title='Targeting Negative Keywords', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/targeting_input.py b/src/adcp/types/domains/core/targeting_input.py index a86a60833..3c3def92e 100644 --- a/src/adcp/types/domains/core/targeting_input.py +++ b/src/adcp/types/domains/core/targeting_input.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/targeting_input.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictFloat @@ -85,6 +85,7 @@ class StoreCatchment(AdCPBaseModel): description='Filter to specific stores within the catalog. Omit to target all stores.', min_length=1, ), + NoExplicitNull, ] = None catchment_ids: Annotated[ list[str] | None, @@ -92,6 +93,7 @@ class StoreCatchment(AdCPBaseModel): description="Catchment zone IDs to target (e.g., 'walk', 'drive'). Omit to target all catchment zones.", min_length=1, ), + NoExplicitNull, ] = None @@ -133,6 +135,7 @@ class KeywordTarget(AdCPBaseModel): description="Per-keyword bid price, denominated in the same currency as the package's pricing option. Overrides the package-level bid_price for this keyword. Inherits the max_bid interpretation from the pricing option: when max_bid is true, this is the keyword's bid ceiling; when false, this is the exact bid. If omitted, the package bid_price applies.", ge=0.0, ), + NoExplicitNull, ] = None @@ -155,6 +158,7 @@ class GeoProximityItem(AdCPBaseModel): ge=-90.0, le=90.0, ), + NoExplicitNull, ] = None lng: Annotated[ StrictFloat | None, @@ -163,38 +167,44 @@ class GeoProximityItem(AdCPBaseModel): ge=-180.0, le=180.0, ), + NoExplicitNull, ] = None label: Annotated[ str | None, Field( description="Human-readable label for this entry (e.g., 'Düsseldorf', 'Heathrow Airport', 'Primary trade area')." ), + NoExplicitNull, ] = None travel_time: Annotated[ TravelTime | None, Field( description='Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks.' ), + NoExplicitNull, ] = None transport_mode: Annotated[ transport_mode_1.TransportMode | None, Field( description='Transportation mode for isochrone calculation. Required when travel_time is provided.' ), + NoExplicitNull, ] = None radius: Annotated[ Radius | None, Field( description='Simple radius from the point. The platform draws a circle of this distance around the coordinates.' ), + NoExplicitNull, ] = None geometry: Annotated[ Geometry | None, Field( description='Pre-computed GeoJSON geometry defining the proximity boundary. Use when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom boundaries. When geometry is provided, lat/lng are not required.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class TargetingOverlayInput(AdCPBaseModel): diff --git a/src/adcp/types/domains/core/targeting_modification.py b/src/adcp/types/domains/core/targeting_modification.py index 086fbe0e2..84160f97f 100644 --- a/src/adcp/types/domains/core/targeting_modification.py +++ b/src/adcp/types/domains/core/targeting_modification.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/targeting_modification.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import ext as ext_1 @@ -32,7 +32,7 @@ class TargetingModification1(AdCPBaseModel): ), ] reason: Annotated[str, Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Path(StrEnum): @@ -77,6 +77,7 @@ class TargetingModification2(AdCPBaseModel): Field( description='Stable identity of exactly one postal-area group in the requested array. Required only for postal-area paths. Zero or multiple matches make the modification invalid.' ), + NoExplicitNull, ] = None values: Annotated[ list[str], @@ -86,7 +87,7 @@ class TargetingModification2(AdCPBaseModel): ), ] reason: Annotated[str, Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None TargetingModification = Annotated[ diff --git a/src/adcp/types/domains/core/targeting_overlay_requirements.py b/src/adcp/types/domains/core/targeting_overlay_requirements.py index 9ae9b0230..84add91f3 100644 --- a/src/adcp/types/domains/core/targeting_overlay_requirements.py +++ b/src/adcp/types/domains/core/targeting_overlay_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/targeting_overlay_requirements.json -# timestamp: 2026-10-03T00:35:15+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel from ..enums import browser_family, daypart_timezone_mode, match_type, metro_system, transport_mode @@ -86,6 +86,7 @@ class DaypartRequirement1(AdCPBaseModel): description='Exact concrete IANA timezone identifiers that must remain selectable after discovery. Valid only when timezone_modes includes iana.', min_length=1, ), + NoExplicitNull, ] = None @@ -103,12 +104,12 @@ class GeoProximity(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - radius: Required | None = None - travel_time: Required | None = None - geometry: Required | None = None - transport_modes: Annotated[list[transport_mode.TransportMode] | None, Field(min_length=1)] = ( - None - ) + radius: Annotated[Required | None, NoExplicitNull] = None + travel_time: Annotated[Required | None, NoExplicitNull] = None + geometry: Annotated[Required | None, NoExplicitNull] = None + transport_modes: Annotated[ + list[transport_mode.TransportMode] | None, Field(min_length=1), NoExplicitNull + ] = None class Demographics(AdCPBaseModel): @@ -122,47 +123,56 @@ class TargetingOverlayRequirements(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - geo_countries: Required | None = None - geo_countries_exclude: Required | None = None - geo_regions: Required | geo_region_requirement.GeographicRegionRequirement | None = None - geo_regions_exclude: Required | geo_region_requirement.GeographicRegionRequirement | None = None - geo_metros: MetroRequirement | None = None - geo_metros_exclude: MetroRequirement | None = None - geo_places: geo_place_requirement.GeographicPlaceRequirement | None = None - geo_places_exclude: geo_place_requirement.GeographicPlaceRequirement | None = None - geo_postal_areas: Required | positive_postal_area_support.PositivePostalAreaSupport | None = ( - None - ) - geo_postal_areas_exclude: ( - Required | positive_postal_area_support.PositivePostalAreaSupport | None - ) = None - geo_proximity: Required | GeoProximity | None = None - daypart_targets: DaypartRequirement | None = None - audience_include: Required | None = None - audience_exclude: Required | None = None - signal_targeting_groups: Required | None = None - demographics: Required | Demographics | None = None - frequency_cap: Required | None = None + geo_countries: Annotated[Required | None, NoExplicitNull] = None + geo_countries_exclude: Annotated[Required | None, NoExplicitNull] = None + geo_regions: Annotated[ + Required | geo_region_requirement.GeographicRegionRequirement | None, NoExplicitNull + ] = None + geo_regions_exclude: Annotated[ + Required | geo_region_requirement.GeographicRegionRequirement | None, NoExplicitNull + ] = None + geo_metros: Annotated[MetroRequirement | None, NoExplicitNull] = None + geo_metros_exclude: Annotated[MetroRequirement | None, NoExplicitNull] = None + geo_places: Annotated[ + geo_place_requirement.GeographicPlaceRequirement | None, NoExplicitNull + ] = None + geo_places_exclude: Annotated[ + geo_place_requirement.GeographicPlaceRequirement | None, NoExplicitNull + ] = None + geo_postal_areas: Annotated[ + Required | positive_postal_area_support.PositivePostalAreaSupport | None, NoExplicitNull + ] = None + geo_postal_areas_exclude: Annotated[ + Required | positive_postal_area_support.PositivePostalAreaSupport | None, NoExplicitNull + ] = None + geo_proximity: Annotated[Required | GeoProximity | None, NoExplicitNull] = None + daypart_targets: Annotated[DaypartRequirement | None, NoExplicitNull] = None + audience_include: Annotated[Required | None, NoExplicitNull] = None + audience_exclude: Annotated[Required | None, NoExplicitNull] = None + signal_targeting_groups: Annotated[Required | None, NoExplicitNull] = None + demographics: Annotated[Required | Demographics | None, NoExplicitNull] = None + frequency_cap: Annotated[Required | None, NoExplicitNull] = None frequency_cap_support: Annotated[ frequency_cap_requirements.FrequencyCapRequirements | None, Field( description='Minimum structured package-cap support. Matches a broad legacy frequency_cap: true or a containing frequency_cap_support object. Mutually exclusive with a frequency_cap: true requirement, which would exclude every constrained product.' ), + NoExplicitNull, ] = None - property_list: Required | None = None - property_list_exclude: Required | None = None - collection_list: Required | None = None - collection_list_exclude: Required | None = None - placement_selection: Required | None = None - age_restriction: Required | None = None - device_platform: Required | None = None - device_platform_exclude: Required | None = None - device_type: Required | None = None - device_type_exclude: Required | None = None - browser: BrowserRequirement | None = None - browser_exclude: BrowserRequirement | None = None - store_catchments: Required | None = None - language: Required | None = None - keyword_targets: KeywordRequirement | None = None - negative_keywords: KeywordRequirement | None = None - ext: ext_1.ExtensionObject | None = None + property_list: Annotated[Required | None, NoExplicitNull] = None + property_list_exclude: Annotated[Required | None, NoExplicitNull] = None + collection_list: Annotated[Required | None, NoExplicitNull] = None + collection_list_exclude: Annotated[Required | None, NoExplicitNull] = None + placement_selection: Annotated[Required | None, NoExplicitNull] = None + age_restriction: Annotated[Required | None, NoExplicitNull] = None + device_platform: Annotated[Required | None, NoExplicitNull] = None + device_platform_exclude: Annotated[Required | None, NoExplicitNull] = None + device_type: Annotated[Required | None, NoExplicitNull] = None + device_type_exclude: Annotated[Required | None, NoExplicitNull] = None + browser: Annotated[BrowserRequirement | None, NoExplicitNull] = None + browser_exclude: Annotated[BrowserRequirement | None, NoExplicitNull] = None + store_catchments: Annotated[Required | None, NoExplicitNull] = None + language: Annotated[Required | None, NoExplicitNull] = None + keyword_targets: Annotated[KeywordRequirement | None, NoExplicitNull] = None + negative_keywords: Annotated[KeywordRequirement | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/targeting_overlay_support.py b/src/adcp/types/domains/core/targeting_overlay_support.py index 0e7756166..98d1f5da2 100644 --- a/src/adcp/types/domains/core/targeting_overlay_support.py +++ b/src/adcp/types/domains/core/targeting_overlay_support.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/targeting_overlay_support.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Any, Annotated, Literal -from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, SchemaInt +from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, RootModel, StringConstraints @@ -26,9 +26,9 @@ class PlacementSelection(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - max_values_per_package: Annotated[SchemaInt | None, Field(ge=1)] = None - max_packages: Annotated[SchemaInt | None, Field(ge=1)] = None - ext: ext_1.ExtensionObject | None = None + max_values_per_package: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_packages: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Supported(RootModel[Literal[True]]): @@ -46,7 +46,7 @@ class CountrySupport1(AdCPBaseModel): ge=1, ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CountrySupport(RootModel[Supported | CountrySupport1]): @@ -64,15 +64,16 @@ class MetroSupport1(AdCPBaseModel): extra='forbid', ) systems: Annotated[list[metro_system.MetroAreaSystem], Field(min_length=1)] - max_values_per_package: Annotated[SchemaInt | None, Field(ge=1)] = None + max_values_per_package: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_packages: Annotated[ SchemaInt | None, Field( description='Optional maximum number of independently targeted packages the seller will create from this configured product.', ge=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class MetroSupport(RootModel[Supported | MetroSupport1]): @@ -111,7 +112,7 @@ class PlaceCatalogSupport(AdCPBaseModel): description='Exact catalog versions selectable on later package overlays.', min_length=1 ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class PlaceSupport(AdCPBaseModel): @@ -122,15 +123,16 @@ class PlaceSupport(AdCPBaseModel): dict[GeoPlaceSystemKey, PlaceCatalogSupport], Field(min_length=1), ] - max_values_per_package: Annotated[SchemaInt | None, Field(ge=1)] = None + max_values_per_package: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_packages: Annotated[ SchemaInt | None, Field( description='Optional maximum number of independently place-targeted packages the seller will create from this configured product.', ge=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class KeywordSupport1(AdCPBaseModel): @@ -138,7 +140,7 @@ class KeywordSupport1(AdCPBaseModel): extra='forbid', ) supported_match_types: Annotated[list[match_type.MatchType], Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class KeywordSupport(RootModel[Supported | KeywordSupport1]): @@ -156,7 +158,7 @@ class BrowserSupport1(AdCPBaseModel): extra='forbid', ) families: Annotated[list[browser_family.BrowserFamily], Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class BrowserSupport(RootModel[Supported | BrowserSupport1]): @@ -189,8 +191,9 @@ class DaypartSupport1(AdCPBaseModel): Field( description="Concrete IANA timezone identifiers accepted by this product. true means every identifier valid in the seller's supported IANA TZDB; an array is the exact supported subset. Required when timezone_modes includes iana and forbidden otherwise." ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class DaypartSupport(RootModel[Literal[True] | DaypartSupport1]): @@ -207,17 +210,18 @@ class GeoProximity(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - radius: Supported | None = None - travel_time: Supported | None = None - geometry: Supported | None = None - transport_modes: Annotated[list[transport_mode.TransportMode] | None, Field(min_length=1)] = ( - None - ) + radius: Annotated[Supported | None, NoExplicitNull] = None + travel_time: Annotated[Supported | None, NoExplicitNull] = None + geometry: Annotated[Supported | None, NoExplicitNull] = None + transport_modes: Annotated[ + list[transport_mode.TransportMode] | None, Field(min_length=1), NoExplicitNull + ] = None max_values_per_package: Annotated[ SchemaInt | None, Field(description='Maximum number of proximity entries accepted on one package.', ge=1), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Demographics(AdCPBaseModel): @@ -225,54 +229,59 @@ class Demographics(AdCPBaseModel): extra='allow', ) age: Supported - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class TargetingOverlaySupport(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - geo_countries: CountrySupport | None = None - geo_countries_exclude: CountrySupport | None = None - geo_regions: Supported | geo_region_support.GeographicRegionSupport | None = None - geo_regions_exclude: Supported | geo_region_support.GeographicRegionSupport | None = None - geo_metros: MetroSupport | None = None - geo_metros_exclude: MetroSupport | None = None - geo_places: PlaceSupport | None = None - geo_places_exclude: PlaceSupport | None = None - geo_postal_areas: Supported | positive_postal_area_support.PositivePostalAreaSupport | None = ( - None - ) - geo_postal_areas_exclude: ( - Supported | positive_postal_area_support.PositivePostalAreaSupport | None - ) = None - geo_proximity: Supported | GeoProximity | None = None - daypart_targets: DaypartSupport | None = None - audience_include: Supported | None = None - audience_exclude: Supported | None = None - signal_targeting_groups: Supported | None = None - demographics: Supported | Demographics | None = None - frequency_cap: Supported | None = None + geo_countries: Annotated[CountrySupport | None, NoExplicitNull] = None + geo_countries_exclude: Annotated[CountrySupport | None, NoExplicitNull] = None + geo_regions: Annotated[ + Supported | geo_region_support.GeographicRegionSupport | None, NoExplicitNull + ] = None + geo_regions_exclude: Annotated[ + Supported | geo_region_support.GeographicRegionSupport | None, NoExplicitNull + ] = None + geo_metros: Annotated[MetroSupport | None, NoExplicitNull] = None + geo_metros_exclude: Annotated[MetroSupport | None, NoExplicitNull] = None + geo_places: Annotated[PlaceSupport | None, NoExplicitNull] = None + geo_places_exclude: Annotated[PlaceSupport | None, NoExplicitNull] = None + geo_postal_areas: Annotated[ + Supported | positive_postal_area_support.PositivePostalAreaSupport | None, NoExplicitNull + ] = None + geo_postal_areas_exclude: Annotated[ + Supported | positive_postal_area_support.PositivePostalAreaSupport | None, NoExplicitNull + ] = None + geo_proximity: Annotated[Supported | GeoProximity | None, NoExplicitNull] = None + daypart_targets: Annotated[DaypartSupport | None, NoExplicitNull] = None + audience_include: Annotated[Supported | None, NoExplicitNull] = None + audience_exclude: Annotated[Supported | None, NoExplicitNull] = None + signal_targeting_groups: Annotated[Supported | None, NoExplicitNull] = None + demographics: Annotated[Supported | Demographics | None, NoExplicitNull] = None + frequency_cap: Annotated[Supported | None, NoExplicitNull] = None frequency_cap_support: Annotated[ frequency_cap_constraints.FrequencyCapConstraints | None, Field( description='Independently positive constrained package-cap support. Mutually exclusive with legacy frequency_cap: a product declares one form or the other.' ), + NoExplicitNull, ] = None - property_list: Supported | None = None - property_list_exclude: Supported | None = None - collection_list: Supported | None = None - collection_list_exclude: Supported | None = None - placement_selection: Supported | PlacementSelection | None = None - age_restriction: Supported | None = None - device_platform: Supported | None = None - device_platform_exclude: Supported | None = None - device_type: Supported | None = None - device_type_exclude: Supported | None = None - browser: BrowserSupport | None = None - browser_exclude: BrowserSupport | None = None - store_catchments: Supported | None = None - language: Supported | None = None - keyword_targets: KeywordSupport | None = None - negative_keywords: KeywordSupport | None = None - ext: ext_1.ExtensionObject | None = None + property_list: Annotated[Supported | None, NoExplicitNull] = None + property_list_exclude: Annotated[Supported | None, NoExplicitNull] = None + collection_list: Annotated[Supported | None, NoExplicitNull] = None + collection_list_exclude: Annotated[Supported | None, NoExplicitNull] = None + placement_selection: Annotated[Supported | PlacementSelection | None, NoExplicitNull] = None + age_restriction: Annotated[Supported | None, NoExplicitNull] = None + device_platform: Annotated[Supported | None, NoExplicitNull] = None + device_platform_exclude: Annotated[Supported | None, NoExplicitNull] = None + device_type: Annotated[Supported | None, NoExplicitNull] = None + device_type_exclude: Annotated[Supported | None, NoExplicitNull] = None + browser: Annotated[BrowserSupport | None, NoExplicitNull] = None + browser_exclude: Annotated[BrowserSupport | None, NoExplicitNull] = None + store_catchments: Annotated[Supported | None, NoExplicitNull] = None + language: Annotated[Supported | None, NoExplicitNull] = None + keyword_targets: Annotated[KeywordSupport | None, NoExplicitNull] = None + negative_keywords: Annotated[KeywordSupport | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/tasks_get_request.py b/src/adcp/types/domains/core/tasks_get_request.py index 59654efce..bc0046fb3 100644 --- a/src/adcp/types/domains/core/tasks_get_request.py +++ b/src/adcp/types/domains/core/tasks_get_request.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: core/tasks_get_request.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from . import account_ref @@ -24,18 +25,21 @@ class TasksGetRequest(AdcpVersionEnvelope): Field( description='Account scope for the task lookup. Sellers MUST return REFERENCE_NOT_FOUND for a task_id that exists only under a different account or principal. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account.' ), + NoExplicitNull, ] = None include_history: Annotated[ StrictBool | None, Field( description='Include full conversation history for this task (may increase response size)' ), + NoExplicitNull, ] = False include_result: Annotated[ StrictBool | None, Field( description="Include the task's canonical terminal result payload when one exists. Defaults to false for lightweight status-only polls. When true, sellers MUST include result for completed, failed, or rejected terminal tasks when that task produced a terminal artifact; canceled tasks may have no result. The legacy singular error field remains a convenience for failed tasks but does not replace the canonical terminal result." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/tasks_get_response.py b/src/adcp/types/domains/core/tasks_get_response.py index 99e8c8f85..6a0bfee66 100644 --- a/src/adcp/types/domains/core/tasks_get_response.py +++ b/src/adcp/types/domains/core/tasks_get_response.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/tasks_get_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..enums import adcp_protocol, task_status @@ -23,15 +23,21 @@ class Progress(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None class Details(AdCPBaseModel): @@ -39,11 +45,15 @@ class Details(AdCPBaseModel): extra='allow', ) protocol: Annotated[ - adcp_protocol.AdcpProtocol | None, Field(description='AdCP protocol where error occurred') + adcp_protocol.AdcpProtocol | None, + Field(description='AdCP protocol where error occurred'), + NoExplicitNull, + ] = None + operation: Annotated[ + str | None, Field(description='Specific operation that failed'), NoExplicitNull ] = None - operation: Annotated[str | None, Field(description='Specific operation that failed')] = None specific_context: Annotated[ - dict[str, Any] | None, Field(description='Domain-specific error context') + dict[str, Any] | None, Field(description='Domain-specific error context'), NoExplicitNull ] = None @@ -53,7 +63,9 @@ class Error(AdCPBaseModel): ) code: Annotated[str, Field(description='Error code for programmatic handling')] message: Annotated[str, Field(description='Detailed error message')] - details: Annotated[Details | None, Field(description='Additional error context')] = None + details: Annotated[ + Details | None, Field(description='Additional error context'), NoExplicitNull + ] = None class Type(StrEnum): @@ -93,30 +105,38 @@ class TasksGetResponse(AdcpVersionEnvelope, ProtocolEnvelope): Field( description='When the task completed (ISO 8601, only for completed/failed/canceled tasks)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ - StrictBool | None, Field(description='Whether this task has webhook configuration') + StrictBool | None, + Field(description='Whether this task has webhook configuration'), + NoExplicitNull, ] = None progress: Annotated[ - Progress | None, Field(description='Progress information for long-running tasks') + Progress | None, + Field(description='Progress information for long-running tasks'), + NoExplicitNull, ] = None error: Annotated[ Error | None, Field( description='Convenience summary for failed tasks. When include_result was true and the canonical terminal result is also present, this error MUST agree with the canonical fatal error in result. A legacy poll carrying only this singular summary proves failure status but not equivalence to a richer terminal webhook artifact.' ), + NoExplicitNull, ] = None history: Annotated[ list[HistoryItem] | None, Field( description='Complete conversation history for this task (only included if include_history was true in request)' ), + NoExplicitNull, ] = None result: Annotated[ dict[str, Any] | None, Field( description='Canonical task-specific terminal payload. Present when include_result was true and a completed, failed, or rejected task produced a terminal artifact; canceled tasks may omit it. For failed tasks, the singular error field is a convenience summary and MUST agree with the canonical fatal error represented here. Consumers and sellers MUST resolve and validate the exact schema through manifest.task_result_resolution: use terminal_schema_overrides[task_type] when present, otherwise tools[task_type].response_schema. The polling envelope keeps this field generic so tasks/get does not embed every task response schema.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/tasks_list_request.py b/src/adcp/types/domains/core/tasks_list_request.py index 2cb1b573d..2b2dc2036 100644 --- a/src/adcp/types/domains/core/tasks_list_request.py +++ b/src/adcp/types/domains/core/tasks_list_request.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/tasks_list_request.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import adcp_protocol, sort_direction, task_status @@ -24,53 +24,69 @@ class Filters(AdCPBaseModel): extra='allow', ) protocol: Annotated[ - adcp_protocol.AdcpProtocol | None, Field(description='Filter by single AdCP protocol') + adcp_protocol.AdcpProtocol | None, + Field(description='Filter by single AdCP protocol'), + NoExplicitNull, ] = None protocols: Annotated[ list[adcp_protocol.AdcpProtocol] | None, Field(description='Filter by multiple AdCP protocols', min_length=1), + NoExplicitNull, ] = None status: Annotated[ - task_status.TaskStatus | None, Field(description='Filter by single task status') + task_status.TaskStatus | None, + Field(description='Filter by single task status'), + NoExplicitNull, ] = None statuses: Annotated[ list[task_status.TaskStatus] | None, Field(description='Filter by multiple task statuses', min_length=1), + NoExplicitNull, ] = None task_type: Annotated[ - task_type_1.TaskType | None, Field(description='Filter by single task type') + task_type_1.TaskType | None, Field(description='Filter by single task type'), NoExplicitNull ] = None task_types: Annotated[ list[task_type_1.TaskType] | None, Field(description='Filter by multiple task types', min_length=1), + NoExplicitNull, ] = None created_after: Annotated[ - AwareDatetime | None, Field(description='Filter tasks created after this date (ISO 8601)') + AwareDatetime | None, + Field(description='Filter tasks created after this date (ISO 8601)'), + NoExplicitNull, ] = None created_before: Annotated[ - AwareDatetime | None, Field(description='Filter tasks created before this date (ISO 8601)') + AwareDatetime | None, + Field(description='Filter tasks created before this date (ISO 8601)'), + NoExplicitNull, ] = None updated_after: Annotated[ AwareDatetime | None, Field(description='Filter tasks last updated after this date (ISO 8601)'), + NoExplicitNull, ] = None updated_before: Annotated[ AwareDatetime | None, Field(description='Filter tasks last updated before this date (ISO 8601)'), + NoExplicitNull, ] = None task_ids: Annotated[ list[str] | None, Field(description='Filter by specific task IDs', max_length=100, min_length=1), + NoExplicitNull, ] = None context_contains: Annotated[ str | None, Field( description='Filter tasks where context contains this text (searches media_buy_id, signal_id, etc.)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ StrictBool | None, Field(description='Filter tasks that have webhook configuration when true'), + NoExplicitNull, ] = None @@ -86,9 +102,11 @@ class Sort(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - field: Annotated[Field1 | None, Field(description='Field to sort by')] = Field1.created_at + field: Annotated[Field1 | None, Field(description='Field to sort by'), NoExplicitNull] = ( + Field1.created_at + ) direction: Annotated[ - sort_direction.SortDirection | None, Field(description='Sort direction') + sort_direction.SortDirection | None, Field(description='Sort direction'), NoExplicitNull ] = sort_direction.SortDirection.desc @@ -101,17 +119,19 @@ class TasksListRequest(AdcpVersionEnvelope): Field( description="Account scope for task reconciliation. Sellers MUST only return tasks created for the caller's authenticated account + principal pair. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account." ), + NoExplicitNull, ] = None - filters: Annotated[Filters | None, Field(description='Filter criteria for querying tasks')] = ( - None - ) - sort: Annotated[Sort | None, Field(description='Sorting parameters')] = None - pagination: pagination_request.PaginationRequest | None = None + filters: Annotated[ + Filters | None, Field(description='Filter criteria for querying tasks'), NoExplicitNull + ] = None + sort: Annotated[Sort | None, Field(description='Sorting parameters'), NoExplicitNull] = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None include_history: Annotated[ StrictBool | None, Field( description='Include full conversation history for each task (may significantly increase response size)' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/tasks_list_response.py b/src/adcp/types/domains/core/tasks_list_response.py index 0f5b7e550..9bc088955 100644 --- a/src/adcp/types/domains/core/tasks_list_response.py +++ b/src/adcp/types/domains/core/tasks_list_response.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/tasks_list_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..enums import task_status @@ -26,12 +26,17 @@ class DomainBreakdown(AdCPBaseModel): media_buy: Annotated[ SchemaInt | None, Field(alias='media-buy', description='Number of media-buy tasks in results', ge=0), + NoExplicitNull, ] = None signals: Annotated[ - SchemaInt | None, Field(description='Number of signals tasks in results', ge=0) + SchemaInt | None, + Field(description='Number of signals tasks in results', ge=0), + NoExplicitNull, ] = None creative: Annotated[ - SchemaInt | None, Field(description='Number of creative tasks in results', ge=0) + SchemaInt | None, + Field(description='Number of creative tasks in results', ge=0), + NoExplicitNull, ] = None @@ -55,21 +60,26 @@ class QuerySummary(AdCPBaseModel): total_matching: Annotated[ SchemaInt | None, Field(description='Total number of tasks matching filters (across all pages)', ge=0), + NoExplicitNull, ] = None returned: Annotated[ - SchemaInt | None, Field(description='Number of tasks returned in this response', ge=0) + SchemaInt | None, + Field(description='Number of tasks returned in this response', ge=0), + NoExplicitNull, ] = None domain_breakdown: Annotated[ - DomainBreakdown | None, Field(description='Count of tasks by domain') + DomainBreakdown | None, Field(description='Count of tasks by domain'), NoExplicitNull ] = None status_breakdown: Annotated[ - dict[str, SchemaInt] | None, Field(description='Count of tasks by status') + dict[str, SchemaInt] | None, Field(description='Count of tasks by status'), NoExplicitNull ] = None filters_applied: Annotated[ - list[str] | None, Field(description='List of filters that were applied to the query') + list[str] | None, + Field(description='List of filters that were applied to the query'), + NoExplicitNull, ] = None sort_applied: Annotated[ - SortApplied | None, Field(description='Sort order that was applied') + SortApplied | None, Field(description='Sort order that was applied'), NoExplicitNull ] = None @@ -98,9 +108,12 @@ class Task(AdCPBaseModel): Field( description='When the task completed (ISO 8601, only for completed/failed/canceled tasks)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ - StrictBool | None, Field(description='Whether this task has webhook configuration') + StrictBool | None, + Field(description='Whether this task has webhook configuration'), + NoExplicitNull, ] = None @@ -113,5 +126,5 @@ class TasksListResponse(AdcpVersionEnvelope, ProtocolEnvelope): ] tasks: Annotated[list[Task], Field(description='Array of tasks matching the query criteria')] pagination: pagination_response.PaginationResponse - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/tracker_execution_selector.py b/src/adcp/types/domains/core/tracker_execution_selector.py index c3b58ceed..b0b04b94b 100644 --- a/src/adcp/types/domains/core/tracker_execution_selector.py +++ b/src/adcp/types/domains/core/tracker_execution_selector.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/tracker_execution_selector.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import pixel_tracking_event, tracker_execution_actor, tracker_firing_path @@ -35,7 +35,9 @@ class TrackerExecutionSelector1(AdCPBaseModel): ), ] = 'img' custom_event_name: Annotated[ - str | None, Field(description='Required only when event is custom.', min_length=1) + str | None, + Field(description='Required only when event is custom.', min_length=1), + NoExplicitNull, ] = None execution_actor: tracker_execution_actor.TrackerExecutionActor firing_paths: Annotated[ @@ -62,7 +64,7 @@ class TrackerExecutionSelector3(DaastTrackerConstraints): daast_versions: daast_tracker_constraints.DaastVersions daast_event: daast_tracker_constraints.DaastEvent target: daast_tracker_constraints.DaastTarget - offset: daast_tracker_constraints.DaastOffset | None = None + offset: Annotated[daast_tracker_constraints.DaastOffset | None, NoExplicitNull] = None execution_actor: tracker_execution_actor.TrackerExecutionActor firing_paths: Annotated[ list[tracker_firing_path.TrackerFiringPath], @@ -88,7 +90,7 @@ class TrackerExecutionSelector2(VastTrackerConstraints): vast_versions: vast_tracker_constraints.VastVersions vast_event: vast_tracker_constraints.VastEvent target: vast_tracker_constraints.VastTarget - offset: vast_tracker_constraints.VastOffset | None = None + offset: Annotated[vast_tracker_constraints.VastOffset | None, NoExplicitNull] = None execution_actor: tracker_execution_actor.TrackerExecutionActor firing_paths: Annotated[ list[tracker_firing_path.TrackerFiringPath], diff --git a/src/adcp/types/domains/core/transformer.py b/src/adcp/types/domains/core/transformer.py index 59666ebe1..c694305ec 100644 --- a/src/adcp/types/domains/core/transformer.py +++ b/src/adcp/types/domains/core/transformer.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/transformer.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field, StrictBool, model_validator @@ -59,6 +59,7 @@ class VoiceSynthesisRefItem(AdCPBaseModel): Field( description='Optional rights offering or buyer-specific grant identifier associated with this provisioned voice. Brand-side voice_synthesis uses rights_offering_id for the configuration-time offering anchor. This transformer field may carry that offering ID or a grant ID after provisioning; it remains provenance metadata only, not a build_creative rights token.' ), + NoExplicitNull, ] = None @@ -90,23 +91,29 @@ class Multiplicity(AdCPBaseModel): extra='allow', ) supports_catalog_fanout: Annotated[ - StrictBool | None, Field(description='Whether this transformer accepts max_creatives.') + StrictBool | None, + Field(description='Whether this transformer accepts max_creatives.'), + NoExplicitNull, ] = False max_creatives_limit: Annotated[ SchemaInt | None, Field(description='Per-transformer ceiling on max_creatives (≤ the agent ceiling).', ge=1), + NoExplicitNull, ] = None supports_variants: Annotated[ StrictBool | None, Field(description='Whether this transformer accepts max_variants > 1 / variant_axis.'), + NoExplicitNull, ] = False max_variants_limit: Annotated[ SchemaInt | None, Field(description='Per-transformer ceiling on max_variants (≤ the agent ceiling).', ge=1), + NoExplicitNull, ] = None variant_dimensions: Annotated[ list[VariantDimension] | None, Field(description="Variant axis dimensions this transformer supports (⊆ the agent's)."), + NoExplicitNull, ] = None @@ -116,6 +123,7 @@ class InputFormat1(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -123,12 +131,14 @@ class InputFormat1(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -136,60 +146,70 @@ class InputFormat1(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -197,12 +217,14 @@ class InputFormat1(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: image.CanonicalFormatImage @@ -214,6 +236,7 @@ class InputFormat2(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -221,12 +244,14 @@ class InputFormat2(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -234,60 +259,70 @@ class InputFormat2(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -295,12 +330,14 @@ class InputFormat2(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: html5.CanonicalFormatHtml5Banner @@ -312,6 +349,7 @@ class InputFormat3(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -319,12 +357,14 @@ class InputFormat3(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -332,60 +372,70 @@ class InputFormat3(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -393,12 +443,14 @@ class InputFormat3(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: display_tag.CanonicalFormatDisplayTag @@ -410,6 +462,7 @@ class InputFormat4(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -417,12 +470,14 @@ class InputFormat4(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -430,60 +485,70 @@ class InputFormat4(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -491,12 +556,14 @@ class InputFormat4(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: image_carousel.CanonicalFormatImageCarousel @@ -508,6 +575,7 @@ class InputFormat5(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -515,12 +583,14 @@ class InputFormat5(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -528,60 +598,70 @@ class InputFormat5(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -589,12 +669,14 @@ class InputFormat5(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: video_hosted.CanonicalFormatHostedVideo @@ -606,6 +688,7 @@ class InputFormat6(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -613,12 +696,14 @@ class InputFormat6(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -626,60 +711,70 @@ class InputFormat6(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -687,12 +782,14 @@ class InputFormat6(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: video_vast.CanonicalFormatVastVideo @@ -704,6 +801,7 @@ class InputFormat7(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -711,12 +809,14 @@ class InputFormat7(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -724,60 +824,70 @@ class InputFormat7(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -785,12 +895,14 @@ class InputFormat7(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: audio_hosted.CanonicalFormatHostedAudio @@ -802,6 +914,7 @@ class InputFormat8(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -809,12 +922,14 @@ class InputFormat8(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -822,60 +937,70 @@ class InputFormat8(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -883,12 +1008,14 @@ class InputFormat8(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_vast'] = 'audio_vast' params: audio_vast.CanonicalFormatVastAudio @@ -900,6 +1027,7 @@ class InputFormat9(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -907,12 +1035,14 @@ class InputFormat9(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -920,60 +1050,70 @@ class InputFormat9(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -981,12 +1121,14 @@ class InputFormat9(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: audio_daast.CanonicalFormatDaastAudio @@ -998,6 +1140,7 @@ class InputFormat10(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1005,12 +1148,14 @@ class InputFormat10(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1018,60 +1163,70 @@ class InputFormat10(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1079,12 +1234,14 @@ class InputFormat10(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven @@ -1096,6 +1253,7 @@ class InputFormat11(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1103,12 +1261,14 @@ class InputFormat11(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1116,60 +1276,70 @@ class InputFormat11(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1177,12 +1347,14 @@ class InputFormat11(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: native_in_feed.CanonicalFormatNativeInFeed @@ -1194,6 +1366,7 @@ class InputFormat12(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1201,12 +1374,14 @@ class InputFormat12(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1214,60 +1389,70 @@ class InputFormat12(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1275,12 +1460,14 @@ class InputFormat12(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: responsive_creative.CanonicalFormatResponsiveCreative @@ -1292,6 +1479,7 @@ class InputFormat13(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1299,12 +1487,14 @@ class InputFormat13(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1312,60 +1502,70 @@ class InputFormat13(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1373,12 +1573,14 @@ class InputFormat13(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement @@ -1390,6 +1592,7 @@ class InputFormat14(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1397,12 +1600,14 @@ class InputFormat14(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1410,60 +1615,70 @@ class InputFormat14(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1471,12 +1686,14 @@ class InputFormat14(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay @@ -1488,6 +1705,7 @@ class InputFormat15(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1495,12 +1713,14 @@ class InputFormat15(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1508,60 +1728,70 @@ class InputFormat15(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1569,12 +1799,14 @@ class InputFormat15(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['coordinated_placements'] = 'coordinated_placements' params: coordinated_placements.CanonicalFormatCoordinatedPlacements @@ -1586,6 +1818,7 @@ class InputFormat16(AdCPBaseModel): Field( description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." ), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -1593,12 +1826,14 @@ class InputFormat16(AdCPBaseModel): 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])?)*$', ), + NoExplicitNull, ] = None tracker_execution_contract: Annotated[ tracker_execution_contract_1.TrackerExecutionContract | None, Field( description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -1606,60 +1841,70 @@ class InputFormat16(AdCPBaseModel): description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', min_length=1, ), + NoExplicitNull, ] = None technical_requirements_complete: Annotated[ StrictBool | None, Field( description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None sample_render_url: Annotated[ AnyUrl | None, Field( description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' ), + NoExplicitNull, ] = 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`." ), + NoExplicitNull, ] = 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." ), + NoExplicitNull, ] = None locale_policy: Annotated[ creative_locale_policy.CreativeLocalePolicy | None, Field( description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' ), + NoExplicitNull, ] = None canonical_formats_only: Annotated[ StrictBool | 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.' ), + NoExplicitNull, ] = False experimental: Annotated[ StrictBool | None, Field( description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." ), + NoExplicitNull, ] = 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) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are 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 may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' ), + NoExplicitNull, ] = None v1_format_ref: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1667,12 +1912,14 @@ class InputFormat16(AdCPBaseModel): 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/` + `id: ` (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/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — 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, ), + NoExplicitNull, ] = 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.' ), + NoExplicitNull, ] = None format_kind: Literal['custom'] = 'custom' params: Annotated[ @@ -1786,12 +2033,14 @@ class Transformer(AdCPBaseModel): description: Annotated[ str | None, Field(description='Plain-text explanation of what this transformer produces and how.'), + NoExplicitNull, ] = None metadata: Annotated[ dict[str, Any] | None, Field( description='Transformer-specific attributes a buyer can filter or display (e.g. provider, modality, language).' ), + NoExplicitNull, ] = None voice_synthesis_ref: Annotated[ list[VoiceSynthesisRefItem] | None, @@ -1799,6 +2048,7 @@ class Transformer(AdCPBaseModel): description='Optional discovery/audit anchors for voice transformers provisioned from brand-agent voice_synthesis entries. Informational only: these references help buyers match a discovered transformer to brand/rights-agent provenance, but they do not assert build-time authorization or require the creative agent to perform rights-token validation.', min_length=1, ), + NoExplicitNull, ] = None input_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1806,6 +2056,7 @@ class Transformer(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named formats this transformer accepts as input. Use input_formats with canonical declarations.', ), + NoExplicitNull, ] = None output_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -1814,6 +2065,7 @@ class Transformer(AdCPBaseModel): description='**DEPRECATED in 3.2.** Legacy named formats this transformer can produce. Use output_capability_ids.', min_length=1, ), + NoExplicitNull, ] = None input_formats: Annotated[ list[InputFormat] | None, @@ -1821,6 +2073,7 @@ class Transformer(AdCPBaseModel): description='Canonical format declarations this transformer accepts as input. Omitted means it builds from a brief or raw assets rather than transforming an existing creative. Compatibility uses canonical constraint satisfaction, not identifier equality. Transformer self-description has no seller production authority, so tracker_execution_contract and tracker_execution_contract_digest are forbidden.', min_length=1, ), + NoExplicitNull, ] = None output_capability_ids: Annotated[ list[OutputCapabilityId] | None, @@ -1828,12 +2081,14 @@ class Transformer(AdCPBaseModel): description="Canonical output capabilities this transformer can produce. Every value MUST match this agent's get_adcp_capabilities `creative.supported_formats[].capability_id`. A build_creative request's target_capability_id(s) MUST be a subset of this array.", min_length=1, ), + NoExplicitNull, ] = None params: Annotated[ list[transformer_param.TransformerParam] | None, Field( description="Configuration knobs this transformer exposes. The buyer supplies values in build_creative `config`, keyed by each param's `field`. Enumerable param values (e.g. account-specific voices) are returned only when requested via list_transformers `expand_params`." ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -1841,12 +2096,14 @@ class Transformer(AdCPBaseModel): description='Per-account rate-card options for using this transformer. Present when the list_transformers request set include_pricing=true with an account. The applied option is echoed back per-leaf on the build_creative response and reconciled via report_usage.', min_length=1, ), + NoExplicitNull, ] = None multiplicity: Annotated[ Multiplicity | None, Field( description="Optional per-transformer fan-out limits that NARROW the agent-level get_adcp_capabilities `creative.multiplicity`. Same shape as the agent-level object. When present, this transformer's authoritative; its ceilings (max_creatives_limit / max_variants_limit) MUST NOT exceed the agent ceilings, and its variant_dimensions MUST be a subset of the agent's. Omit to inherit the agent-level capability unchanged." ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/transformer_param.py b/src/adcp/types/domains/core/transformer_param.py index 2651bd37c..51341f697 100644 --- a/src/adcp/types/domains/core/transformer_param.py +++ b/src/adcp/types/domains/core/transformer_param.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/transformer_param.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -32,12 +32,15 @@ class Option(AdCPBaseModel): value: Annotated[ Any, Field(description='The value the buyer passes in `config` for this field.') ] - label: Annotated[str | None, Field(description='Human-readable label for this option.')] = None + label: Annotated[ + str | None, Field(description='Human-readable label for this option.'), NoExplicitNull + ] = None metadata: Annotated[ dict[str, Any] | None, Field( description='Option-specific attributes the buyer can filter or display (e.g. for a voice: language, gender, provider, custom).' ), + NoExplicitNull, ] = None @@ -66,6 +69,7 @@ class TransformerParam(AdCPBaseModel): description='Optional maximum character length for a `free_text` param. Omit for no declared limit.', ge=1, ), + NoExplicitNull, ] = None allowed_values: Annotated[ list[Any] | None, @@ -73,26 +77,31 @@ class TransformerParam(AdCPBaseModel): description='The closed set of legal values. Present when `value_source` is `inline`.', min_length=1, ), + NoExplicitNull, ] = None minimum: Annotated[ StrictFloat | None, Field(description='Inclusive lower bound. Present when `value_source` is `range`.'), + NoExplicitNull, ] = None maximum: Annotated[ StrictFloat | None, Field(description='Inclusive upper bound. Present when `value_source` is `range`.'), + NoExplicitNull, ] = None options: Annotated[ list[Option] | None, Field( description="Account-scoped legal values for an `enumerable` param. Populated ONLY when this param's `field` was named in the list_transformers `expand_params` request — otherwise omitted (the buyer enumerates on demand). Brief-filtered and paginated; use `options_cursor` for the next page." ), + NoExplicitNull, ] = None options_cursor: Annotated[ str | None, Field( description="Opaque pagination cursor for this param's `options[]`. Present when more option values are available than were returned. Pass back to list_transformers (scoped to this transformer + field) to fetch the next page." ), + NoExplicitNull, ] = None default: Annotated[ Any | None, @@ -105,7 +114,10 @@ class TransformerParam(AdCPBaseModel): Field( description='Whether the buyer MUST supply this field in `config`. When false and no `default` is declared, the agent chooses.' ), + NoExplicitNull, ] = False description: Annotated[ - str | None, Field(description='Human-readable explanation of what this knob does.') + str | None, + Field(description='Human-readable explanation of what this knob does.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/truncation_sentinel.py b/src/adcp/types/domains/core/truncation_sentinel.py index e83010fec..1e2471f03 100644 --- a/src/adcp/types/domains/core/truncation_sentinel.py +++ b/src/adcp/types/domains/core/truncation_sentinel.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/truncation_sentinel.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -26,12 +26,14 @@ class FieldTruncation(AdCPBaseModel): Field( description='Optional bounded human-readable excerpt of the original content. Sellers SHOULD keep previews under 8 KiB and SHOULD truncate on a UTF-8 codepoint boundary. Absent when the seller cannot safely surface even a preview (e.g., the original content is binary and not text-decodable, or seller policy forbids any payload surfacing). Receivers MUST NOT parse `preview` as a complete representation of the original value.' ), + NoExplicitNull, ] = None preview_format: Annotated[ str | None, Field( description='Hint for how to render `preview`. Common values: `text` (plain UTF-8), `json` (JSON fragment), `base64` (base64-encoded binary), `xml`, `html`. The list is open — adopters MAY use other MIME-derived shorthands. Receivers SHOULD treat unknown values as `text` and SHOULD NOT reject the sentinel when the value is unfamiliar.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/user_match.py b/src/adcp/types/domains/core/user_match.py index 857fd05e9..47a687ee4 100644 --- a/src/adcp/types/domains/core/user_match.py +++ b/src/adcp/types/domains/core/user_match.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/user_match.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..enums import uid_type @@ -29,7 +29,9 @@ class UserMatch(AdCPBaseModel): extra='allow', ) uids: Annotated[ - list[Uid] | None, Field(description='Universal ID values for user matching', min_length=1) + list[Uid] | None, + Field(description='Universal ID values for user matching', min_length=1), + NoExplicitNull, ] = None hashed_email: Annotated[ str | None, @@ -37,6 +39,7 @@ class UserMatch(AdCPBaseModel): description='SHA-256 hash of lowercase, trimmed email address. Buyer must normalize before hashing: lowercase, trim whitespace. Pseudonymous PII, not anonymous — the email namespace is small enough that an unsalted SHA-256 is recoverable via precomputed dictionaries. Treat as PII for retention, consent, and access-control purposes. See docs/reference/privacy-considerations#unsalted-hashed-identifiers-are-pseudonymous-not-anonymous.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None hashed_phone: Annotated[ str | None, @@ -44,22 +47,29 @@ class UserMatch(AdCPBaseModel): description='SHA-256 hash of E.164-formatted phone number (e.g. +12065551234). Buyer must normalize to E.164 before hashing. Pseudonymous PII, not anonymous — the E.164 namespace is small enough that an unsalted SHA-256 is recoverable via precomputed dictionaries. Treat as PII for retention, consent, and access-control purposes. See docs/reference/privacy-considerations#unsalted-hashed-identifiers-are-pseudonymous-not-anonymous.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None click_id: Annotated[ str | None, Field(description='Platform click identifier (fbclid, gclid, ttclid, ScCid, etc.)'), + NoExplicitNull, ] = None click_id_type: Annotated[ str | None, Field(description='Type of click identifier (e.g. fbclid, gclid, ttclid, msclkid, ScCid)'), + NoExplicitNull, ] = None client_ip: Annotated[ - str | None, Field(description='Client IP address for probabilistic matching') + str | None, + Field(description='Client IP address for probabilistic matching'), + NoExplicitNull, ] = None client_user_agent: Annotated[ - str | None, Field(description='Client user agent string for probabilistic matching') + str | None, + Field(description='Client user agent string for probabilistic matching'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> UserMatch: diff --git a/src/adcp/types/domains/core/vast_media_file_requirements.py b/src/adcp/types/domains/core/vast_media_file_requirements.py index 25dd654bb..0df5a4f95 100644 --- a/src/adcp/types/domains/core/vast_media_file_requirements.py +++ b/src/adcp/types/domains/core/vast_media_file_requirements.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/vast_media_file_requirements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -40,6 +40,7 @@ class VastMediafileRequirements(AdCPBaseModel): description='Accepted values of the required `MediaFile@delivery` attribute. `progressive` identifies a directly downloadable media file; `streaming` identifies a streaming media resource.', min_length=1, ), + NoExplicitNull, ] = None mime_types: Annotated[ list[MimeType] | None, @@ -47,6 +48,7 @@ class VastMediafileRequirements(AdCPBaseModel): description='Accepted MIME types from `MediaFile@type`, compared case-insensitively after trimming optional whitespace. Parameters are not accepted in this field. Examples: `video/mp4`, `video/webm`, `audio/mpeg`, `audio/aac`.', min_length=1, ), + NoExplicitNull, ] = None containers: Annotated[ list[Container] | None, @@ -54,6 +56,7 @@ class VastMediafileRequirements(AdCPBaseModel): description='Accepted normalized container identifiers, such as `mp4`, `webm`, or `mpeg_ts`. A receiver MUST determine the container from authoritative metadata or safe byte inspection and MUST NOT guess it only from the media URI suffix.', min_length=1, ), + NoExplicitNull, ] = None codecs: Annotated[ list[Codec] | None, @@ -61,18 +64,27 @@ class VastMediafileRequirements(AdCPBaseModel): description='Accepted codec identifiers for `MediaFile@codec`, using the codec syntax referenced by the applicable VAST version. A missing codec attribute does not satisfy a declared codec constraint unless safe inspection establishes the codec.', min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ - SchemaInt | None, Field(description='Minimum accepted `MediaFile@width` in pixels.', ge=1) + SchemaInt | None, + Field(description='Minimum accepted `MediaFile@width` in pixels.', ge=1), + NoExplicitNull, ] = None max_width: Annotated[ - SchemaInt | None, Field(description='Maximum accepted `MediaFile@width` in pixels.', ge=1) + SchemaInt | None, + Field(description='Maximum accepted `MediaFile@width` in pixels.', ge=1), + NoExplicitNull, ] = None min_height: Annotated[ - SchemaInt | None, Field(description='Minimum accepted `MediaFile@height` in pixels.', ge=1) + SchemaInt | None, + Field(description='Minimum accepted `MediaFile@height` in pixels.', ge=1), + NoExplicitNull, ] = None max_height: Annotated[ - SchemaInt | None, Field(description='Maximum accepted `MediaFile@height` in pixels.', ge=1) + SchemaInt | None, + Field(description='Maximum accepted `MediaFile@height` in pixels.', ge=1), + NoExplicitNull, ] = None min_bitrate_kbps: Annotated[ SchemaInt | None, @@ -80,6 +92,7 @@ class VastMediafileRequirements(AdCPBaseModel): description='Minimum accepted MediaFile bitrate in kilobits per second. A fixed `MediaFile@bitrate` must be at least this value. For adaptive/streaming media, the declared `MediaFile@minBitrate` must be at least this value. Safe byte inspection may establish the value when metadata is absent.', ge=1, ), + NoExplicitNull, ] = None max_bitrate_kbps: Annotated[ SchemaInt | None, @@ -87,6 +100,7 @@ class VastMediafileRequirements(AdCPBaseModel): description='Maximum accepted MediaFile bitrate in kilobits per second. A fixed `MediaFile@bitrate` must not exceed this value. For adaptive/streaming media, the declared `MediaFile@maxBitrate` must not exceed this value. Safe byte inspection may establish the value when metadata is absent.', ge=1, ), + NoExplicitNull, ] = None max_file_size_bytes: Annotated[ SchemaInt | None, @@ -94,4 +108,5 @@ class VastMediafileRequirements(AdCPBaseModel): description='Maximum accepted MediaFile size in exact bytes. `MediaFile@fileSize`, when present, is expressed in bytes; a receiver MAY verify it against safely fetched media bytes and MUST use the verified byte count if the values disagree.', ge=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/vast_tracker_constraints.py b/src/adcp/types/domains/core/vast_tracker_constraints.py index 94f3664d0..21205d470 100644 --- a/src/adcp/types/domains/core/vast_tracker_constraints.py +++ b/src/adcp/types/domains/core/vast_tracker_constraints.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/vast_tracker_constraints.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import Field, RootModel @@ -60,7 +60,7 @@ class VastVersions(RootModel[list[vast_version.VastVersion]]): class VastTrackerConstraints(AdCPBaseModel): - vast_event: VastEvent | None = None - target: VastTarget | None = VastTarget.linear - offset: VastOffset | None = None - vast_versions: VastVersions | None = None + vast_event: Annotated[VastEvent | None, NoExplicitNull] = None + target: Annotated[VastTarget | None, NoExplicitNull] = VastTarget.linear + offset: Annotated[VastOffset | None, NoExplicitNull] = None + vast_versions: Annotated[VastVersions | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/vehicle_item.py b/src/adcp/types/domains/core/vehicle_item.py index b3c6b9bcf..546ddbf55 100644 --- a/src/adcp/types/domains/core/vehicle_item.py +++ b/src/adcp/types/domains/core/vehicle_item.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/vehicle_item.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from . import ext as ext_1 @@ -81,29 +81,56 @@ class VehicleItem(AdCPBaseModel): make: Annotated[str, Field(description="Vehicle manufacturer (e.g., 'Honda', 'Ford', 'BMW').")] model: Annotated[str, Field(description="Vehicle model (e.g., 'Civic', 'F-150', 'X5').")] year: Annotated[SchemaInt, Field(description='Model year.', ge=1900)] - price: Annotated[price_1.Price | None, Field(description='Vehicle price.')] = None - condition: Annotated[Condition | None, Field(description='Vehicle condition.')] = None + price: Annotated[price_1.Price | None, Field(description='Vehicle price.'), NoExplicitNull] = ( + None + ) + condition: Annotated[ + Condition | None, Field(description='Vehicle condition.'), NoExplicitNull + ] = None vin: Annotated[ - str | None, Field(description='Vehicle Identification Number (17-character VIN).') + str | None, + Field(description='Vehicle Identification Number (17-character VIN).'), + NoExplicitNull, ] = None trim: Annotated[ - str | None, Field(description="Trim level (e.g., 'EX', 'Limited', 'Sport').") + str | None, + Field(description="Trim level (e.g., 'EX', 'Limited', 'Sport')."), + NoExplicitNull, + ] = None + mileage: Annotated[Mileage | None, Field(description='Odometer reading.'), NoExplicitNull] = ( + None + ) + body_style: Annotated[ + BodyStyle | None, Field(description='Vehicle body style.'), NoExplicitNull + ] = None + transmission: Annotated[ + Transmission | None, Field(description='Transmission type.'), NoExplicitNull + ] = None + fuel_type: Annotated[ + FuelType | None, Field(description='Fuel or powertrain type.'), NoExplicitNull + ] = None + exterior_color: Annotated[str | None, Field(description='Exterior color.'), NoExplicitNull] = ( + None + ) + interior_color: Annotated[str | None, Field(description='Interior color.'), NoExplicitNull] = ( + None + ) + location: Annotated[ + Location | None, Field(description='Dealer or vehicle location.'), NoExplicitNull + ] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Primary vehicle image URL.'), NoExplicitNull + ] = None + url: Annotated[ + AnyUrl | None, Field(description='Vehicle listing page URL.'), NoExplicitNull ] = None - mileage: Annotated[Mileage | None, Field(description='Odometer reading.')] = None - body_style: Annotated[BodyStyle | None, Field(description='Vehicle body style.')] = None - transmission: Annotated[Transmission | None, Field(description='Transmission type.')] = None - fuel_type: Annotated[FuelType | None, Field(description='Fuel or powertrain type.')] = None - exterior_color: Annotated[str | None, Field(description='Exterior color.')] = None - interior_color: Annotated[str | None, Field(description='Interior color.')] = None - location: Annotated[Location | None, Field(description='Dealer or vehicle location.')] = None - image_url: Annotated[AnyUrl | None, Field(description='Primary vehicle image URL.')] = None - url: Annotated[AnyUrl | None, Field(description='Vehicle listing page URL.')] = None tags: Annotated[ list[str] | None, Field( description="Tags for filtering (e.g., 'low-mileage', 'one-owner', 'dealer-certified').", min_length=1, ), + NoExplicitNull, ] = None assets: Annotated[ list[offering_asset_group.OfferingAssetGroup] | None, @@ -111,5 +138,6 @@ class VehicleItem(AdCPBaseModel): description="Typed creative asset pools for this vehicle. Uses the same OfferingAssetGroup structure as offering-type catalogs. Standard group IDs: 'images_landscape' (exterior hero), 'images_vertical' (9:16 for Stories), 'images_square' (1:1). Enables formats to declare typed image requirements that map unambiguously to the right asset regardless of platform.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/vendor_metric_optimization_supported_metric.py b/src/adcp/types/domains/core/vendor_metric_optimization_supported_metric.py index 613c76d8c..f2b1c2b18 100644 --- a/src/adcp/types/domains/core/vendor_metric_optimization_supported_metric.py +++ b/src/adcp/types/domains/core/vendor_metric_optimization_supported_metric.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/vendor_metric_optimization_supported_metric.json -# timestamp: 2026-05-30T17:30:49+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from . import brand_ref, vendor_metric_id @@ -39,4 +39,5 @@ class VendorMetricOptimizationSupportedMetric(AdCPBaseModel): Field( description='Target kinds available for `vendor_metric` goals against this `(vendor, metric_id)` pair. Values match `target.kind` on the optimization goal. `cost_per` — target cost per metric unit (e.g., $0.05 per attention-second). `threshold_rate` — minimum per-impression value (e.g., attention_score ≥ 70). Only these target kinds are accepted — goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes the metric within budget — no declaration needed for that mode. When omitted, buyers can still set target-less vendor_metric goals.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/vendor_metric_value.py b/src/adcp/types/domains/core/vendor_metric_value.py index 8729dcfd3..377745be9 100644 --- a/src/adcp/types/domains/core/vendor_metric_value.py +++ b/src/adcp/types/domains/core/vendor_metric_value.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/vendor_metric_value.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import attribution_methodology as attribution_methodology_1 @@ -21,11 +21,15 @@ class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None class VendorMetricValue(AdCPBaseModel): @@ -56,6 +60,7 @@ class VendorMetricValue(AdCPBaseModel): description="Unit of the value. Free-form to accommodate the heterogeneity of vendor metrics (e.g., `score`, `seconds`, `persons`, `gCO2e`, `USD`, `lift_percent`). When the value is monetary, use the ISO 4217 code (e.g., `USD`, `EUR`); for non-monetary, use whatever the vendor publishes. Optional on every row — the canonical unit lives at the vendor's measurement-agent metric definition (`brand.json` `agents[type='measurement']`). When sellers populate it inline they SHOULD match the vendor's published unit; buyers MAY resolve from the vendor's measurement agent when this field is absent.", examples=['score', 'seconds', 'persons', 'gCO2e', 'USD', 'lift_percent', 'index'], ), + NoExplicitNull, ] = None measurable_impressions: Annotated[ StrictFloat | None, @@ -63,6 +68,7 @@ class VendorMetricValue(AdCPBaseModel): description='Number of impressions in this reporting period that the vendor was able to measure. Coverage denominator — buyers compute coverage rate as `measurable_impressions / impressions`. When absent, coverage is unspecified — buyers MUST NOT compute a coverage rate or assume full coverage. When the vendor measured zero impressions but is integrated, set to 0 explicitly. When the entry is omitted from `vendor_metric_values` entirely, the buyer infers no measurement happened (no integration). This pattern parallels `viewability.measurable_impressions` (`delivery-metrics.json#/properties/viewability`), which has handled vendor coverage in the IAS/DV/MRC ecosystem for over a decade — same convention: absence is unknown, not full. For channels where the atomic observation unit is a play or screen-second rather than an impression (DOOH, cinema, place-based), report `measurable_plays` or `measurable_play_seconds` instead — `impressions` there is itself a modelled figure (`plays × audience multiplier`), so `measurable_impressions / impressions` divides a measured count by a model output and has no interpretation.', ge=0.0, ), + NoExplicitNull, ] = None measurable_plays: Annotated[ StrictFloat | None, @@ -70,6 +76,7 @@ class VendorMetricValue(AdCPBaseModel): description='Number of plays (loop plays / spots aired) in this reporting period that the vendor was able to measure. Coverage denominator for channels where a play, not an impression, is the atomic observation unit — DOOH, cinema, place-based audio. Buyers compute coverage as `measurable_plays / plays` (top-level `plays` on `delivery-metrics.json`). Same absence semantics as `measurable_impressions`: absent means coverage is unspecified and buyers MUST NOT compute a rate; 0 means the vendor is integrated but measured nothing. A row SHOULD carry exactly one coverage denominator — the one matching the unit the vendor actually observes.', ge=0.0, ), + NoExplicitNull, ] = None measurable_play_seconds: Annotated[ StrictFloat | None, @@ -77,22 +84,26 @@ class VendorMetricValue(AdCPBaseModel): description="Play-seconds — seconds of creative playout summed across endpoints (screens, speakers, players) — in this reporting period that the vendor was able to measure. Medium-neutral on purpose: place-based audio has plays and duration but no screen. Coverage denominator when the vendor meters exposure duration rather than discrete plays. On screen networks buyers compute coverage as `measurable_play_seconds / dooh_metrics.screen_time_seconds`; on other place-based media, against the seller's reported playout seconds for the period. Same absence semantics as `measurable_impressions`.", ge=0.0, ), + NoExplicitNull, ] = None vendor_relationship: Annotated[ vendor_relationship_1.VendorRelationship | None, Field( description="Optional echo of the product's `reporting_capabilities.vendor_metrics[].vendor_relationship` for this `(vendor, metric_id)`, so the delivery row is self-describing without joining back to the product (same reasoning as `viewability.vendor`). When present it MUST equal the declared value; buyers MAY resolve from the product declaration when absent. Absence on the row is *undeclared*, never `third_party`. A relationship disposition, not a trust ranking. Deliberately not a `qualifier` key: the relationship is constant across every row for a (seller, vendor) pair and does not partition rows, so it stays out of the `(vendor, metric_id, qualifier)` reconciliation join." ), + NoExplicitNull, ] = None qualifier: Annotated[ Qualifier | None, Field( description='Optional qualifier disambiguating this row from sibling rows for the same (vendor, metric_id) — e.g., the same vendor outcome metric reported under 7-day and 30-day attribution windows. Same closed key set as `committed-metric`. When the matching `committed_metrics` entry carries a qualifier, this row MUST carry the identical qualifier so reconciliation joins on `(vendor, metric_id, qualifier)`.' ), + NoExplicitNull, ] = None breakdown: Annotated[ dict[str, Any] | None, Field( description="Optional structured payload for vendor metrics that don't fit a single scalar — panel demographic breakouts, co-view audience composition, incremental reach + frequency + lift decompositions. Free-form; the keys and value semantics are defined by the vendor (see the vendor's `brand.json` measurement-agent docs). Buyers MUST treat this object as opaque without consulting the vendor's documentation. Vendors place any fields beyond the standard envelope (e.g., confidence intervals, panel sizes) inside this object rather than at the top level." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/vendor_pricing_option.py b/src/adcp/types/domains/core/vendor_pricing_option.py index e4f5e35ac..a969ac217 100644 --- a/src/adcp/types/domains/core/vendor_pricing_option.py +++ b/src/adcp/types/domains/core/vendor_pricing_option.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/vendor_pricing_option.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictFloat @@ -27,7 +27,7 @@ class VendorPricingOption1(AdCPBaseModel): model: Literal['cpm'] = 'cpm' cpm: Annotated[StrictFloat, Field(description='Cost per thousand impressions', ge=0.0)] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class VendorPricingOption2(AdCPBaseModel): @@ -44,12 +44,13 @@ class VendorPricingOption2(AdCPBaseModel): description='Optional CPM cap. When set, the effective charge is min(percent × media_spend_per_mille, max_cpm).', ge=0.0, ), + NoExplicitNull, ] = None currency: Annotated[ str, Field(description='ISO 4217 currency code for the resulting charge', pattern='^[A-Z]{3}$'), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Period(StrEnum): @@ -67,7 +68,7 @@ class VendorPricingOption3(AdCPBaseModel): amount: Annotated[StrictFloat, Field(description='Fixed charge for the billing period', ge=0.0)] period: Annotated[Period, Field(description='Billing period for the flat fee.')] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class VendorPricingOption4(AdCPBaseModel): @@ -83,7 +84,7 @@ class VendorPricingOption4(AdCPBaseModel): ] unit_price: Annotated[StrictFloat, Field(description='Cost per one unit', ge=0.0)] currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Metadata(AdCPBaseModel): @@ -96,6 +97,7 @@ class Metadata(AdCPBaseModel): description="One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim — summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", min_length=1, ), + NoExplicitNull, ] = None @@ -123,8 +125,9 @@ class VendorPricingOption5(AdCPBaseModel): description='ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class VendorPricingOption6(AdCPBaseModel): @@ -141,6 +144,7 @@ class VendorPricingOption6(AdCPBaseModel): description='**DEPRECATED in 3.2.** Legacy named-format pricing scope. Use applies_to_output_capability_ids.', min_length=1, ), + NoExplicitNull, ] = None applies_to_output_capability_ids: Annotated[ list[AppliesToOutputCapabilityId] | None, @@ -148,6 +152,7 @@ class VendorPricingOption6(AdCPBaseModel): description='Creative transformers only: scopes this pricing option to canonical output capabilities advertised in creative.supported_formats[].capability_id. When absent, the option is the default for any output. A build targeting an output that matches no scoped option and has no unscoped default is rejected with UNPRICEABLE_OUTPUT.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/verification_token_claims.py b/src/adcp/types/domains/core/verification_token_claims.py index c1f781f67..71a1ceb59 100644 --- a/src/adcp/types/domains/core/verification_token_claims.py +++ b/src/adcp/types/domains/core/verification_token_claims.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/verification_token_claims.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field @@ -49,12 +49,16 @@ class AgenticadvertisingOrgVerificationTokenClaims(AdCPBaseModel): Field( description='Grading policy that produced the badge. Absence on a historical token means Legacy; verification_modes remains an independent evidence axis.' ), + NoExplicitNull, ] = None first_failing_spec_at: Annotated[ AwareDatetime | None, Field( description='Start of the current Strict Spec failure episode. Omitted when no Strict Spec failure clock is active. The registry remains authoritative for real-time status.' ), + NoExplicitNull, ] = None - adcp_version: Annotated[str | None, Field(pattern='^[1-9][0-9]*\\.[0-9]+$')] = None - protocol_version: str | None = None + adcp_version: Annotated[str | None, Field(pattern='^[1-9][0-9]*\\.[0-9]+$'), NoExplicitNull] = ( + None + ) + protocol_version: Annotated[str | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/version_envelope.py b/src/adcp/types/domains/core/version_envelope.py index d75760373..d12ef821d 100644 --- a/src/adcp/types/domains/core/version_envelope.py +++ b/src/adcp/types/domains/core/version_envelope.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/version_envelope.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import Field @@ -18,6 +18,7 @@ class AdcpVersionEnvelope(AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -27,4 +28,5 @@ class AdcpVersionEnvelope(AdCPBaseModel): ge=1, le=99, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/warning.py b/src/adcp/types/domains/core/warning.py index a233f4469..1fc49daaf 100644 --- a/src/adcp/types/domains/core/warning.py +++ b/src/adcp/types/domains/core/warning.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/warning.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import warning_code @@ -38,5 +38,6 @@ class Warning(AdCPBaseModel): Field( description='Optional seller-specific structured diagnostics. AdCP 3.2 defines interoperability through code and affected_resource only; buyers MUST NOT require portable keys inside details. Seller extensions that are not direct diagnostics belong in ext.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/warning_resource.py b/src/adcp/types/domains/core/warning_resource.py index 11a11e41a..68c654df7 100644 --- a/src/adcp/types/domains/core/warning_resource.py +++ b/src/adcp/types/domains/core/warning_resource.py @@ -1,12 +1,13 @@ # generated by datamodel-codegen: # filename: core/warning_resource.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum +from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict @@ -22,6 +23,6 @@ class WarningAffectedResource(AdCPBaseModel): extra='forbid', ) resource_type: ResourceType - media_buy_id: str | None = None - package_id: str | None = None - creative_id: str | None = None + media_buy_id: Annotated[str | None, NoExplicitNull] = None + package_id: Annotated[str | None, NoExplicitNull] = None + creative_id: Annotated[str | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/core/webhook_activity_record.py b/src/adcp/types/domains/core/webhook_activity_record.py index 9ac54ddb8..499a48330 100644 --- a/src/adcp/types/domains/core/webhook_activity_record.py +++ b/src/adcp/types/domains/core/webhook_activity_record.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/webhook_activity_record.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..enums import notification_type as notification_type_1 @@ -40,12 +40,14 @@ class WebhookActivityRecord(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None subscriber_id: Annotated[ str | None, Field( description='Identifies which registered webhook subscriber received this fire. **Required on records from account-anchored notification channels** (`notification_configs[]` registered via `sync_accounts`) — every subscriber has a `subscriber_id` at registration time, the seller MUST echo it on every fire and every activity record. **Optional on records from per-resource push channels** (`push_notification_config` on a media buy or task) — the calling principal is unambiguous in single-subscriber configurations and the field MAY be omitted; sellers MUST populate it once `reporting_webhook` adopts multi-subscriber (per #3009 in AdCP 4.0). Buyers MUST NOT use absence as a signal that no other subscribers exist; that information is not exposed by this surface.' ), + NoExplicitNull, ] = None fired_at: Annotated[ AwareDatetime, @@ -71,6 +73,7 @@ class WebhookActivityRecord(AdCPBaseModel): description='Sequence number from the webhook payload. Surfaced here so the buyer can spot stale-sequence drops and gaps without correlating against their own endpoint log. Absent for notification types that do not carry a sequence number.', ge=0, ), + NoExplicitNull, ] = None attempt: Annotated[ SchemaInt, @@ -112,6 +115,7 @@ class WebhookActivityRecord(AdCPBaseModel): description="Size of the request body the seller sent, in bytes. Useful for diagnosing oversized-payload rejections from the buyer's gateway.", ge=0, ), + NoExplicitNull, ] = None error_message: Annotated[ str | None, @@ -125,4 +129,5 @@ class WebhookActivityRecord(AdCPBaseModel): Field( description='Resource-specific extension slot. Adopters MAY surface a resource-specific cross-reference (e.g., `creative_id` on a creative-lifecycle record, `media_buy_id` on a record nested inside an account-level read) under `ext` rather than adding top-level fields — the canonical record shape stays uniform across resources and the `ext` envelope absorbs per-resource needs. Top-level extensions are not permitted (`additionalProperties: false`).' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/webhook_challenge.py b/src/adcp/types/domains/core/webhook_challenge.py index 5823f7f0e..41f1a47e7 100644 --- a/src/adcp/types/domains/core/webhook_challenge.py +++ b/src/adcp/types/domains/core/webhook_challenge.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: core/webhook_challenge.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..enums import notification_type @@ -35,6 +35,7 @@ class DeliveryAuth(AdCPBaseModel): description='SHA-256 hex fingerprint of the exact legacy credential string supplied in notification_configs[].authentication.credentials. Required when mode is Bearer or HMAC-SHA256; absent for rfc9421.', pattern='^[a-f0-9]{64}$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/core/webhook_challenge_response.py b/src/adcp/types/domains/core/webhook_challenge_response.py index ab3e3427f..aed4a1bc0 100644 --- a/src/adcp/types/domains/core/webhook_challenge_response.py +++ b/src/adcp/types/domains/core/webhook_challenge_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: core/webhook_challenge_response.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator @@ -25,6 +25,7 @@ class WebhookChallengeResponse(AdCPBaseModel): min_length=32, pattern='^[A-Za-z0-9_.:-]{32,255}$', ), + NoExplicitNull, ] = None token: Annotated[ str | None, @@ -34,6 +35,7 @@ class WebhookChallengeResponse(AdCPBaseModel): min_length=32, pattern='^[A-Za-z0-9_.:-]{32,255}$', ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/core/wholesale_feed_event.py b/src/adcp/types/domains/core/wholesale_feed_event.py index 2d23ce13f..16e2a5c1c 100644 --- a/src/adcp/types/domains/core/wholesale_feed_event.py +++ b/src/adcp/types/domains/core/wholesale_feed_event.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/wholesale_feed_event.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Annotated, Literal from uuid import UUID -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..enums import signal_catalog_type, signal_value_type @@ -65,6 +65,7 @@ class AppliesTo2(AdCPBaseModel): description="Optional. When present, names the accounts whose overlays are affected. When omitted, subscribers infer that their own principal's overlay is affected (they received the event because the per-subscriber scope filter routed it to them).", min_length=1, ), + NoExplicitNull, ] = None @@ -105,6 +106,7 @@ class Payload(AdCPBaseModel): description='Full post-change pricing_options array. NOT a delta — consumers replace the prior options with this list.', min_length=1, ), + NoExplicitNull, ] = None canonical_pricing_options: Annotated[ list[canonical_pricing_option.CanonicalPricingOption] | None, @@ -112,22 +114,26 @@ class Payload(AdCPBaseModel): description='Full post-change canonical pricing array for list_products mirrors.', min_length=1, ), + NoExplicitNull, ] = None previous_pricing_option_ids: Annotated[ list[str] | None, Field(description='Optional. Lets consumers detect which pricing_option_ids were retired.'), + NoExplicitNull, ] = None effective_at: Annotated[ AwareDatetime | None, Field( description='When the price change takes effect. A value in the future is a pre-announcement: consumers MAY warm caches but MUST NOT bind decisions until the effective time has passed. See specs/wholesale-feed-webhooks.md §`effective_at` and pre-announce.' ), + NoExplicitNull, ] = None retracts_event_id: Annotated[ UUID | None, Field( description="Optional. When this event retracts a prior pre-announced *.priced (per the §`effective_at` and pre-announce rules), set this to the event_id of the announcement being retracted. Lets consumers detect retraction deterministically by event_id lookup rather than maintaining per-entity 'latest-event' state. When present, the retraction MUST carry effective_at <= the retracted event's effective_at." ), + NoExplicitNull, ] = None applies_to: AppliesTo @@ -174,7 +180,7 @@ class Payload20(AdCPBaseModel): extra='allow', ) product_id: str - removal_reason: RemovalReason | None = None + removal_reason: Annotated[RemovalReason | None, NoExplicitNull] = None applies_to: AppliesTo @@ -220,23 +226,25 @@ class Payload23(AdCPBaseModel): extra='allow', ) signal_agent_segment_id: str - signal_ref: signal_ref_1.SignalRef | None = None + signal_ref: Annotated[signal_ref_1.SignalRef | None, NoExplicitNull] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption], Field(description='Full post-change pricing_options array. NOT a delta.', min_length=1), ] - previous_pricing_option_ids: list[str] | None = None + previous_pricing_option_ids: Annotated[list[str] | None, NoExplicitNull] = None effective_at: Annotated[ AwareDatetime | None, Field( description='When the price change takes effect. A value in the future is a pre-announcement: consumers MAY warm caches but MUST NOT bind decisions until the effective time has passed. See specs/wholesale-feed-webhooks.md §`effective_at` and pre-announce.' ), + NoExplicitNull, ] = None retracts_event_id: Annotated[ UUID | None, Field( description='Optional. When this event retracts a prior pre-announced *.priced, set this to the event_id of the announcement being retracted. See product.priced for the full retraction contract.' ), + NoExplicitNull, ] = None applies_to: AppliesTo @@ -283,8 +291,8 @@ class Payload24(AdCPBaseModel): extra='allow', ) signal_agent_segment_id: str - signal_ref: signal_ref_1.SignalRef | None = None - removal_reason: RemovalReason | None = None + signal_ref: Annotated[signal_ref_1.SignalRef | None, NoExplicitNull] = None + removal_reason: Annotated[RemovalReason | None, NoExplicitNull] = None applies_to: AppliesTo @@ -343,6 +351,7 @@ class Payload25(AdCPBaseModel): Field( description='Advisory recommendation. Consumers SHOULD repair by re-reading the affected feed named by affected_entity_type; the recommendation is not a synchronized trigger. Modeled as a single-value enum (not a boolean) so future minor versions can extend the recommendation vocabulary without a breaking shape change. Consumers MUST be prepared to ignore unknown recommendation values.' ), + NoExplicitNull, ] = None applies_to: AppliesTo affected_entity_type: Annotated[ @@ -399,6 +408,7 @@ class Signal(SignalListing): Field( description='Canonical signal reference for this wholesale signal. New events SHOULD use signal_ref.' ), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -406,6 +416,7 @@ class Signal(SignalListing): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str, @@ -413,11 +424,11 @@ class Signal(SignalListing): ] name: Annotated[str, Field(description='Human-readable signal name', min_length=1)] description: Annotated[str, Field(description='Detailed signal description', min_length=1)] - value_type: signal_value_type.SignalValueType | None = None - categories: Annotated[list[str] | None, Field(min_length=1)] = None - range: Range | None = None + value_type: Annotated[signal_value_type.SignalValueType | None, NoExplicitNull] = None + categories: Annotated[list[str] | None, Field(min_length=1), NoExplicitNull] = None + range: Annotated[Range | None, NoExplicitNull] = None signal_type: signal_catalog_type.SignalAvailabilityType - data_provider: Annotated[str | None, Field(min_length=1)] = None + data_provider: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None coverage_percentage: Annotated[ StrictFloat | None, Field( @@ -426,16 +437,18 @@ class Signal(SignalListing): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None coverage_forecast: Annotated[ signal_coverage_forecast.SignalCoverageForecast | None, Field( description='Optional forecast-shaped signal availability guidance using the same wire shape as get_signals.signals[].coverage_forecast. When present, this is authoritative for signal-level discovery coverage.' ), + NoExplicitNull, ] = None deployments: Annotated[Sequence[deployment.Deployment], Field(min_length=1)] pricing_options: Annotated[ - list[vendor_pricing_option.VendorPricingOption] | None, Field(min_length=1) + list[vendor_pricing_option.VendorPricingOption] | None, Field(min_length=1), NoExplicitNull ] = None @@ -444,7 +457,7 @@ class Payload21(AdCPBaseModel): extra='allow', ) signal_agent_segment_id: str - signal_ref: signal_ref_1.SignalRef | None = None + signal_ref: Annotated[signal_ref_1.SignalRef | None, NoExplicitNull] = None applies_to: Annotated[ AppliesTo, Field( @@ -496,12 +509,13 @@ class Payload22(AdCPBaseModel): extra='allow', ) signal_agent_segment_id: str - signal_ref: signal_ref_1.SignalRef | None = None + signal_ref: Annotated[signal_ref_1.SignalRef | None, NoExplicitNull] = None changed_fields: Annotated[ list[str] | None, Field( description='Advisory list of changed top-level field names. Consumers MAY use for fine-grained re-render, but the signal object is the source of truth for this webhook payload.' ), + NoExplicitNull, ] = None applies_to: AppliesTo signal: Annotated[ @@ -559,12 +573,14 @@ class Payload17(AdCPBaseModel): Field( description='Legacy 3.x post-change Product. Subscribers using get_products consume this form.' ), + NoExplicitNull, ] = None canonical_product: Annotated[ canonical_product_1.CanonicalProduct | None, Field( description='Canonical post-change Product for subscribers that negotiated lifecycle_tools.list_products. Consumers replace the matching canonical mirror entry directly.' ), + NoExplicitNull, ] = None applies_to: Annotated[ AppliesTo, @@ -619,16 +635,19 @@ class Payload18(AdCPBaseModel): product: Annotated[ product_1.Product | None, Field(description='Legacy 3.x post-change Product for get_products mirrors.'), + NoExplicitNull, ] = None canonical_product: Annotated[ canonical_product_1.CanonicalProduct | None, Field(description='Canonical post-change Product for list_products mirrors.'), + NoExplicitNull, ] = None changed_fields: Annotated[ list[str] | None, Field( description="Advisory list of changed top-level field names (e.g., ['format_ids', 'performance_standards']). Consumers MAY use for fine-grained re-render, but the product object is the source of truth for this webhook payload." ), + NoExplicitNull, ] = None applies_to: AppliesTo diff --git a/src/adcp/types/domains/core/wholesale_feed_webhook.py b/src/adcp/types/domains/core/wholesale_feed_webhook.py index 6a78e73dd..eb768c8c7 100644 --- a/src/adcp/types/domains/core/wholesale_feed_webhook.py +++ b/src/adcp/types/domains/core/wholesale_feed_webhook.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/wholesale_feed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated from uuid import UUID -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from . import ext as ext_1 @@ -94,12 +94,14 @@ class WholesaleFeedWebhook(AdCPBaseModel): Field( description='Product representation selected by the receiving notification config. Present on product.* fires; canonical uses canonical_product/canonical_pricing_options and legacy uses product/pricing_options.' ), + NoExplicitNull, ] = None previous_wholesale_feed_version: Annotated[ str | None, Field( description='Opaque version token for the affected wholesale feed before this change, when the seller can cheaply provide it. Receivers MAY use this to detect obvious gaps, but MUST NOT require it.' ), + NoExplicitNull, ] = None cache_scope: Annotated[ CacheScope, @@ -113,4 +115,4 @@ class WholesaleFeedWebhook(AdCPBaseModel): description='The actual product, signal, or bulk-change event. Consumers MAY apply this payload to their local mirror. Before any binding action, or when ordering/gap checks fail, consumers MUST reconcile through list_products / get_signals.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/audit_observation.py b/src/adcp/types/domains/creative/audit_observation.py index ebe0e57b5..b67ba72bb 100644 --- a/src/adcp/types/domains/creative/audit_observation.py +++ b/src/adcp/types/domains/creative/audit_observation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/audit_observation.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat from ..core import ext as ext_1 @@ -40,7 +40,9 @@ class Details(AdCPBaseModel): AnyUrl, Field(description='Governance agent URL that produced the observation.') ] feature_id: Annotated[ - str | None, Field(description='Feature or policy check that produced the observation.') + str | None, + Field(description='Feature or policy check that produced the observation.'), + NoExplicitNull, ] = None claimed_value: Annotated[ ClaimedValue, @@ -55,12 +57,14 @@ class Details(AdCPBaseModel): confidence: Annotated[ StrictFloat | None, Field(description='Confidence score for the observation, when applicable.', ge=0.0, le=1.0), + NoExplicitNull, ] = None substituted_for: Annotated[ AnyUrl | None, Field( description='Buyer-nominated verifier URL when the seller or orchestrator used a different on-list governance agent.' ), + NoExplicitNull, ] = None @@ -104,4 +108,4 @@ class CreativeAuditObservation(AdCPBaseModel): description='Audit-safe structured details. Mirrors the safe allowlist keys used for `PROVENANCE_CLAIM_CONTRADICTED`; value shapes remain observation-specific. Top-level `ext` remains the standard protocol extension point, but details do not allow arbitrary verifier response fields.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/creative_assignment_changed_webhook.py b/src/adcp/types/domains/creative/creative_assignment_changed_webhook.py index 5f4cc9518..947fce518 100644 --- a/src/adcp/types/domains/creative/creative_assignment_changed_webhook.py +++ b/src/adcp/types/domains/creative/creative_assignment_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/creative_assignment_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -46,4 +46,4 @@ class CreativeAssignmentChangedWebhook(AdCPBaseModel): creative_id: str change_kind: ChangeKind observed_at: AwareDatetime - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/creative_feature_result.py b/src/adcp/types/domains/creative/creative_feature_result.py index 51210c409..f4f63795e 100644 --- a/src/adcp/types/domains/creative/creative_feature_result.py +++ b/src/adcp/types/domains/creative/creative_feature_result.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/creative_feature_result.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import ext as ext_1 @@ -33,29 +33,36 @@ class CreativeFeatureResult(AdCPBaseModel): Field( description="Unit of measurement for quantitative values (e.g., 'percentage', 'score')" ), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, Field(description='Confidence score for this value (0-1)', ge=0.0, le=1.0), + NoExplicitNull, ] = None measured_at: Annotated[ - AwareDatetime | None, Field(description='When this feature was evaluated') + AwareDatetime | None, Field(description='When this feature was evaluated'), NoExplicitNull ] = None expires_at: Annotated[ AwareDatetime | None, Field(description='When this evaluation expires and should be refreshed'), + NoExplicitNull, ] = None methodology_version: Annotated[ - str | None, Field(description='Version of the methodology used to evaluate this feature') + str | None, + Field(description='Version of the methodology used to evaluate this feature'), + NoExplicitNull, ] = None details: Annotated[ dict[str, Any] | None, Field(description='Additional vendor-specific details about this evaluation'), + NoExplicitNull, ] = None policy_id: Annotated[ str | None, Field( description='Optional attribution — when this feature was evaluated for the purpose of a specific policy, policy_id references the authorizing PolicyEntry. Creative agents and sellers populate when the measurement was motivated by a specific policy; do NOT populate when the feature is a generic measurement (carbon score, brand consistency) unrelated to any policy. See /docs/governance/policy-attribution.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/creative_purged_webhook.py b/src/adcp/types/domains/creative/creative_purged_webhook.py index 340bc8e66..979e10e3b 100644 --- a/src/adcp/types/domains/creative/creative_purged_webhook.py +++ b/src/adcp/types/domains/creative/creative_purged_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/creative_purged_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -98,6 +98,7 @@ class CreativePurgedWebhook(AdCPBaseModel): description='Human-readable supplement. Sellers MUST NOT include third-party PII or the asset content in this field — the field is meant for operational context, not asset preservation.', max_length=500, ), + NoExplicitNull, ] = None initiator: Annotated[ Initiator, @@ -105,4 +106,4 @@ class CreativePurgedWebhook(AdCPBaseModel): description='Who initiated the purge. `seller` — explicit decision (legal team, content policy, takedown handler). `system` — automated retention sweep, storage policy enforcement. `buyer` never appears on this event — buyer-initiated destruction (if and when supported) is acknowledged on the `sync_creatives` response path.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/creative_status_changed_webhook.py b/src/adcp/types/domains/creative/creative_status_changed_webhook.py index 21e563e53..1ba5c03a9 100644 --- a/src/adcp/types/domains/creative/creative_status_changed_webhook.py +++ b/src/adcp/types/domains/creative/creative_status_changed_webhook.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/creative_status_changed_webhook.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import creative_revision_id @@ -107,6 +107,7 @@ class CreativeStatusChangedWebhook(AdCPBaseModel): Field( description='Buyer-authored input revision to which this transition applies. Required when the reviewed state has revision identity and the seller advertises creative.supports_revisions; it need not be the current list revision when the fire is received. Omitted for unversioned current content. A stale review outcome MUST NOT emit a fire as though it applied to a newer current revision.' ), + NoExplicitNull, ] = None transition: Annotated[ Transition, @@ -126,6 +127,7 @@ class CreativeStatusChangedWebhook(AdCPBaseModel): description='Human-readable supplement to `reason_code`. Free text from the seller. Sellers MUST NOT include third-party PII in this field.', max_length=500, ), + NoExplicitNull, ] = None initiator: Annotated[ Initiator, @@ -133,4 +135,4 @@ class CreativeStatusChangedWebhook(AdCPBaseModel): description='Who initiated the transition. `seller` — explicit decision by the seller (human reviewer, policy operator, takedown handler). `system` — automated seller-side process (processing pipeline, retention sweep, inactivity scan). `buyer` never appears on this event — buyer-initiated transitions are acknowledged on the `sync_creatives` response path and MUST NOT fire `creative.status_changed`.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/get_creative_delivery_request.py b/src/adcp/types/domains/creative/get_creative_delivery_request.py index a0d8958db..58a12a31a 100644 --- a/src/adcp/types/domains/creative/get_creative_delivery_request.py +++ b/src/adcp/types/domains/creative/get_creative_delivery_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/get_creative_delivery_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..core import account_ref @@ -29,6 +30,7 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account for routing and scoping. Limits results to creatives within this account.' ), + NoExplicitNull, ] = None media_buy_ids: Annotated[ list[str] | None, @@ -36,6 +38,7 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description='Filter to specific media buys by publisher ID. If omitted, returns creative delivery across all matching media buys.', min_length=1, ), + NoExplicitNull, ] = None creative_ids: Annotated[ list[str] | None, @@ -43,6 +46,7 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description='Filter to specific creatives by ID. If omitted, returns delivery for all creatives matching the other filters.', min_length=1, ), + NoExplicitNull, ] = None start_date: Annotated[ str | None, @@ -50,6 +54,7 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description="Start date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.", pattern='^\\d{4}-\\d{2}-\\d{2}$', ), + NoExplicitNull, ] = None end_date: Annotated[ str | None, @@ -57,6 +62,7 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description="End date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.", pattern='^\\d{4}-\\d{2}-\\d{2}$', ), + NoExplicitNull, ] = None max_variants: Annotated[ SchemaInt | None, @@ -64,15 +70,17 @@ class GetCreativeDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description='Maximum number of variants to return per creative. When omitted, the agent returns all variants. Use this to limit response size for generative creatives that may produce large numbers of variants.', ge=1, ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description='Pagination parameters for the creatives array in the response. Uses cursor-based pagination consistent with other list operations.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> GetCreativeDeliveryRequest: diff --git a/src/adcp/types/domains/creative/get_creative_delivery_response.py b/src/adcp/types/domains/creative/get_creative_delivery_response.py index e710949a3..25127fa9f 100644 --- a/src/adcp/types/domains/creative/get_creative_delivery_response.py +++ b/src/adcp/types/domains/creative/get_creative_delivery_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/get_creative_delivery_response.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -32,6 +32,7 @@ class ReportingPeriod(AdCPBaseModel): Field( description="IANA timezone identifier for the reporting period (e.g., 'America/New_York', 'UTC'). Platforms report in their native timezone." ), + NoExplicitNull, ] = None @@ -50,6 +51,7 @@ class Pagination(AdCPBaseModel): description='Total number of creatives matching the request filters. Canonical field name (matches `PaginationResponse.total_count`). Sellers SHOULD populate this and the deprecated `total` field identically until 4.0; buyers SHOULD prefer this field.', ge=0, ), + NoExplicitNull, ] = None total: Annotated[ SchemaInt | None, @@ -58,6 +60,7 @@ class Pagination(AdCPBaseModel): description='**Deprecated** — use `total_count` instead. Retained as a legacy alias for 3.x backward compatibility; removed in AdCP 4.0. Sellers populating this field MUST also populate `total_count` with the same value.', ge=0, ), + NoExplicitNull, ] = None @@ -71,6 +74,7 @@ class Creative(AdCPBaseModel): Field( description="Publisher's media buy identifier for this creative. Present when the request spanned multiple media buys, so the buyer can correlate each creative to its media buy." ), + NoExplicitNull, ] = None format_id: Annotated[ format_id_1.FormatReferenceStructuredObject | None, @@ -78,19 +82,24 @@ class Creative(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named format of this creative. New responses use `format_kind` and optional `format_option_ref`.', ), + NoExplicitNull, ] = None format_kind: Annotated[ - str | None, Field(description='Canonical format kind delivered for this creative.') + str | None, + Field(description='Canonical format kind delivered for this creative.'), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Portable reference to the product or publisher format option used for delivery, when disambiguation is required.' ), + NoExplicitNull, ] = None totals: Annotated[ delivery_metrics.DeliveryMetrics | None, Field(description='Aggregate delivery metrics across all variants of this creative'), + NoExplicitNull, ] = None variant_count: Annotated[ SchemaInt | None, @@ -98,6 +107,7 @@ class Creative(AdCPBaseModel): description='Total number of agent-unique variant_id rows for this creative. When max_variants was specified in the request, this may exceed the number of items in the variants array.', ge=0, ), + NoExplicitNull, ] = None variants: Annotated[ list[creative_variant.CreativeVariant], @@ -116,12 +126,14 @@ class GetCreativeDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Account identifier. Present when the response spans or is scoped to a specific account.' ), + NoExplicitNull, ] = None media_buy_id: Annotated[ str | None, Field( description="Publisher's media buy identifier. Present when the request was scoped to a single media buy." ), + NoExplicitNull, ] = None currency: Annotated[ str, @@ -139,9 +151,12 @@ class GetCreativeDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Pagination information. Present when the request included pagination parameters. **Note:** `get_creative_delivery` uses page-based pagination (`limit`/`offset`) for historical reasons, distinct from the cursor-based [`PaginationResponse`](/schemas/v3/core/pagination-response.json) used by `list_*` tools. Field naming aligned with `PaginationResponse.total_count` in 3.1; the legacy `total` field is retained as a deprecated alias until 4.0. Sellers MUST populate both fields identically; buyers SHOULD prefer `total_count` (the canonical name) and ignore `total` if both are present.' ), + NoExplicitNull, ] = None errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific errors and warnings') + list[error.Error] | None, + Field(description='Task-specific errors and warnings'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/get_creative_features_async_response_submitted.py b/src/adcp/types/domains/creative/get_creative_features_async_response_submitted.py index e941d061b..c4e2bbb1a 100644 --- a/src/adcp/types/domains/creative/get_creative_features_async_response_submitted.py +++ b/src/adcp/types/domains/creative/get_creative_features_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/get_creative_features_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,6 +36,7 @@ class GetCreativeFeaturesSubmitted(AdCPBaseModel): description='Provider-generated identity allocated when the evaluation is accepted. Optional in AdCP 3.x and required in AdCP 4.0. Providers SHOULD emit it in 3.x; when present, exact replays and the terminal result MUST carry this same value. Distinct from task_id and idempotency_key.', min_length=1, ), + NoExplicitNull, ] = None message: Annotated[ str | None, @@ -43,6 +44,7 @@ class GetCreativeFeaturesSubmitted(AdCPBaseModel): description='Optional human-readable explanation of why the evaluation is submitted. Plain text only; callers treat it as untrusted provider input.', max_length=2000, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/get_creative_features_request.py b/src/adcp/types/domains/creative/get_creative_features_request.py index c56997404..0147d5891 100644 --- a/src/adcp/types/domains/creative/get_creative_features_request.py +++ b/src/adcp/types/domains/creative/get_creative_features_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/get_creative_features_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -30,6 +31,7 @@ class GetCreativeFeaturesRequest(AdcpRequest, AdcpVersionEnvelope): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None creative_manifest: Annotated[ creative_manifest_1.CreativeManifest, @@ -43,18 +45,21 @@ class GetCreativeFeaturesRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional filter to specific features. If omitted, returns all available features.', min_length=1, ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference | None, Field( description='Account for billing this evaluation. Required when the governance agent charges per evaluation.' ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async terminal completion/failure notifications. A submitted acknowledgement remains pollable through get_task_status whether or not this field is present. If the provider accepts this field and returns status: submitted, it MUST deliver at least the terminal completion or failure notification to the configured URL. If it cannot honor the webhook channel, it MUST reject the request instead of silently downgrading delivery.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/get_creative_features_response.py b/src/adcp/types/domains/creative/get_creative_features_response.py index d4ba8cf32..8c3110dc1 100644 --- a/src/adcp/types/domains/creative/get_creative_features_response.py +++ b/src/adcp/types/domains/creative/get_creative_features_response.py @@ -31,7 +31,7 @@ class GetCreativeFeaturesResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEn class GetCreativeFeaturesResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: Annotated[str, StringConstraints(min_length=1)] evaluation_id: Annotated[str, StringConstraints(min_length=1)] | None = None diff --git a/src/adcp/types/domains/creative/get_creative_features_terminal_success.py b/src/adcp/types/domains/creative/get_creative_features_terminal_success.py index a834279b9..f742bd712 100644 --- a/src/adcp/types/domains/creative/get_creative_features_terminal_success.py +++ b/src/adcp/types/domains/creative/get_creative_features_terminal_success.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/get_creative_features_terminal_success.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -25,6 +25,7 @@ class GetCreativeFeaturesSuccess(AdCPBaseModel): description='Provider-generated identity for this evaluation. Optional in AdCP 3.x and required in AdCP 4.0. Providers SHOULD emit it in 3.x. When present, it MUST be stable across exact response replays and, for async work whose submitted acknowledgement included evaluation_id, MUST be identical to that value. Used for result provenance, consumption and provider-cost reconciliation, and support; it is not an idempotency key or task identity.', min_length=1, ), + NoExplicitNull, ] = None results: Annotated[ list[creative_feature_result.CreativeFeatureResult], @@ -35,32 +36,38 @@ class GetCreativeFeaturesSuccess(AdCPBaseModel): Field( description="URL to the vendor's full assessment report. The vendor controls what information is disclosed and access control." ), + NoExplicitNull, ] = None audit_observations: Annotated[ list[audit_observation.CreativeAuditObservation] | None, Field( description='Non-blocking audit observations from the governance agent. Observations surface audit-worthy claims that are not verifier refutations and are not rejection grounds by themselves.' ), + NoExplicitNull, ] = None pricing_option_id: Annotated[ str | None, Field( description='Which rate card pricing option was applied for this evaluation. Present when the governance agent charges for evaluations and account was provided in the request.' ), + NoExplicitNull, ] = None vendor_cost: Annotated[ StrictFloat | None, Field(description='Cost incurred for this evaluation, denominated in currency.', ge=0.0), + NoExplicitNull, ] = None currency: Annotated[ str | None, Field(description='ISO 4217 currency code for vendor_cost.', pattern='^[A-Z]{3}$'), + NoExplicitNull, ] = None consumption: Annotated[ creative_consumption.CreativeConsumption | None, Field( description='Structured consumption details for this evaluation. Informational — lets the buyer verify that vendor_cost is consistent with the rate card. vendor_cost is the billing source of truth.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_creative_formats_request.py b/src/adcp/types/domains/creative/list_creative_formats_request.py index cc7d1aaac..b754aceae 100644 --- a/src/adcp/types/domains/creative/list_creative_formats_request.py +++ b/src/adcp/types/domains/creative/list_creative_formats_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creative_formats_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -40,12 +41,14 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Return only these specific named-format IDs. Use canonical-format discovery in 4.0.', min_length=1, ), + NoExplicitNull, ] = None type: Annotated[ Type | None, Field( description='Filter by format type (technical categories with distinct requirements)' ), + NoExplicitNull, ] = None asset_types: Annotated[ list[asset_content_type.AssetContentType] | None, @@ -53,45 +56,54 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats that include these asset types. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns formats with images and text, ['javascript'] returns formats accepting JavaScript tags.", min_length=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, Field( description='Maximum width in pixels (inclusive). Returns formats with width <= this value. Omit for responsive/fluid formats.' ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, Field( description='Maximum height in pixels (inclusive). Returns formats with height <= this value. Omit for responsive/fluid formats.' ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, Field( description='Minimum width in pixels (inclusive). Returns formats with width >= this value.' ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, Field( description='Minimum height in pixels (inclusive). Returns formats with height >= this value.' ), + NoExplicitNull, ] = None is_responsive: Annotated[ StrictBool | None, Field( description='Filter for responsive formats that adapt to container size. When true, returns formats without fixed dimensions.' ), + NoExplicitNull, ] = None name_search: Annotated[ - str | None, Field(description='Search for formats by name (case-insensitive partial match)') + str | None, + Field(description='Search for formats by name (case-insensitive partial match)'), + NoExplicitNull, ] = None wcag_level: Annotated[ wcag_level_1.WcagLevel | None, Field( description='Filter to formats that meet at least this WCAG conformance level (A < AA < AAA)' ), + NoExplicitNull, ] = None disclosure_positions: Annotated[ list[disclosure_position.DisclosurePosition] | None, @@ -99,6 +111,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats that support all of these disclosure positions. When a format has disclosure_capabilities, match against those positions. Otherwise fall back to supported_disclosure_positions. Use to find formats compatible with a brief's compliance requirements.", min_length=1, ), + NoExplicitNull, ] = None disclosure_persistence: Annotated[ list[disclosure_persistence_1.DisclosurePersistence] | None, @@ -106,6 +119,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='Filter to formats where each requested persistence mode is supported by at least one position in disclosure_capabilities. Different positions may satisfy different modes. Use to find formats compatible with jurisdiction-specific persistence requirements (e.g., continuous for EU AI Act).', min_length=1, ), + NoExplicitNull, ] = None output_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -114,6 +128,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='**DEPRECATED in 3.1.** Discover build capability via `list_transformers` (filter its `output_format_ids`) instead — build capability is a property of transformers, not a relationship between formats. *Legacy:* filter to formats whose `output_format_ids` includes any of these format IDs.', min_length=1, ), + NoExplicitNull, ] = None input_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -122,6 +137,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='**DEPRECATED in 3.1.** Discover build capability via `list_transformers` (filter its `input_format_ids`) instead. *Legacy:* filter to formats whose `input_format_ids` includes any of these format IDs.', min_length=1, ), + NoExplicitNull, ] = None include_pricing: Annotated[ StrictBool | None, @@ -129,6 +145,7 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='**DEPRECATED in 3.1. Removed at 4.0.** Use `list_transformers` with `include_pricing` and `account` for transformation and generation pricing. *Legacy 3.x behavior:* include `pricing_options` on each returned format. Requires `account`; when false or omitted, pricing is not computed. Agents supporting the 3.1 format-pricing surface MUST continue to honor this field through 3.x.', ), + NoExplicitNull, ] = False account: Annotated[ account_ref.AccountReference | None, @@ -136,7 +153,8 @@ class ListCreativeFormatsRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='**DEPRECATED for `list_creative_formats` in 3.1. Removed at 4.0.** Use the account-scoped `list_transformers` request instead. *Legacy 3.x behavior:* identifies the rate card used when `include_pricing` is true.', ), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_creative_formats_response.py b/src/adcp/types/domains/creative/list_creative_formats_response.py index 87cf3f3f6..1da0e9eb8 100644 --- a/src/adcp/types/domains/creative/list_creative_formats_response.py +++ b/src/adcp/types/domains/creative/list_creative_formats_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creative_formats_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import context as context_1 @@ -28,11 +28,12 @@ class CreativeAgent(AdCPBaseModel): ), ] agent_name: Annotated[ - str | None, Field(description='Human-readable name for the creative agent') + str | None, Field(description='Human-readable name for the creative agent'), NoExplicitNull ] = None capabilities: Annotated[ list[creative_agent_capability.CreativeAgentCapability] | None, Field(description='Capabilities this creative agent provides'), + NoExplicitNull, ] = None @@ -53,10 +54,13 @@ class ListCreativeFormatsResponseCreativeAgent(AdcpResponse, AdcpVersionEnvelope deprecated=True, description='Deprecated recursive discovery projection retained only for historical 3.x responses. New buyers use registry reverse lookup and direct get_adcp_capabilities confirmation.', ), + NoExplicitNull, ] = None errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific errors and warnings') + list[error.Error] | None, + Field(description='Task-specific errors and warnings'), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_creatives_request.py b/src/adcp/types/domains/creative/list_creatives_request.py index 7d945c87d..d45494ac2 100644 --- a/src/adcp/types/domains/creative/list_creatives_request.py +++ b/src/adcp/types/domains/creative/list_creatives_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creatives_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -54,10 +54,12 @@ class Sort(AdCPBaseModel): extra='allow', ) field: Annotated[ - creative_sort_field.CreativeSortField | None, Field(description='Field to sort by') + creative_sort_field.CreativeSortField | None, + Field(description='Field to sort by'), + NoExplicitNull, ] = creative_sort_field.CreativeSortField.created_date direction: Annotated[ - sort_direction.SortDirection | None, Field(description='Sort direction') + sort_direction.SortDirection | None, Field(description='Sort direction'), NoExplicitNull ] = sort_direction.SortDirection.desc @@ -65,17 +67,20 @@ class ListCreativesRequest(AdcpRequest, AdcpVersionEnvelope): model_config = ConfigDict( extra='allow', ) - filters: creative_filters.CreativeFilters | None = None - sort: Annotated[Sort | None, Field(description='Sorting parameters')] = None - pagination: pagination_request.PaginationRequest | None = None + filters: Annotated[creative_filters.CreativeFilters | None, NoExplicitNull] = None + sort: Annotated[Sort | None, Field(description='Sorting parameters'), NoExplicitNull] = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None include_assignments: Annotated[ - StrictBool | None, Field(description='Include package assignment information in response') + StrictBool | None, + Field(description='Include package assignment information in response'), + NoExplicitNull, ] = True assignment_projection: Annotated[ AssignmentProjection | None, Field( description='Controls nested assigned_packages projection when assignments are included. all returns up to assignment_limit active assignments per creative. matching returns only assignments matching filters.indicator_types and requires that filter. Use matching for compact indicator discovery; get_media_buys remains the complete authoritative repair path when assignments_truncated is true.' ), + NoExplicitNull, ] = AssignmentProjection.all assignment_limit: Annotated[ SchemaInt | None, @@ -84,40 +89,47 @@ class ListCreativesRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=200, ), + NoExplicitNull, ] = 50 include_snapshot: Annotated[ StrictBool | None, Field( description='Include a lightweight delivery snapshot per creative (lifetime impressions and last-served date). For detailed performance analytics, use get_creative_delivery.' ), + NoExplicitNull, ] = False include_items: Annotated[ StrictBool | None, Field(description='Include items for multi-asset formats like carousels and native ads'), + NoExplicitNull, ] = False include_variables: Annotated[ StrictBool | None, Field( description='Include dynamic content variable definitions (DCO slots) for each creative' ), + NoExplicitNull, ] = False include_pricing: Annotated[ StrictBool | None, Field( description='Include pricing_options on each creative. Requires account to be provided. When false or omitted, pricing is not computed.' ), + NoExplicitNull, ] = False include_purged: Annotated[ StrictBool | None, Field( description="Include soft-purged creative tombstones in the result set. When true, creatives destroyed via `creative.purged` with `purge_kind: soft` surface as tombstone records carrying `purged: true`, `purged_at`, and the purge reason — within the seller's webhook activity retention window (30 days from `purged_at`, MUST match `webhook-activity-record` retention). Hard-purged creatives MUST NOT appear regardless of this flag. When false or omitted, the result set excludes all purged creatives — same default as today." ), + NoExplicitNull, ] = False include_webhook_activity: Annotated[ StrictBool | None, Field( description='Include recent webhook activity per creative. When true, each returned creative carries a `webhook_activity[]` array of the most recent fires scoped to that creative — `creative.status_changed` and `creative.purged` deliveries. Adoption of the `webhook_activity[]` pattern per `snapshot-and-log.mdx § Webhook activity log pattern`. Retention is 30 days from `completed_at` (MUST). Three-state presence applies: omitted = seller does not surface; `[]` = persists but no recent fires; non-empty = actual records.' ), + NoExplicitNull, ] = False webhook_activity_limit: Annotated[ SchemaInt | None, @@ -126,12 +138,14 @@ class ListCreativesRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=200, ), + NoExplicitNull, ] = 50 account: Annotated[ account_ref.AccountReference | None, Field( description="Account reference for pricing and access. When provided with include_pricing, the agent returns pricing_options from this account's rate card on each creative." ), + NoExplicitNull, ] = None fields: Annotated[ list[Field1] | None, @@ -139,6 +153,7 @@ class ListCreativesRequest(AdcpRequest, AdcpVersionEnvelope): description="Specific fields to include in response (omit for all fields). The 'concept' value returns both concept_id and concept_name. `format_id` is a deprecated 3.x compatibility projection; new integrations request `format_kind` and `format_option_ref`. Selecting localization automatically includes creative_id, status, assets, the selected format identity, and localization_unavailable when applicable. Selecting rights_attestation_evaluations automatically includes rights so each seller-produced result can be reconciled with the exact retained constraint and reference.", min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_creatives_response.py b/src/adcp/types/domains/creative/list_creatives_response.py index ebc685840..c3e438ffd 100644 --- a/src/adcp/types/domains/creative/list_creatives_response.py +++ b/src/adcp/types/domains/creative/list_creatives_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creatives_response.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from collections.abc import Sequence from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StrictBool, StringConstraints, model_validator from ..core import account as account_1 @@ -88,6 +88,7 @@ class Indicator(AdCPBaseModel): Field( description='When the seller first detected the current uninterrupted occurrence of this indicator. Keep this value stable while the condition remains present. If the condition clears and is later detected again, use the new detection time. Optional because some upstream platforms expose current assessments without an original detection timestamp.' ), + NoExplicitNull, ] = None scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -95,12 +96,14 @@ class Indicator(AdCPBaseModel): description='Optional narrower publisher or placement scope within the enclosing media buy, package, or package–creative assignment. Omit only when the seller evaluated and asserts the indicator across the whole enclosing resource/relationship. When partial indicators_evaluated_scope is declared, every returned indicator MUST include scope and every entry MUST fall within that coverage. This is scope, not source: the responding seller remains the source.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Seller- or provider-specific detail such as scores, thresholds, evaluation windows, methodology identifiers, or upstream attribution. Core consumers must not need ext to understand the broad meaning of type.' ), + NoExplicitNull, ] = None @@ -130,6 +133,7 @@ class Snapshot(AdCPBaseModel): Field( description='Last time this creative served an impression. Absent when the creative has never served.' ), + NoExplicitNull, ] = None @@ -160,25 +164,29 @@ class StatusSummary(AdCPBaseModel): extra='allow', ) processing: Annotated[ - SchemaInt | None, Field(description='Number of creatives being processed', ge=0) + SchemaInt | None, + Field(description='Number of creatives being processed', ge=0), + NoExplicitNull, ] = None approved: Annotated[ - SchemaInt | None, Field(description='Number of approved creatives', ge=0) + SchemaInt | None, Field(description='Number of approved creatives', ge=0), NoExplicitNull ] = None pending_review: Annotated[ - SchemaInt | None, Field(description='Number of creatives pending review', ge=0) + SchemaInt | None, + Field(description='Number of creatives pending review', ge=0), + NoExplicitNull, ] = None rejected: Annotated[ - SchemaInt | None, Field(description='Number of rejected creatives', ge=0) + SchemaInt | None, Field(description='Number of rejected creatives', ge=0), NoExplicitNull ] = None archived: Annotated[ - SchemaInt | None, Field(description='Number of archived creatives', ge=0) + SchemaInt | None, Field(description='Number of archived creatives', ge=0), NoExplicitNull ] = None class SortApplied(AdCPBaseModel): - field: str | None = None - direction: sort_direction.SortDirection | None = None + field: Annotated[str | None, NoExplicitNull] = None + direction: Annotated[sort_direction.SortDirection | None, NoExplicitNull] = None class QuerySummary(AdCPBaseModel): @@ -193,10 +201,12 @@ class QuerySummary(AdCPBaseModel): SchemaInt, Field(description='Number of creatives returned in this response', ge=0) ] filters_applied: Annotated[ - list[str] | None, Field(description='List of filters that were applied to the query') + list[str] | None, + Field(description='List of filters that were applied to the query'), + NoExplicitNull, ] = None sort_applied: Annotated[ - SortApplied | None, Field(description='Sort order that was applied') + SortApplied | None, Field(description='Sort order that was applied'), NoExplicitNull ] = None @@ -210,12 +220,14 @@ class AssignedPackage(IndicatorBearingResourceState): description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', min_length=1, ), + NoExplicitNull, ] = None indicators: Annotated[ list[Indicator] | None, Field( description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' ), + NoExplicitNull, ] = None package_id: Annotated[str, Field(description='Package identifier')] media_buy_id: Annotated[ @@ -223,6 +235,7 @@ class AssignedPackage(IndicatorBearingResourceState): Field( description='Media buy containing this package. A seller advertising list_creatives in media_buy.relationship_notifications.projection_tasks MUST include this field on every assignment row, including rows where indicators is omitted as unknown, so buyers can key and reread the relationship unambiguously when package IDs are reused across media buys.' ), + NoExplicitNull, ] = None assigned_date: Annotated[AwareDatetime, Field(description='When this assignment was created')] approval_status: Annotated[ @@ -230,12 +243,14 @@ class AssignedPackage(IndicatorBearingResourceState): Field( description='Aggregate approval state for this creative in this package assignment. This mirrors the same relationship in get_media_buys. Sellers advertising list_creatives as an indicator projection task MUST include it; partially_approved requires approval_scopes.' ), + NoExplicitNull, ] = None rejection_reason: Annotated[ str | None, Field( description='Human-readable explanation when approval_status is rejected. Mirrors get_media_buys for the same relationship.' ), + NoExplicitNull, ] = None approval_scopes: Annotated[ list[creative_approval_scope.ScopedCreativeApproval] | None, @@ -243,12 +258,14 @@ class AssignedPackage(IndicatorBearingResourceState): description='Complete, disjoint publisher/placement approval partition when approval_status is partially_approved. A normalized scope appears once. For one publisher, use either one publisher-wide row or placement-specific rows, never both. Omit when one approval_status applies uniformly to the whole assignment. The same scoped outcomes are mirrored on get_media_buys.', min_length=2, ), + NoExplicitNull, ] = None indicators_as_of: Annotated[ AwareDatetime | None, Field( description='When the seller last completed the evaluation represented by indicators for this relationship. Required whenever indicators is present, including an empty array.' ), + NoExplicitNull, ] = None indicators_evaluated_scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -256,6 +273,7 @@ class AssignedPackage(IndicatorBearingResourceState): description='Optional publisher or placement scopes covered by this evaluation. Omit when indicators covers the whole package–creative assignment. When present, scopes not listed remain unknown; every returned indicator.scope entry MUST be contained by this set.', min_length=1, ), + NoExplicitNull, ] = None @@ -273,6 +291,7 @@ class Assignments(AdCPBaseModel): ge=0, le=200, ), + NoExplicitNull, ] = None matching_assignment_count: Annotated[ SchemaInt | None, @@ -280,18 +299,21 @@ class Assignments(AdCPBaseModel): description='Total active assignments matching filters.indicator_types. MUST be present exactly when assignment_projection was matching. May exceed returned_assignment_count.', ge=0, ), + NoExplicitNull, ] = None assignments_truncated: Annotated[ StrictBool | None, Field( description='True exactly when more qualifying assignments exist than were returned under assignment_limit. Qualifying means assignment_count for the all projection and matching_assignment_count for the matching projection. Buyers needing complete state repair through get_media_buys.' ), + NoExplicitNull, ] = None assigned_packages: Annotated[ list[AssignedPackage] | None, Field( description='Bounded package assignment projection. Under assignment_projection: matching, contains only assignments carrying a requested indicator type; otherwise contains active assignments up to assignment_limit. The response ceiling is enforced via verifier_constraints rather than maxItems, so payloads from 3.1 sellers remain schema-valid.' ), + NoExplicitNull, ] = None @@ -309,15 +331,19 @@ class Creative(AdCPBaseModel): Field( description='Current buyer-authored input revision for this creative. Present when the current effective content has revision identity and the seller advertises creative.supports_revisions; omitted after an accepted content-bearing legacy update without revision_id.' ), + NoExplicitNull, ] = None representation_selection: Annotated[ representation_selection_1.RepresentationSelection | None, Field( description='Exact source-set and selected-output lineage retained when the current stored creative was resolved from a CreativeRepresentationSet.' ), + NoExplicitNull, ] = None account: Annotated[ - account_1.Account | None, Field(description='Account that owns this creative') + account_1.Account | None, + Field(description='Account that owns this creative'), + NoExplicitNull, ] = None name: Annotated[str, Field(description='Human-readable creative name')] format_id: Annotated[ @@ -326,18 +352,21 @@ class Creative(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format path. New listed creatives use `format_kind` and optional `format_option_ref`.', ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description='Canonical 3.2 path. The canonical format kind this creative targets. Mutually exclusive with deprecated `format_id`.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Optional reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context.' ), + NoExplicitNull, ] = None status: Annotated[ creative_status.CreativeStatus, Field(description='Current approval status of the creative') @@ -349,27 +378,33 @@ class Creative(AdCPBaseModel): Field( description='Assets for this creative, keyed by asset_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema.' ), + NoExplicitNull, ] = None component_assets: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, Field( description='Preserved component-addressed asset maps for `coordinated_placements`, keyed by coordinated component ID.' ), + NoExplicitNull, ] = None localization: Annotated[ creative_localization_readback.CreativeLocalizationReadback | None, Field( description='Authoritative exact materialized locale-variant state. Present for localized creatives when complete. The enclosing creative status is the single review lifecycle for all variants.' ), + NoExplicitNull, ] = None localization_unavailable: Annotated[ LocalizationUnavailable | None, Field( description='Per-creative fail-closed state returned instead of localization when the seller knows the creative is localized but cannot construct complete exact readback. The creative remains in this page and counts toward query_summary.returned and pagination; buyers may continue using the base creative fields but MUST NOT infer locale eligibility.' ), + NoExplicitNull, ] = None tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') + list[str] | None, + Field(description='User-defined tags for organization and searchability'), + NoExplicitNull, ] = None rights: Annotated[ list[rights_constraint.RightsConstraint] | None, @@ -377,6 +412,7 @@ class Creative(AdCPBaseModel): description="Exact rights constraints retained from the buyer's creative submission. Presence is presentation readback, not proof that the seller accepted or verified the rights.", min_length=1, ), + NoExplicitNull, ] = None rights_attestation_evaluations: Annotated[ list[rights_attestation_evaluation.RightsAttestationEvaluation] | None, @@ -384,41 +420,50 @@ class Creative(AdCPBaseModel): description="Complete seller-produced verifier-of-record results for retained rights references. Buyers MUST ignore any evaluation they originally supplied and rely only on this seller readback for this seller's eligibility decision. This array has no independent item ceiling because rights is not capped; under a required policy every applicable retained constraint needs a corresponding current verified result.", min_length=1, ), + NoExplicitNull, ] = None concept_id: Annotated[ str | None, Field( description='Creative concept this creative belongs to. Concepts group related creatives across sizes and formats.' ), + NoExplicitNull, + ] = None + concept_name: Annotated[ + str | None, Field(description='Human-readable concept name'), NoExplicitNull ] = None - concept_name: Annotated[str | None, Field(description='Human-readable concept name')] = None variables: Annotated[ list[creative_variable.CreativeVariable] | None, Field( description='Dynamic content variables (DCO slots) for this creative. Included when include_variables=true.' ), + NoExplicitNull, ] = None assignments: Annotated[ Assignments | None, Field(description='Current package assignments (included when include_assignments=true)'), + NoExplicitNull, ] = None snapshot: Annotated[ Snapshot | None, Field( description='Lightweight delivery snapshot (included when include_snapshot=true). For detailed performance analytics, use get_creative_delivery.' ), + NoExplicitNull, ] = None snapshot_unavailable_reason: Annotated[ snapshot_unavailable_reason_1.SnapshotUnavailableReason | None, Field( description='Machine-readable reason the snapshot is omitted. Present only when include_snapshot was true and snapshot data is unavailable for this creative.' ), + NoExplicitNull, ] = None items: Annotated[ list[creative_item.CreativeItem] | None, Field( description='Items for multi-asset formats like carousels and native ads (included when include_items=true)' ), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -426,12 +471,14 @@ class Creative(AdCPBaseModel): description='Pricing options for using this creative (serving, delivery). Used by ad servers and library agents. Transformation agents expose build pricing on canonical transformer.pricing_options entries from list_transformers instead. Present when include_pricing=true and account provided. The buyer passes the applied pricing_option_id in report_usage.', min_length=1, ), + NoExplicitNull, ] = None purge: Annotated[ Purge | None, Field( description="Tombstone block — present only when this record is a soft-purged creative surfaced via `include_purged: true`. The record's `status` field reflects the last status before purge (frozen — buyers MUST treat the creative as gone; assignments, snapshot, and serving operations no longer apply). Tombstones surface for the seller's webhook activity retention window (30 days from `purge.at`). Hard purges (`purge_kind: hard` on the webhook) do not surface on this read — the [`creative.purged`](https://adcontextprotocol.org/schemas/v3/creative/creative-purged-webhook.json) webhook is the only signal." ), + NoExplicitNull, ] = None webhook_activity: Annotated[ list[webhook_activity_record.WebhookActivityRecord] | None, @@ -439,6 +486,7 @@ class Creative(AdCPBaseModel): description='Recent webhook fires scoped to this creative — creative.status_changed, creative.purged, creative.assignment_changed, and assignment-level indicators.changed deliveries. Present only when include_webhook_activity is true. Account-anchored records include subscriber_id; the parent creative_id disambiguates the record. Retention: 30 days from completed_at. See snapshot-and-log.mdx § Webhook activity log pattern.', max_length=200, ), + NoExplicitNull, ] = None @@ -472,17 +520,20 @@ class ListCreativesResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) Field( description='Breakdown of creatives by canonical format kind. Keys SHOULD be `format_kind` values; an implementation may append a stable option suffix when separate product or publisher options must be distinguished.' ), + NoExplicitNull, ] = None status_summary: Annotated[ - StatusSummary | None, Field(description='Breakdown of creatives by status') + StatusSummary | None, Field(description='Breakdown of creatives by status'), NoExplicitNull ] = None errors: Annotated[ list[error.Error] | None, Field(description='Task-specific errors (e.g., invalid filters, account not found)'), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_transformers_request.py b/src/adcp/types/domains/creative/list_transformers_request.py index ad24fcc9e..528f98b82 100644 --- a/src/adcp/types/domains/creative/list_transformers_request.py +++ b/src/adcp/types/domains/creative/list_transformers_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_transformers_request.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool @@ -44,6 +44,7 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): transformer_ids: Annotated[ list[str] | None, Field(description='Return only these specific transformer IDs.', min_length=1), + NoExplicitNull, ] = None input_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -52,6 +53,7 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='**DEPRECATED in 3.2.** Filter by legacy named input formats. Use input_format_kinds.', min_length=1, ), + NoExplicitNull, ] = None output_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -60,6 +62,7 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='**DEPRECATED in 3.2.** Filter by legacy named output formats. Use output_capability_ids.', min_length=1, ), + NoExplicitNull, ] = None input_format_kinds: Annotated[ list[str] | None, @@ -67,6 +70,7 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='Filter to transformers whose canonical input_formats include any of these canonical format kinds.', min_length=1, ), + NoExplicitNull, ] = None output_capability_ids: Annotated[ list[OutputCapabilityId] | None, @@ -74,16 +78,19 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description='Filter to transformers that can produce any of these canonical creative.supported_formats capability IDs.', min_length=1, ), + NoExplicitNull, ] = None name_search: Annotated[ str | None, Field(description='Search transformers by name (case-insensitive partial match).'), + NoExplicitNull, ] = None brief: Annotated[ str | None, Field( description="Natural-language brief used to rank and filter transformers (and their enumerable option values when expanded) — e.g. 'warm female Spanish-language voiceover'. Curates to intent rather than returning the full set, the way get_products curates inventory." ), + NoExplicitNull, ] = None expand_params: Annotated[ list[str] | None, @@ -91,6 +98,7 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description="Param `field` names for which to return the FIRST page of account-scoped option VALUES inline on each transformer's `params[].options[]`. Omit to return param descriptors without enumerated values (the lean default). When a param's options are truncated, its `params[].options_cursor` is set — fetch the next page via `expand_pagination` (below).", min_length=1, ), + NoExplicitNull, ] = None expand_pagination: Annotated[ list[ExpandPaginationItem] | None, @@ -98,17 +106,20 @@ class ListTransformersRequestCreativeAgent(AdcpRequest, AdcpVersionEnvelope): description="Fetch the NEXT page of a specific param's account-scoped options, using the `options_cursor` a prior response returned for that `(transformer, param)`. Scoped per `(transformer_id, field)` so multiple params can be paged independently. Use this instead of `expand_params` once you hold a cursor.", min_length=1, ), + NoExplicitNull, ] = None include_pricing: Annotated[ StrictBool | None, Field(description='Include `pricing_options` on each transformer. Requires `account`.'), + NoExplicitNull, ] = False account: Annotated[ account_ref.AccountReference | None, Field( description='Account reference. Transformers are account-scoped — the returned set, the enumerable option values, and (with include_pricing) the rate card are all resolved for this credential.' ), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/list_transformers_response.py b/src/adcp/types/domains/creative/list_transformers_response.py index b96006e15..0e9145b39 100644 --- a/src/adcp/types/domains/creative/list_transformers_response.py +++ b/src/adcp/types/domains/creative/list_transformers_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_transformers_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -27,8 +28,10 @@ class ListTransformersResponseCreativeAgent(AdcpResponse, AdcpVersionEnvelope, P Field(description='Transformer descriptors matching the query.'), ] errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific errors and warnings.') + list[error.Error] | None, + Field(description='Task-specific errors and warnings.'), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/preview_creative_request.py b/src/adcp/types/domains/creative/preview_creative_request.py index 8c1daca29..204b77e1d 100644 --- a/src/adcp/types/domains/creative/preview_creative_request.py +++ b/src/adcp/types/domains/creative/preview_creative_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/preview_creative_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -42,12 +42,14 @@ class Input(AdCPBaseModel): Field( description="Macro values to use for this preview. Supports all universal macros from the format's supported_macros list." ), + NoExplicitNull, ] = None context_description: Annotated[ str | None, Field( description="Natural language description of the context for AI-generated content (e.g., 'User just searched for running shoes', 'Podcast discussing weather patterns')" ), + NoExplicitNull, ] = None @@ -57,11 +59,14 @@ class Input10(AdCPBaseModel): ) name: Annotated[str, Field(description='Human-readable name for this input set')] macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to use for this preview') + dict[str, str] | None, + Field(description='Macro values to use for this preview'), + NoExplicitNull, ] = None context_description: Annotated[ str | None, Field(description='Natural language description of the context for AI-generated content'), + NoExplicitNull, ] = None @@ -75,6 +80,7 @@ class Request(AdCPBaseModel): description='Canonical preview-operation selector for this batch item. Overrides the batch-level target_capability_id and MUST identify an advertised capability whose operations contains preview. If neither item nor batch supplies one, renderer inference is permitted only for a unique compatible preview capability.', pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None format_id: Annotated[ format_id_1.FormatReferenceStructuredObject | None, @@ -82,37 +88,46 @@ class Request(AdCPBaseModel): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format preview route. Use target_capability_id plus the canonical identity in creative_manifest.', ), + NoExplicitNull, ] = None creative_manifest: Annotated[ creative_manifest_1.CreativeManifest | None, Field(description='Complete creative manifest with all required assets.'), + NoExplicitNull, ] = None creative_id: Annotated[ str | None, Field( description='Creative-library identifier. Use instead of creative_manifest to preview a stored canonical creative.' ), + NoExplicitNull, ] = None inputs: Annotated[ list[Input10] | None, Field( description='Array of input sets for generating multiple preview variants', min_length=1 ), + NoExplicitNull, ] = None template_id: Annotated[ - str | None, Field(description='Specific template ID for custom format rendering') + str | None, + Field(description='Specific template ID for custom format rendering'), + NoExplicitNull, ] = None quality: Annotated[ creative_quality.CreativeQuality | None, Field(description='Render quality for this preview. Overrides batch-level default.'), + NoExplicitNull, ] = None output_format: Annotated[ preview_output_format.PreviewOutputFormat | None, Field(description='Output format for this preview. Overrides batch-level default.'), + NoExplicitNull, ] = preview_output_format.PreviewOutputFormat.url item_limit: Annotated[ SchemaInt | None, Field(description='Maximum number of catalog items to render in this preview.', ge=1), + NoExplicitNull, ] = None @@ -131,6 +146,7 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Complete creative manifest with all required assets for the format. In single mode, provide exactly one of creative_manifest or creative_id. Also accepted per item in batch mode.' ), + NoExplicitNull, ] = None target_capability_id: Annotated[ str | None, @@ -138,6 +154,7 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description="Canonical preview-operation selector. Identifies one get_adcp_capabilities creative.supported_formats[].capability_id entry whose operations contains preview. In single mode it selects the renderer for this request; in batch mode it is the default for items that omit their own target_capability_id. When omitted, the agent MAY resolve the renderer only if exactly one advertised preview capability satisfies the manifest's canonical declaration; zero matches or multiple matches MUST be rejected with FORMAT_NOT_SUPPORTED rather than choosing nondeterministically. Mutually exclusive with deprecated format_id.", pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None format_id: Annotated[ format_id_1.FormatReferenceStructuredObject | None, @@ -145,6 +162,7 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format preview route. New requests select an advertised preview renderer with target_capability_id and carry portable format identity in creative_manifest_1.format_kind plus optional creative_manifest_1.format_option_ref.', ), + NoExplicitNull, ] = None inputs: Annotated[ list[Input] | None, @@ -152,22 +170,26 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Array of input sets for generating multiple preview variants. Each input set defines macros and context values for one preview rendering. Used in single mode.', min_length=1, ), + NoExplicitNull, ] = None template_id: Annotated[ str | None, Field(description='Specific template ID for custom format rendering. Used in single mode.'), + NoExplicitNull, ] = None quality: Annotated[ creative_quality.CreativeQuality | None, Field( description="Render quality. 'draft' produces fast, lower-fidelity renderings. 'production' produces full-quality renderings. In batch mode, sets the default for all requests (individual items can override)." ), + NoExplicitNull, ] = None output_format: Annotated[ preview_output_format.PreviewOutputFormat | None, Field( description="Output format. 'url' returns preview_url (iframe-embeddable URL), 'html' returns preview_html (raw HTML). In batch mode, sets the default for all requests (individual items can override). Default: 'url'." ), + NoExplicitNull, ] = preview_output_format.PreviewOutputFormat.url item_limit: Annotated[ SchemaInt | None, @@ -175,6 +197,7 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Maximum number of catalog items to render per preview variant. Used in single mode. Creative agents SHOULD default to a reasonable sample when omitted and the catalog is large.', ge=1, ), + NoExplicitNull, ] = None requests: Annotated[ list[Request] | None, @@ -183,30 +206,35 @@ class PreviewCreativeRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None variant_id: Annotated[ str | None, Field( description="Agent-assigned AdCP served-execution identifier from get_creative_delivery. Required when request_type is 'variant'. It is agent-unique when the source agent advertises creative.supports_revisions; for legacy agents the published scope remains agent plus creative, and callers SHOULD also send creative_id to disambiguate reused values." ), + NoExplicitNull, ] = None creative_id: Annotated[ str | None, Field( description='Creative-library identifier. In single mode, previews the stored canonical creative without requiring the caller to reconstruct its manifest. Also available as context in variant mode.' ), + NoExplicitNull, ] = None allow_async: Annotated[ StrictBool | None, Field( description="Opt in to an asynchronous preview response. When true, the creative agent MAY return status 'submitted' with a task_id only when rendering has been handed to a queue or external renderer and will continue after the request connection is released. Active processing on an open connection uses working progress instead. The buyer polls get_task_status for completion. When false or absent, the agent MUST return a synchronous preview response or a terminal protocol error; it MUST NOT return the submitted shape. This field applies to preview_creative only; build_creative already defines its own async lifecycle." ), + NoExplicitNull, ] = False push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for terminal completion/failure notifications when allow_async is true and preview_creative returns a submitted task envelope. Submitted tasks remain pollable through get_task_status whether or not this field is present. If the agent accepts this configuration and returns submitted, it MUST deliver at least the terminal notification; if it cannot honor the webhook, it MUST return a structured error. Presence of this field alone MUST NOT cause asynchronous execution.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/preview_creative_response.py b/src/adcp/types/domains/creative/preview_creative_response.py index 82d3d678d..d5734aea2 100644 --- a/src/adcp/types/domains/creative/preview_creative_response.py +++ b/src/adcp/types/domains/creative/preview_creative_response.py @@ -103,7 +103,7 @@ class PreviewCreativeResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelo class PreviewCreativeResponse4(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') response_type: Literal['submitted'] = 'submitted' status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str diff --git a/src/adcp/types/domains/creative/preview_render.py b/src/adcp/types/domains/creative/preview_render.py index 30d08fbbb..a3284dfe4 100644 --- a/src/adcp/types/domains/creative/preview_render.py +++ b/src/adcp/types/domains/creative/preview_render.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/preview_render.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool, StrictFloat from ..core import preview_renderer_metadata @@ -27,12 +27,17 @@ class Embedding(AdCPBaseModel): requires_https: Annotated[ StrictBool | None, Field(description='Whether this output requires HTTPS for secure embedding'), + NoExplicitNull, ] = None supports_fullscreen: Annotated[ - StrictBool | None, Field(description='Whether this output supports fullscreen mode') + StrictBool | None, + Field(description='Whether this output supports fullscreen mode'), + NoExplicitNull, ] = None csp_policy: Annotated[ - str | None, Field(description='Content Security Policy requirements for embedding') + str | None, + Field(description='Content Security Policy requirements for embedding'), + NoExplicitNull, ] = None @@ -59,17 +64,19 @@ class PreviewRender1(AdCPBaseModel): ), ] dimensions: Annotated[ - Dimensions | None, Field(description='Dimensions for this rendered piece') + Dimensions | None, Field(description='Dimensions for this rendered piece'), NoExplicitNull ] = None embedding: Annotated[ Embedding | None, Field(description='Optional security and embedding metadata for safe iframe integration'), + NoExplicitNull, ] = None renderer: Annotated[ preview_renderer_metadata.PreviewRendererMetadata | None, Field( description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.' ), + NoExplicitNull, ] = None @@ -96,16 +103,19 @@ class PreviewRender2(AdCPBaseModel): ), ] dimensions: Annotated[ - Dimensions | None, Field(description='Dimensions for this rendered piece') + Dimensions | None, Field(description='Dimensions for this rendered piece'), NoExplicitNull ] = None embedding: Annotated[ - Embedding | None, Field(description='Optional security and embedding metadata') + Embedding | None, + Field(description='Optional security and embedding metadata'), + NoExplicitNull, ] = None renderer: Annotated[ preview_renderer_metadata.PreviewRendererMetadata | None, Field( description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.' ), + NoExplicitNull, ] = None @@ -141,17 +151,19 @@ class PreviewRender3(AdCPBaseModel): ), ] dimensions: Annotated[ - Dimensions | None, Field(description='Dimensions for this rendered piece') + Dimensions | None, Field(description='Dimensions for this rendered piece'), NoExplicitNull ] = None embedding: Annotated[ Embedding | None, Field(description='Optional security and embedding metadata for safe iframe integration'), + NoExplicitNull, ] = None renderer: Annotated[ preview_renderer_metadata.PreviewRendererMetadata | None, Field( description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/creative/sync_creatives_async_response_input_required.py b/src/adcp/types/domains/creative/sync_creatives_async_response_input_required.py index 64c70998f..aee77e31b 100644 --- a/src/adcp/types/domains/creative/sync_creatives_async_response_input_required.py +++ b/src/adcp/types/domains/creative/sync_creatives_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_async_response_input_required.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -25,7 +25,9 @@ class SyncCreativesInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why buyer input is needed') + Reason | None, + Field(description='Reason code indicating why buyer input is needed'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/sync_creatives_async_response_submitted.py b/src/adcp/types/domains/creative/sync_creatives_async_response_submitted.py index 15a54bc6d..ad8148467 100644 --- a/src/adcp/types/domains/creative/sync_creatives_async_response_submitted.py +++ b/src/adcp/types/domains/creative/sync_creatives_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,12 +36,14 @@ class SyncCreativesSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Batch ingestion queued; typical turnaround 15-30 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/sync_creatives_async_response_working.py b/src/adcp/types/domains/creative/sync_creatives_async_response_working.py index 24172dac3..ff0738ad6 100644 --- a/src/adcp/types/domains/creative/sync_creatives_async_response_working.py +++ b/src/adcp/types/domains/creative/sync_creatives_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -18,20 +18,30 @@ class SyncCreativesWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None creatives_processed: Annotated[ - SchemaInt | None, Field(description='Number of creatives processed so far', ge=0) + SchemaInt | None, + Field(description='Number of creatives processed so far', ge=0), + NoExplicitNull, ] = None creatives_total: Annotated[ - SchemaInt | None, Field(description='Total number of creatives to process', ge=0) + SchemaInt | None, + Field(description='Total number of creatives to process', ge=0), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/creative/sync_creatives_request.py b/src/adcp/types/domains/creative/sync_creatives_request.py index 11ff22d25..2d76b73e0 100644 --- a/src/adcp/types/domains/creative/sync_creatives_request.py +++ b/src/adcp/types/domains/creative/sync_creatives_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat, model_validator @@ -38,6 +38,7 @@ class Assignment(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None placement_ids: Annotated[ list[str] | None, @@ -45,6 +46,7 @@ class Assignment(AdCPBaseModel): description='Restrict this creative to specific placements within the package. When omitted, the creative is eligible for all placements.', min_length=1, ), + NoExplicitNull, ] = None @@ -60,8 +62,8 @@ class AssignmentOperations1(AdCPBaseModel): operation: Literal['assign'] = 'assign' creative_id: Annotated[str, Field(min_length=1)] package_id: Annotated[str, Field(min_length=1)] - weight: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0)] = None - placement_ids: Annotated[list[PlacementId] | None, Field(min_length=1)] = None + weight: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0), NoExplicitNull] = None + placement_ids: Annotated[list[PlacementId] | None, Field(min_length=1), NoExplicitNull] = None class AssignmentOperations2(AdCPBaseModel): @@ -83,8 +85,8 @@ class AssignmentOperations3(AdCPBaseModel): str, Field(description='Currently assigned creative ID to remove atomically.', min_length=1) ] package_id: Annotated[str, Field(min_length=1)] - weight: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0)] = None - placement_ids: Annotated[list[PlacementId] | None, Field(min_length=1)] = None + weight: Annotated[StrictFloat | None, Field(ge=0.0, le=100.0), NoExplicitNull] = None + placement_ids: Annotated[list[PlacementId] | None, Field(min_length=1), NoExplicitNull] = None AssignmentOperations = Annotated[ @@ -99,6 +101,7 @@ class Creative(CreativeAsset): Field( description='Optional buyer-assigned identity for this exact input-content state. Buyers may send it to any 3.2 peer; creative.supports_revisions gates reliance on immutability, echo, readback, and delivery correlation, not whether the open request shape may carry the field. Within a supporting seller, a revision_id is immutable under one creative_id: reusing it with different canonical revision content MUST fail with CREATIVE_REVISION_CONTENT_MISMATCH. Metadata and assignment fields excluded from revision content may change without minting a new revision.' ), + NoExplicitNull, ] = None localization: Annotated[ creative_localization.CreativeLocalization | None, @@ -122,6 +125,7 @@ class SyncCreativesRequest(AdcpRequest, AdcpVersionEnvelope): max_length=100, min_length=1, ), + NoExplicitNull, ] = None creative_ids: Annotated[ list[str] | None, @@ -130,6 +134,7 @@ class SyncCreativesRequest(AdcpRequest, AdcpVersionEnvelope): max_length=100, min_length=1, ), + NoExplicitNull, ] = None assignments: Annotated[ list[Assignment] | None, @@ -138,6 +143,7 @@ class SyncCreativesRequest(AdcpRequest, AdcpVersionEnvelope): description='Deprecated additive assignment shorthand. Each entry upserts one creative-to-package assignment. Use assignment_operations for explicit assign, unassign, and replace semantics. Standalone creative agents that do not manage media buys ignore this field.', min_length=1, ), + NoExplicitNull, ] = None assignment_operations: Annotated[ list[AssignmentOperations] | None, @@ -146,6 +152,7 @@ class SyncCreativesRequest(AdcpRequest, AdcpVersionEnvelope): max_length=500, min_length=1, ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -161,27 +168,31 @@ class SyncCreativesRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='When true, creatives not included in this sync will be archived. Use with caution for full library replacement. Invalid when creative_ids is provided — delete_missing applies to the entire library scope, not a filtered subset.' ), + NoExplicitNull, ] = False dry_run: Annotated[ StrictBool | None, Field( description="When true, rehearse this sync_creatives operation without applying it. Validates the actual trafficking request in the seller's current context, including library upsert semantics, creative IDs, assignments, account-scoped gates, and seller policies, then returns what would be created/updated/deleted. This is distinct from validate_input, which only validates manifest structure against canonical/product format targets." ), + NoExplicitNull, ] = False validation_mode: Annotated[ validation_mode_1.ValidationMode | None, Field( description="Validation strictness. 'strict' fails entire sync on any validation error. 'lenient' processes valid creatives and reports errors." ), + NoExplicitNull, ] = validation_mode_1.ValidationMode.strict push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async sync notifications. The agent will send a webhook when sync completes if the operation takes longer than immediate response time (typically for large bulk operations or manual approval/HITL).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> SyncCreativesRequest: diff --git a/src/adcp/types/domains/creative/sync_creatives_response.py b/src/adcp/types/domains/creative/sync_creatives_response.py index 5a11c8cce..20cdbd188 100644 --- a/src/adcp/types/domains/creative/sync_creatives_response.py +++ b/src/adcp/types/domains/creative/sync_creatives_response.py @@ -60,7 +60,7 @@ class SyncCreativesResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope class SyncCreativesResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/creative/validate_input_request.py b/src/adcp/types/domains/creative/validate_input_request.py index a8bfc5f2a..5373a681c 100644 --- a/src/adcp/types/domains/creative/validate_input_request.py +++ b/src/adcp/types/domains/creative/validate_input_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/validate_input_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import account_ref, brand_ref, creative_manifest @@ -70,12 +70,14 @@ class ValidateInputRequest(AdcpRequest, AdCPBaseModel): Field( description='Optional account scope for seller-specific product validation. Required by sellers that route product declarations by buyer account.' ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Optional brand scope when account is omitted or the seller keys sandbox validation by brand identity.' ), + NoExplicitNull, ] = None manifest: Annotated[ creative_manifest.CreativeManifest, Field(description='Creative manifest to validate.') @@ -87,4 +89,5 @@ class ValidateInputRequest(AdcpRequest, AdCPBaseModel): max_length=50, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/creative/validate_input_result.py b/src/adcp/types/domains/creative/validate_input_result.py index c7b4fbbf7..d91ebb98b 100644 --- a/src/adcp/types/domains/creative/validate_input_result.py +++ b/src/adcp/types/domains/creative/validate_input_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: creative/validate_input_result.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import macro_resolution_result @@ -67,6 +67,7 @@ class Violation(AdCPBaseModel): Field( description='Optional advisory adjustment hint. Platforms MAY suggest a corrected input shape; buyers MUST treat this as advisory, not authoritative.' ), + NoExplicitNull, ] = None @@ -95,16 +96,19 @@ class ValidateInputResult(AdCPBaseModel): Field( description="When `result_kind` is `validated_fail`, the specific constraints the manifest fails to meet. MUST be absent (or empty) for `validated_pass` and `unvalidatable_nondeterministic` — neither has constraint violations to enumerate (`unvalidatable_nondeterministic` doesn't validate at all; `validated_pass` has nothing to fail)." ), + NoExplicitNull, ] = None warnings: Annotated[ list[Warning] | None, Field( description='Non-blocking observations (e.g. LEAN policy advisories such as hover-triggered expansion or non-user-initiated entry into overlay anchoring) that do not affect `result_kind`. MAY be present alongside `validated_pass`, `validated_fail`, or `unvalidatable_nondeterministic`. Same item shape as `violations`.' ), + NoExplicitNull, ] = None macro_resolution_results: Annotated[ list[macro_resolution_result.MacroResolutionResult] | None, Field( description='Per-token compatibility against the selected target. Required or unsafe `unsupported`/`ambiguous` results make the target `validated_fail`; deliberate preservation for the declared downstream resolver may still pass.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/creative/video_brief.py b/src/adcp/types/domains/creative/video_brief.py index bee1c4e05..c01564708 100644 --- a/src/adcp/types/domains/creative/video_brief.py +++ b/src/adcp/types/domains/creative/video_brief.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: creative/video_brief.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field @@ -27,11 +27,13 @@ class Segment(AdCPBaseModel): description='Text prompt fed to the synthesis pipeline for this segment (subject, action, setting, mood). Renamed from earlier `description` to make explicit that this is a generation prompt — not a description-of-finished-content.' ), ] - vo: Annotated[str | None, Field(description='Voiceover line for this segment (optional).')] = ( - None - ) + vo: Annotated[ + str | None, Field(description='Voiceover line for this segment (optional).'), NoExplicitNull + ] = None caption: Annotated[ - str | None, Field(description='On-screen caption text for this segment (optional).') + str | None, + Field(description='On-screen caption text for this segment (optional).'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/accessibility_violation.py b/src/adcp/types/domains/error_details/accessibility_violation.py index dd15f47d1..a3eaa2001 100644 --- a/src/adcp/types/domains/error_details/accessibility_violation.py +++ b/src/adcp/types/domains/error_details/accessibility_violation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/accessibility_violation.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool @@ -83,6 +83,7 @@ class Violation(AdCPBaseModel): max_length=256, pattern='^[^\\u0000-\\u001F\\u007F]*$', ), + NoExplicitNull, ] = None @@ -103,4 +104,5 @@ class AccessibilityViolationDetails(AdCPBaseModel): Field( description='True when the producer omitted additional violations or remediation text to keep the complete serialized error within the 4096-byte transport safety limit. Omission means false.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/account_moved.py b/src/adcp/types/domains/error_details/account_moved.py index 49548a938..f6508c0dc 100644 --- a/src/adcp/types/domains/error_details/account_moved.py +++ b/src/adcp/types/domains/error_details/account_moved.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/account_moved.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ..core import canonical_account_ref @@ -27,4 +27,5 @@ class AccountMovedDetails(AdCPBaseModel): Field( description='Current account revision when the seller exposes account revisions.', ge=1 ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/account_setup_required.py b/src/adcp/types/domains/error_details/account_setup_required.py index 4b7b5875b..a32396035 100644 --- a/src/adcp/types/domains/error_details/account_setup_required.py +++ b/src/adcp/types/domains/error_details/account_setup_required.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/account_setup_required.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -15,8 +15,10 @@ class AccountSetupRequiredDetails(AdCPBaseModel): extra='allow', ) setup_url: Annotated[ - AnyUrl | None, Field(description='URL where account setup can be completed') + AnyUrl | None, Field(description='URL where account setup can be completed'), NoExplicitNull ] = None setup_steps: Annotated[ - list[str] | None, Field(description='Steps remaining before the account is ready') + list[str] | None, + Field(description='Steps remaining before the account is ready'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/action_not_allowed.py b/src/adcp/types/domains/error_details/action_not_allowed.py index fad18a7d1..899f9837b 100644 --- a/src/adcp/types/domains/error_details/action_not_allowed.py +++ b/src/adcp/types/domains/error_details/action_not_allowed.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/action_not_allowed.json -# timestamp: 2026-09-24T08:59:48+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import media_buy_available_action, media_buy_available_action_id @@ -34,10 +34,12 @@ class ActionNotAllowedDetails(AdCPBaseModel): Field( description="Echo of the buy's resolved `available_actions[]` at rejection time. Buyer SDKs render this to the caller as the recovery option set." ), + NoExplicitNull, ] = None decline_reason: Annotated[ seller_policy_decline_reason.SellerPolicyDeclineReason | None, Field( description='Coarse seller-policy dimension behind a buy-specific denial. Sellers SHOULD populate this for `reason: not_supported_on_buy` when they have a structured reason and disclosure does not expose private thresholds or enforcement controls. It SHOULD be omitted for structural reasons such as `wrong_status`, `not_supported_on_product`, `mode_mismatch`, and `condition_unresolved`. When present, it MUST be consistent with the enclosing error.recovery classification.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/audience_too_small.py b/src/adcp/types/domains/error_details/audience_too_small.py index 0ea9a883e..dec88dc79 100644 --- a/src/adcp/types/domains/error_details/audience_too_small.py +++ b/src/adcp/types/domains/error_details/audience_too_small.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/audience_too_small.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -15,6 +15,8 @@ class AudienceTooSmallDetails(AdCPBaseModel): extra='allow', ) minimum_size: Annotated[ - StrictFloat | None, Field(description='Minimum audience size required') + StrictFloat | None, Field(description='Minimum audience size required'), NoExplicitNull + ] = None + current_size: Annotated[ + StrictFloat | None, Field(description='Current audience size'), NoExplicitNull ] = None - current_size: Annotated[StrictFloat | None, Field(description='Current audience size')] = None diff --git a/src/adcp/types/domains/error_details/authorization_required.py b/src/adcp/types/domains/error_details/authorization_required.py index b49400dab..da78aeb09 100644 --- a/src/adcp/types/domains/error_details/authorization_required.py +++ b/src/adcp/types/domains/error_details/authorization_required.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/authorization_required.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import downstream_connection_requirement @@ -21,24 +21,28 @@ class AuthorizationRequiredDetails(AdCPBaseModel): Field( description='Complete set of downstream connections known to be required for the relevant product, format, or request.' ), + NoExplicitNull, ] = None missing_connections: Annotated[ list[downstream_connection_requirement.DownstreamConnectionRequirement] | None, Field( description='Subset of downstream connections that blocked the current request. Sellers SHOULD populate this array when the caller needs to route a human through a connections flow. Entries with `status` of `missing`, `pending`, `expired`, or `revoked` MUST include either `provider` or `authorization_url` so the buyer can route the remediation unambiguously.' ), + NoExplicitNull, ] = None authorization_url: Annotated[ AnyUrl | None, Field( description='General recovery URL when there is a single obvious authorization step or when the seller has its own connection-management page.' ), + NoExplicitNull, ] = None authorization_instructions: Annotated[ str | None, Field( description='Human-readable recovery instructions. Use `missing_connections[].authorization_instructions` when instructions differ per downstream connection.' ), + NoExplicitNull, ] = None reference_authorization: Annotated[ dict[str, Any] | None, @@ -46,4 +50,5 @@ class AuthorizationRequiredDetails(AdCPBaseModel): deprecated=True, description='Legacy or provider-specific authorization hint for the referenced object. Prefer `missing_connections[]` for new implementations.', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/billing_not_permitted_for_agent.py b/src/adcp/types/domains/error_details/billing_not_permitted_for_agent.py index 113dae0a8..8eae1ad5e 100644 --- a/src/adcp/types/domains/error_details/billing_not_permitted_for_agent.py +++ b/src/adcp/types/domains/error_details/billing_not_permitted_for_agent.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/billing_not_permitted_for_agent.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import billing_party @@ -27,4 +27,5 @@ class BillingNotPermittedForAgentDetails(AdCPBaseModel): Field( description="A single billing value the calling buyer agent MAY retry with autonomously. Typically `operator` for passthrough-only agents. Sellers MAY omit this field when no retryable value exists for this agent's commercial relationship — in that case the rejection is terminal-pending-onboarding and the buyer MUST complete offline payments-relationship onboarding before any value other than the original request will succeed. Sellers MUST NOT populate this field with the full subset of values the agent may use; the field surfaces at most one canonical retry." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/billing_not_supported.py b/src/adcp/types/domains/error_details/billing_not_supported.py index ca1042ad1..f7e3ac825 100644 --- a/src/adcp/types/domains/error_details/billing_not_supported.py +++ b/src/adcp/types/domains/error_details/billing_not_supported.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/billing_not_supported.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import billing_party @@ -27,6 +27,7 @@ class BillingNotSupportedDetails(AdCPBaseModel): Field( description='Which gate fired. `"capability"` — the seller\'s `supported_billing` capability does not include the requested value (advice: choose from `supported_billing` and retry). `"account"` — the seller\'s capability accepts the value generally but not for the specific operator on this account (advice: try the next-most-permissive value the capability allows). Registered subset of the shared discriminator vocabulary in `enums/error-scope.json` — the per-agent scope is reserved for `BILLING_NOT_PERMITTED_FOR_AGENT` (which carries no explicit `scope` field) and for `PERMISSION_DENIED` with `error-details/agent-permission-denied.json`. Sellers MUST omit this field when the response is being returned to a caller whose agent identity has not been established (signed-request derivation or credential-to-agent mapping); see the uniform-response requirement in error-handling.mdx Billing and Account Setup.' ), + NoExplicitNull, ] = None supported_billing: Annotated[ list[billing_party.BillingParty] | None, @@ -34,4 +35,5 @@ class BillingNotSupportedDetails(AdCPBaseModel): description='Echoed copy of the seller\'s `supported_billing` capability for the caller\'s convenience — saves a `get_adcp_capabilities` round-trip when `scope` is `"capability"`. Sellers MAY omit this field; callers MUST treat absence as "call `get_adcp_capabilities`" rather than "any value is permitted."', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/budget_too_low.py b/src/adcp/types/domains/error_details/budget_too_low.py index bc2f8d718..b05c3c1b6 100644 --- a/src/adcp/types/domains/error_details/budget_too_low.py +++ b/src/adcp/types/domains/error_details/budget_too_low.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/budget_too_low.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -15,6 +15,10 @@ class BudgetTooLowDetails(AdCPBaseModel): extra='allow', ) minimum_budget: Annotated[ - StrictFloat | None, Field(description="Seller's minimum budget for this product") + StrictFloat | None, + Field(description="Seller's minimum budget for this product"), + NoExplicitNull, ] = None - currency: Annotated[str | None, Field(description='ISO 4217 currency code')] = None + currency: Annotated[str | None, Field(description='ISO 4217 currency code'), NoExplicitNull] = ( + None + ) diff --git a/src/adcp/types/domains/error_details/conflict.py b/src/adcp/types/domains/error_details/conflict.py index 4095d020c..c2365a5ac 100644 --- a/src/adcp/types/domains/error_details/conflict.py +++ b/src/adcp/types/domains/error_details/conflict.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/conflict.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -15,12 +15,15 @@ class ConflictDetails(AdCPBaseModel): extra='allow', ) resource_id: Annotated[ - str | None, Field(description='Identifier of the conflicting resource') + str | None, Field(description='Identifier of the conflicting resource'), NoExplicitNull ] = None expected_version: Annotated[ StrictFloat | str | None, Field(description='Version or ETag the client was operating against'), + NoExplicitNull, ] = None current_version: Annotated[ - StrictFloat | str | None, Field(description='Current version or ETag on the server') + StrictFloat | str | None, + Field(description='Current version or ETag on the server'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/creative_rejected.py b/src/adcp/types/domains/error_details/creative_rejected.py index 458eb442c..fe18694f7 100644 --- a/src/adcp/types/domains/error_details/creative_rejected.py +++ b/src/adcp/types/domains/error_details/creative_rejected.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/creative_rejected.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -14,10 +14,16 @@ class CreativeRejectedDetails(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - policy_id: Annotated[str | None, Field(description='Identifier for the violated policy')] = None + policy_id: Annotated[ + str | None, Field(description='Identifier for the violated policy'), NoExplicitNull + ] = None policy_url: Annotated[ - AnyUrl | None, Field(description='URL where the full policy can be reviewed') + AnyUrl | None, + Field(description='URL where the full policy can be reviewed'), + NoExplicitNull, ] = None reasons: Annotated[ - list[str] | None, Field(description='Specific reasons the creative was rejected') + list[str] | None, + Field(description='Specific reasons the creative was rejected'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/governance_agent_not_accepted.py b/src/adcp/types/domains/error_details/governance_agent_not_accepted.py index 5b6d9c9fd..f9e072682 100644 --- a/src/adcp/types/domains/error_details/governance_agent_not_accepted.py +++ b/src/adcp/types/domains/error_details/governance_agent_not_accepted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/governance_agent_not_accepted.json -# timestamp: 2026-10-02T23:18:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..governance import accepted_governance_agents as accepted_governance_agents_1 @@ -38,6 +38,7 @@ class GovernanceAgentNotAcceptedDetails2(AdCPBaseModel): max_length=128, pattern='^[A-Za-z0-9_.:-]+$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/policy_violation.py b/src/adcp/types/domains/error_details/policy_violation.py index 4e1dd159f..a512732bc 100644 --- a/src/adcp/types/domains/error_details/policy_violation.py +++ b/src/adcp/types/domains/error_details/policy_violation.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/policy_violation.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -26,12 +26,14 @@ class PolicyViolationDetails(AdCPBaseModel): Field( description='Whose policy produced the rejection. Optional so undisclosed seller policies can return only the typed POLICY_VIOLATION code if even origin would reveal sensitive information.' ), + NoExplicitNull, ] = None policy_id: Annotated[ str | None, Field( description='Shared policy-registry identifier for the violated policy. Use seller_policy_ref instead when the seller is not disclosing a registry policy.' ), + NoExplicitNull, ] = None seller_policy_ref: Annotated[ str | None, @@ -39,12 +41,15 @@ class PolicyViolationDetails(AdCPBaseModel): description='Opaque seller-scoped reference that lets support and audit records correlate an undisclosed policy without publishing its rules. This is not a shared policy-registry ID.', min_length=1, ), + NoExplicitNull, ] = None policy_url: Annotated[ - AnyUrl | None, Field(description='URL where the full policy can be reviewed') + AnyUrl | None, + Field(description='URL where the full policy can be reviewed'), + NoExplicitNull, ] = None violated_rules: Annotated[ - list[str] | None, Field(description='Specific rules that were violated') + list[str] | None, Field(description='Specific rules that were violated'), NoExplicitNull ] = None category: Annotated[ str | None, @@ -52,4 +57,5 @@ class PolicyViolationDetails(AdCPBaseModel): description="Optional coarse policy category disclosed at the seller's discretion.", pattern='^[a-z][a-z0-9_]*$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/rate_limited.py b/src/adcp/types/domains/error_details/rate_limited.py index 16ef25ce8..2cf234ca1 100644 --- a/src/adcp/types/domains/error_details/rate_limited.py +++ b/src/adcp/types/domains/error_details/rate_limited.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/rate_limited.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -22,12 +22,20 @@ class RateLimitedDetails(AdCPBaseModel): extra='allow', ) limit: Annotated[ - StrictFloat | None, Field(description='Maximum requests allowed in the window') + StrictFloat | None, + Field(description='Maximum requests allowed in the window'), + NoExplicitNull, ] = None remaining: Annotated[ - StrictFloat | None, Field(description='Requests remaining in the current window') + StrictFloat | None, + Field(description='Requests remaining in the current window'), + NoExplicitNull, ] = None window_seconds: Annotated[ - StrictFloat | None, Field(description='Duration of the rate-limit window in seconds') + StrictFloat | None, + Field(description='Duration of the rate-limit window in seconds'), + NoExplicitNull, + ] = None + scope: Annotated[ + Scope | None, Field(description='What the limit applies to'), NoExplicitNull ] = None - scope: Annotated[Scope | None, Field(description='What the limit applies to')] = None diff --git a/src/adcp/types/domains/error_details/requote_required.py b/src/adcp/types/domains/error_details/requote_required.py index 3eb8c2608..ee75c1f39 100644 --- a/src/adcp/types/domains/error_details/requote_required.py +++ b/src/adcp/types/domains/error_details/requote_required.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/requote_required.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, RootModel @@ -46,16 +46,19 @@ class RequoteRequiredDetails(AdCPBaseModel): Field( description='Buyer-visible request field path or paths that breached the accepted commercial envelope. Values MUST NOT name seller-internal objects or identifiers. New emitters SHOULD use the array form even for one path; the scalar form remains valid for compatibility with existing 3.x sellers.' ), + NoExplicitNull, ] = None change_term_id: Annotated[ media_buy_change_term_id.MediaBuyChangeTermId | None, Field( description='Accepted proposal change term whose typed constraint the request exceeded.' ), + NoExplicitNull, ] = None decline_reason: Annotated[ seller_policy_decline_reason.SellerPolicyDeclineReason | None, Field( description='Coarse seller-policy dimension that declined the requested shape. Sellers SHOULD populate this when they have a structured reason and disclosure does not expose private thresholds or enforcement controls. When present, it MUST be consistent with the enclosing error.recovery classification.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/stale_response.py b/src/adcp/types/domains/error_details/stale_response.py index 77b47bdd8..b9fcb53f9 100644 --- a/src/adcp/types/domains/error_details/stale_response.py +++ b/src/adcp/types/domains/error_details/stale_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: error_details/stale_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field @@ -19,12 +19,14 @@ class Upstream(AdCPBaseModel): Field( description='URL of the unreachable upstream, when the upstream is HTTP-addressable. Sellers MUST omit when the upstream is an internal (non-URL) dependency.' ), + NoExplicitNull, ] = None name: Annotated[ str | None, Field( description="Human-readable identifier for the upstream, e.g., 'creative-agent-foo' or 'inventory-service'. Used when the upstream is internal or when the URL is not informative on its own." ), + NoExplicitNull, ] = None @@ -37,12 +39,14 @@ class OriginalError(AdCPBaseModel): Field( description="Error code from the underlying failure, e.g., SERVICE_UNAVAILABLE, or a transport-level identifier like 'TIMEOUT' / 'CONNECTION_REFUSED' when the underlying call did not produce an AdCP error code." ), + NoExplicitNull, ] = None message: Annotated[ str | None, Field( description="Short human-readable description of the underlying failure (e.g., 'Connection timeout after 20s'). MUST NOT include credentials, internal hostnames, or stack traces." ), + NoExplicitNull, ] = None @@ -69,16 +73,19 @@ class StaleResponseDetails(AdCPBaseModel): description="The seller's freshness target for this surface, in seconds. `cache_age_seconds - freshness_target_seconds` is how far beyond target the cached entry is. Optional — sellers MAY omit when no public freshness contract is declared.", ge=0, ), + NoExplicitNull, ] = None upstream: Annotated[ Upstream | None, Field( description='Identifies the upstream or sub-agent whose fetch failed and triggered cache fallback. When N upstreams are stale, the seller emits N separate STALE_RESPONSE entries (one per upstream) rather than aggregating into a single entry — mirrors the per-asset advisory precedent set by PIXEL_TRACKER_LOSSY_DOWNGRADE.' ), + NoExplicitNull, ] = None original_error: Annotated[ OriginalError | None, Field( description='Minimal subset of the underlying failure that triggered cache fallback. Sellers MUST NOT include internal stack traces, credentials, or connection strings.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/vast_version_mismatch.py b/src/adcp/types/domains/error_details/vast_version_mismatch.py index 1d19e8d17..cc07dbcc4 100644 --- a/src/adcp/types/domains/error_details/vast_version_mismatch.py +++ b/src/adcp/types/domains/error_details/vast_version_mismatch.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/vast_version_mismatch.json -# timestamp: 2026-10-04T00:56:30+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import format_option_ref as format_option_ref_1 @@ -46,6 +46,7 @@ class VastVersionMismatchDetails1(AdCPBaseModel): Field( description='Role of the inspected document. Only the submitted document is required to equal the asset declaration; wrapper and terminal documents are checked against the applicable acceptance sets.' ), + NoExplicitNull, ] = None product_vast_versions: Annotated[list[vast_version.VastVersion], Field(min_length=1)] seller_vast_versions: Annotated[list[vast_version.VastVersion], Field(min_length=1)] @@ -54,6 +55,7 @@ class VastVersionMismatchDetails1(AdCPBaseModel): Field( description='Selected option reference when the product option is addressable. Omitted for a unique id-less product option.' ), + NoExplicitNull, ] = None supported_versions: Annotated[ list[vast_version.VastVersion] | None, @@ -62,6 +64,7 @@ class VastVersionMismatchDetails1(AdCPBaseModel): description='Deprecated 3.x compatibility field. For an acceptance mismatch, this is the effective product/seller intersection when known. Modern emitters use the typed sets and mismatch_reason.', min_length=1, ), + NoExplicitNull, ] = None @@ -87,17 +90,18 @@ class VastVersionMismatchDetails2(AdCPBaseModel): description='Role of the inspected document. Only the submitted document is required to equal the asset declaration; wrapper and terminal documents are checked against the applicable acceptance sets.' ), ] - product_vast_versions: Annotated[list[vast_version.VastVersion] | None, Field(min_length=1)] = ( - None - ) - seller_vast_versions: Annotated[list[vast_version.VastVersion] | None, Field(min_length=1)] = ( - None - ) + product_vast_versions: Annotated[ + list[vast_version.VastVersion] | None, Field(min_length=1), NoExplicitNull + ] = None + seller_vast_versions: Annotated[ + list[vast_version.VastVersion] | None, Field(min_length=1), NoExplicitNull + ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Selected option reference when the product option is addressable. Omitted for a unique id-less product option.' ), + NoExplicitNull, ] = None supported_versions: Annotated[ list[vast_version.VastVersion] | None, @@ -106,6 +110,7 @@ class VastVersionMismatchDetails2(AdCPBaseModel): description='Deprecated 3.x compatibility field. For an acceptance mismatch, this is the effective product/seller intersection when known. Modern emitters use the typed sets and mismatch_reason.', min_length=1, ), + NoExplicitNull, ] = None @@ -114,9 +119,11 @@ class VastVersionMismatchDetails3(AdCPBaseModel): extra='allow', ) mismatch_reason: Annotated[ - MismatchReason | None, Field(description='Machine-readable modern mismatch branch.') + MismatchReason | None, + Field(description='Machine-readable modern mismatch branch.'), + NoExplicitNull, ] = None - asset_vast_version: vast_version.VastVersion | None = None + asset_vast_version: Annotated[vast_version.VastVersion | None, NoExplicitNull] = None observed_document_vast_version: Annotated[ str | None, Field( @@ -129,18 +136,20 @@ class VastVersionMismatchDetails3(AdCPBaseModel): Field( description='Role of the inspected document. Only the submitted document is required to equal the asset declaration; wrapper and terminal documents are checked against the applicable acceptance sets.' ), + NoExplicitNull, + ] = None + product_vast_versions: Annotated[ + list[vast_version.VastVersion] | None, Field(min_length=1), NoExplicitNull + ] = None + seller_vast_versions: Annotated[ + list[vast_version.VastVersion] | None, Field(min_length=1), NoExplicitNull ] = None - product_vast_versions: Annotated[list[vast_version.VastVersion] | None, Field(min_length=1)] = ( - None - ) - seller_vast_versions: Annotated[list[vast_version.VastVersion] | None, Field(min_length=1)] = ( - None - ) format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Selected option reference when the product option is addressable. Omitted for a unique id-less product option.' ), + NoExplicitNull, ] = None supported_versions: Annotated[ list[vast_version.VastVersion], diff --git a/src/adcp/types/domains/error_details/vendor_error_codes.py b/src/adcp/types/domains/error_details/vendor_error_codes.py index 284bcee04..f6440abee 100644 --- a/src/adcp/types/domains/error_details/vendor_error_codes.py +++ b/src/adcp/types/domains/error_details/vendor_error_codes.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/vendor_error_codes.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, Field @@ -24,7 +24,9 @@ class Codes(AdCPBaseModel): class Vendors(AdCPBaseModel): name: Annotated[str, Field(description='Full vendor name')] - url: Annotated[AnyUrl | None, Field(description='Vendor website or documentation URL')] = None + url: Annotated[ + AnyUrl | None, Field(description='Vendor website or documentation URL'), NoExplicitNull + ] = None codes: Annotated[ dict[str, Codes], Field(description='Map of code suffix to description and recovery classification'), @@ -35,4 +37,5 @@ class VendorErrorCodeRegistry(AdCPBaseModel): vendors: Annotated[ dict[str, Vendors] | None, Field(description='Map of vendor prefix to vendor metadata and codes'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/error_details/version_unsupported.py b/src/adcp/types/domains/error_details/version_unsupported.py index dbfa92cfa..1c0fe528f 100644 --- a/src/adcp/types/domains/error_details/version_unsupported.py +++ b/src/adcp/types/domains/error_details/version_unsupported.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: error_details/version_unsupported.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt, ScalarStr - from typing import Annotated +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarInt, ScalarStr from pydantic import ConfigDict, Field from ..core.version_envelope import AdcpVersionEnvelope @@ -43,6 +43,7 @@ class VersionUnsupportedDetails(AdcpVersionEnvelope): deprecated=True, description='DEPRECATED in favor of `supported_versions`. Major versions the seller supports. Servers SHOULD emit both through 3.x; removed in 4.0.', ), + NoExplicitNull, ] = None build_version: Annotated[ str | None, @@ -56,4 +57,5 @@ class VersionUnsupportedDetails(AdcpVersionEnvelope): ], pattern='^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$', ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/extensions/extension_meta.py b/src/adcp/types/domains/extensions/extension_meta.py index 6aa395838..766691f9e 100644 --- a/src/adcp/types/domains/extensions/extension_meta.py +++ b/src/adcp/types/domains/extensions/extension_meta.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: extensions/extension_meta.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, Field @@ -29,9 +29,12 @@ class AdcpExtensionFileSchema(AdCPBaseModel): description="Last AdCP version this extension is compatible with (e.g., '3.0'). Omit if extension is still valid for current and future versions.", pattern='^\\d+\\.\\d+$', ), + NoExplicitNull, ] = None docs_url: Annotated[ - AnyUrl | None, Field(description='URL to documentation for implementors of this extension') + AnyUrl | None, + Field(description='URL to documentation for implementors of this extension'), + NoExplicitNull, ] = None type: Annotated[ Literal['object'], @@ -42,7 +45,9 @@ class AdcpExtensionFileSchema(AdCPBaseModel): Field(description='Schema properties defining the structure of ext.{namespace} data'), ] required: Annotated[ - list[str] | None, Field(description='Required properties within the extension data') + list[str] | None, + Field(description='Required properties within the extension data'), + NoExplicitNull, ] = None additionalProperties: Annotated[ Any | None, diff --git a/src/adcp/types/domains/formats/canonical/_base.py b/src/adcp/types/domains/formats/canonical/_base.py index 11adf76c7..3fe546af2 100644 --- a/src/adcp/types/domains/formats/canonical/_base.py +++ b/src/adcp/types/domains/formats/canonical/_base.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/_base.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat from pydantic import ConfigDict, Field, StrictBool @@ -77,13 +77,19 @@ class Slot(AdCPBaseModel): ), ] required: Annotated[ - StrictBool | None, Field(description='Whether this slot is required for a valid manifest.') + StrictBool | None, + Field(description='Whether this slot is required for a valid manifest.'), + NoExplicitNull, ] = False min: Annotated[ - SchemaInt | None, Field(description='Minimum count for repeatable / pool slots.', ge=0) + SchemaInt | None, + Field(description='Minimum count for repeatable / pool slots.', ge=0), + NoExplicitNull, ] = None max: Annotated[ - SchemaInt | None, Field(description='Maximum count for repeatable / pool slots.', ge=1) + SchemaInt | None, + Field(description='Maximum count for repeatable / pool slots.', ge=1), + NoExplicitNull, ] = None max_chars: Annotated[ SchemaInt | None, @@ -91,6 +97,7 @@ class Slot(AdCPBaseModel): description="Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.", ge=1, ), + NoExplicitNull, ] = None max_size_kb: Annotated[ SchemaInt | None, @@ -98,6 +105,7 @@ class Slot(AdCPBaseModel): description="Per-slot file size limit in exact kilobytes, where 1 KB = 1,000 bytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.", ge=1, ), + NoExplicitNull, ] = None pixel_ratios: Annotated[ list[PixelRatio] | None, @@ -105,6 +113,7 @@ class Slot(AdCPBaseModel): description="Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.", min_length=1, ), + NoExplicitNull, ] = None required_pixel_ratios: Annotated[ list[RequiredPixelRatio] | None, @@ -112,28 +121,33 @@ class Slot(AdCPBaseModel): description='Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.', min_length=1, ), + NoExplicitNull, ] = None logo_slots: Annotated[ list[logo_slot.LogoSlot] | None, Field( description='When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.' ), + NoExplicitNull, ] = None required_logo_slots: Annotated[ list[logo_slot.LogoSlot] | None, Field( description='Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.' ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field(description='Human-readable description of what the slot expects from the buyer.'), + NoExplicitNull, ] = None consumed_for_production: Annotated[ StrictBool | None, Field( description="Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`, `audio_vast`), the default `false` is sufficient and the flag MAY be omitted." ), + NoExplicitNull, ] = False @@ -146,18 +160,21 @@ class CanonicalFormatBase(AdCPBaseModel): Field( description='When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.' ), + NoExplicitNull, ] = False deprecated: Annotated[ StrictBool | None, Field( description="When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path." ), + NoExplicitNull, ] = False v1_translatable: Annotated[ StrictBool | None, Field( description="Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`." ), + NoExplicitNull, ] = True since_version: Annotated[ str | None, @@ -165,6 +182,7 @@ class CanonicalFormatBase(AdCPBaseModel): description="AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.", pattern='^[1-9]\\d*\\.(0|[1-9]\\d*)$', ), + NoExplicitNull, ] = None migration_target_version: Annotated[ str | None, @@ -172,48 +190,56 @@ class CanonicalFormatBase(AdCPBaseModel): description="AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').", pattern='^[1-9]\\d*\\.(0|[1-9]\\d*)$', ), + NoExplicitNull, ] = None composition_model: Annotated[ CompositionModel | None, Field( description='Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).' ), + NoExplicitNull, ] = None provenance_required: Annotated[ StrictBool | None, Field( description='When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.' ), + NoExplicitNull, ] = None platform_extensions: Annotated[ list[platform_extension_ref.PlatformExtensionReference] | None, Field( description='Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: "platform_extension_field", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.' ), + NoExplicitNull, ] = None synthesis_nondeterministic: Annotated[ StrictBool | None, Field( description="When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error." ), + NoExplicitNull, ] = False slots: Annotated[ list[Slot] | None, Field( description="Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry." ), + NoExplicitNull, ] = None required_connections: Annotated[ list[downstream_connection_requirement.DownstreamConnectionRequirement] | None, Field( description='Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.' ), + NoExplicitNull, ] = None reference_mutability: Annotated[ ReferenceMutability | None, Field( description='Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.' ), + NoExplicitNull, ] = None production_window_business_days: Annotated[ SchemaInt | None, @@ -221,4 +247,5 @@ class CanonicalFormatBase(AdCPBaseModel): description='Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).', ge=0, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/agent_placement.py b/src/adcp/types/domains/formats/canonical/agent_placement.py index 3caf1718c..5526c68f7 100644 --- a/src/adcp/types/domains/formats/canonical/agent_placement.py +++ b/src/adcp/types/domains/formats/canonical/agent_placement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/agent_placement.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ._base import CanonicalFormatBase @@ -49,6 +49,7 @@ class CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement(CanonicalFormatBa Field( description='How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text.' ), + NoExplicitNull, ] = None max_mention_length_chars: Annotated[ SchemaInt | None, @@ -56,6 +57,7 @@ class CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement(CanonicalFormatBa description='For text output: maximum length of the surface-composed mention text.', ge=1, ), + NoExplicitNull, ] = None max_mention_duration_ms: Annotated[ SchemaInt | None, @@ -63,28 +65,33 @@ class CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement(CanonicalFormatBa description='For audio output: maximum duration of the spoken mention in milliseconds.', ge=1, ), + NoExplicitNull, ] = None supports_offering_reference: Annotated[ StrictBool | None, Field( description='Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context.' ), + NoExplicitNull, ] = None supports_landing_page_url: Annotated[ StrictBool | None, Field( description='Whether the surface attaches a landing page URL to the mention (citation, learn-more link).' ), + NoExplicitNull, ] = None tone_constraints: Annotated[ list[str] | None, Field( description="**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement — adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." ), + NoExplicitNull, ] = None disclosure_required: Annotated[ StrictBool | None, Field( description='Whether the surface must include an explicit sponsorship disclosure label.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/audio_daast.py b/src/adcp/types/domains/formats/canonical/audio_daast.py index 992d7d5e3..ee6adb1b5 100644 --- a/src/adcp/types/domains/formats/canonical/audio_daast.py +++ b/src/adcp/types/domains/formats/canonical/audio_daast.py @@ -1,14 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/audio_daast.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt - from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt +from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictBool from ...core import daast_tracker_constraints @@ -40,12 +39,14 @@ class CanonicalFormatDaastAudio(CanonicalFormatBase): deprecated=True, description='Deprecated one-element alias for `daast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None daast_versions: Annotated[ daast_tracker_constraints.DaastVersions | None, Field( description="Accepted DAAST versions for this format option. A tracker execution selector's daast_versions must be a nonempty subset of this set." ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRangeItem] | None, @@ -54,6 +55,7 @@ class CanonicalFormatDaastAudio(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -61,8 +63,9 @@ class CanonicalFormatDaastAudio(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None - linear_required: StrictBool | None = None - max_wrapper_depth: Annotated[SchemaInt | None, Field(ge=0)] = None - ssl_required: StrictBool | None = None - companion_image_required: StrictBool | None = None + linear_required: Annotated[StrictBool | None, NoExplicitNull] = None + max_wrapper_depth: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None + companion_image_required: Annotated[StrictBool | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/formats/canonical/audio_hosted.py b/src/adcp/types/domains/formats/canonical/audio_hosted.py index bc9bc1480..29739932a 100644 --- a/src/adcp/types/domains/formats/canonical/audio_hosted.py +++ b/src/adcp/types/domains/formats/canonical/audio_hosted.py @@ -1,15 +1,14 @@ # generated by datamodel-codegen: # filename: formats/canonical/audio_hosted.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt - from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt +from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ._base import CanonicalFormatBase @@ -74,6 +73,7 @@ class CanonicalFormatHostedAudio(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -81,45 +81,55 @@ class CanonicalFormatHostedAudio(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None - audio_codecs: list[AudioCodec] | None = None - audio_sample_rates: list[AudioSampleRate] | None = None - audio_channels: list[AudioChannel] | None = None - min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None - max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None + audio_codecs: Annotated[list[AudioCodec] | None, NoExplicitNull] = None + audio_sample_rates: Annotated[list[AudioSampleRate] | None, NoExplicitNull] = None + audio_channels: Annotated[list[AudioChannel] | None, NoExplicitNull] = None + min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_file_size_mb: Annotated[ StrictFloat | None, Field( description='Maximum hosted audio file size in decimal megabytes. Agents that proxy or cache media SHOULD advertise their effective transport ceiling here.', gt=0.0, ), + NoExplicitNull, ] = None loudness_lufs: Annotated[ StrictFloat | None, Field( description='Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values.' ), + NoExplicitNull, ] = None loudness_tolerance_db: Annotated[ StrictFloat | None, Field(description='Permitted deviation from loudness_lufs in dB.', ge=0.0), + NoExplicitNull, ] = None true_peak_dbfs: Annotated[ - StrictFloat | None, Field(description='Maximum true-peak level in dBFS (typical: -2).') + StrictFloat | None, + Field(description='Maximum true-peak level in dBFS (typical: -2).'), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource | None, Field( description="Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. `publisher_owned_reference` is valid only when the product accepts a reference asset whose publisher-owned source resolves to playable audio. `publisher_host_recorded` remains the normal audio-specific host-read value." ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description="Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly — they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted - companion_image_required: StrictBool | None = None - companion_image_aspect_ratio: str | None = None - companion_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None + companion_image_required: Annotated[StrictBool | None, NoExplicitNull] = None + companion_image_aspect_ratio: Annotated[str | None, NoExplicitNull] = None + companion_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = ( + None + ) + brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None diff --git a/src/adcp/types/domains/formats/canonical/audio_vast.py b/src/adcp/types/domains/formats/canonical/audio_vast.py index 841bbe664..ae34e0228 100644 --- a/src/adcp/types/domains/formats/canonical/audio_vast.py +++ b/src/adcp/types/domains/formats/canonical/audio_vast.py @@ -1,14 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/audio_vast.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt - from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt +from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictBool from ...core import vast_media_file_requirements @@ -40,6 +39,7 @@ class CanonicalFormatVastAudio(CanonicalFormatBase): description='VAST audio versions accepted by this product format option. Standardized audio support begins at VAST 4.1; listing 2.0, 3.0, or 4.0 explicitly declares seller-supported legacy audio interoperability rather than standards-conformant VAST audio. The asset declares exactly one vast_version; compatibility requires membership in this set and the seller-wide execution set.', min_length=1, ), + NoExplicitNull, ] = None vast_version: Annotated[ vast_version_1.VastVersion | None, @@ -47,12 +47,14 @@ class CanonicalFormatVastAudio(CanonicalFormatBase): deprecated=True, description='Deprecated one-element alias for vast_versions. Producers use either the singular legacy alias or the plural 3.2 field, never both. VAST 4.1+ is the standards-conformant audio profile; older values declare legacy audio interoperability.', ), + NoExplicitNull, ] = None media_file_requirements: Annotated[ vast_media_file_requirements.VastMediafileRequirements | None, Field( description='Technical acceptance constraints for alternative audio MediaFile renditions in each resolved InLine linear creative. Declared MIME types must be audio/*; visual-dimension constraints are invalid because VAST represents audio MediaFiles with width and height 0.' ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRangeItem] | None, @@ -61,6 +63,7 @@ class CanonicalFormatVastAudio(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -68,16 +71,21 @@ class CanonicalFormatVastAudio(CanonicalFormatBase): description='When set, the resolved audio creative duration must equal this value. Takes precedence over duration_ms_range.', ge=1, ), + NoExplicitNull, ] = None skippable_after_ms: Annotated[ SchemaInt | None, Field(description='When skippable, the buyer-side skip threshold in milliseconds.', ge=0), + NoExplicitNull, ] = None max_wrapper_depth: Annotated[ - SchemaInt | None, Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0) + SchemaInt | None, + Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None companion_image_required: Annotated[ StrictBool | None, Field(description='Whether the resolved VAST audio ad must include a companion creative.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/coordinated_placements.py b/src/adcp/types/domains/formats/canonical/coordinated_placements.py index 1cf202bf3..8accf090a 100644 --- a/src/adcp/types/domains/formats/canonical/coordinated_placements.py +++ b/src/adcp/types/domains/formats/canonical/coordinated_placements.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/coordinated_placements.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat, ScalarInt, ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -295,7 +295,7 @@ class Breakpoint(AdCPBaseModel): pattern='^[a-z][a-z0-9_]*$', ), ] - width: Annotated[SchemaInt | None, Field(ge=1)] = None + width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None width_range: Annotated[ list[WidthRangeItem] | None, Field( @@ -303,14 +303,16 @@ class Breakpoint(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None width_mode: Annotated[ WidthMode | None, Field( description='Fluid width classes. `full_bleed`: edge-to-edge viewport width (IAB adhesion, topscroll). `gutter_residual`: the margin remaining beside the page content column (desktop skins). Fluid breakpoints cannot be targeted by `rendered_canvases` supply — semantic validation rejects that pairing.' ), + NoExplicitNull, ] = None - height: Annotated[SchemaInt | None, Field(ge=1)] = None + height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None height_range: Annotated[ list[HeightRangeItem] | None, Field( @@ -318,6 +320,7 @@ class Breakpoint(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None viewport_height_percent: Annotated[ StrictFloat | None, @@ -326,6 +329,7 @@ class Breakpoint(AdCPBaseModel): gt=0.0, le=100.0, ), + NoExplicitNull, ] = None canvas_aspect_ratio: Annotated[ str | None, @@ -333,6 +337,7 @@ class Breakpoint(AdCPBaseModel): description='Aspect ratio the rendered canvas maintains across the size range (IAB flexible units, ratio-bucket responsive). When both fixed `width` and `height` are declared, the ratio MUST be consistent with them.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None @@ -358,12 +363,14 @@ class State(AdCPBaseModel): Field( description='Asset group IDs rendered in this state (components mode). Makes the template executable: given components and bindings, assembly is deterministic. Every value MUST resolve to a declared slot. Omitted means all supplied component slots may render.' ), + NoExplicitNull, ] = None motion: Annotated[ Motion | None, Field( description='Whether the seller-rendered layout animates within this state (attract loops, load animations). Intra-state animation is seller-rendered; buyer canvases stay static images.' ), + NoExplicitNull, ] = Motion.static max_animation_s: Annotated[ StrictFloat | None, @@ -371,6 +378,7 @@ class State(AdCPBaseModel): description='Upper bound on intra-state animation duration. Required when `motion` is `animated`.', gt=0.0, ), + NoExplicitNull, ] = None breakpoints: Annotated[list[Breakpoint], Field(min_length=1)] close_affordance: Annotated[ @@ -455,18 +463,21 @@ class Transitions(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent | None, Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -484,6 +495,7 @@ class Transitions(AdCPBaseModel): duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -492,12 +504,14 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -506,6 +520,7 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -514,12 +529,14 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -547,18 +564,21 @@ class Transitions1(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent | None, Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -576,6 +596,7 @@ class Transitions1(AdCPBaseModel): duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -584,12 +605,14 @@ class Transitions1(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -598,6 +621,7 @@ class Transitions1(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -606,12 +630,14 @@ class Transitions1(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -645,12 +671,14 @@ class Transitions2(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -664,10 +692,12 @@ class Transitions2(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat, @@ -690,6 +720,7 @@ class Transitions2(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -698,12 +729,14 @@ class Transitions2(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -743,12 +776,14 @@ class Transitions3(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ Literal['scroll_linked'], @@ -762,10 +797,12 @@ class Transitions3(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -774,6 +811,7 @@ class Transitions3(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference, @@ -802,6 +840,7 @@ class Transitions3(AdCPBaseModel): Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -835,12 +874,14 @@ class Transitions4(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -854,10 +895,12 @@ class Transitions4(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -866,12 +909,14 @@ class Transitions4(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -880,6 +925,7 @@ class Transitions4(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -888,12 +934,14 @@ class Transitions4(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -921,6 +969,7 @@ class Transitions5(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent, @@ -933,6 +982,7 @@ class Transitions5(AdCPBaseModel): Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -946,10 +996,12 @@ class Transitions5(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -958,12 +1010,14 @@ class Transitions5(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -972,6 +1026,7 @@ class Transitions5(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -980,12 +1035,14 @@ class Transitions5(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -1039,9 +1096,9 @@ class SharedSlot(AdCPBaseModel): ) asset_group_id: Annotated[str, Field(pattern='^[a-z0-9_]+$')] asset_type: AssetType - required: StrictBool | None = False - min: Annotated[SchemaInt | None, Field(ge=0)] = None - max: Annotated[SchemaInt | None, Field(ge=1)] = None + required: Annotated[StrictBool | None, NoExplicitNull] = False + min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + max: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None consumed_by: Annotated[ list[str], Field( @@ -1075,18 +1132,21 @@ class Components(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference, @@ -1094,15 +1154,15 @@ class Components(AdCPBaseModel): description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), ] - format_kind: str | None = None - params: dict[str, Any] | None = None + format_kind: Annotated[str | None, NoExplicitNull] = None + params: Annotated[dict[str, Any] | None, NoExplicitNull] = None class Params(CanonicalFormatBase): model_config = ConfigDict( extra='allow', ) - motion_level: MotionLevel | None = None + motion_level: Annotated[MotionLevel | None, NoExplicitNull] = None slots: Annotated[ Any | None, Field( @@ -1122,6 +1182,7 @@ class Params(CanonicalFormatBase): description="Logical render width in pixels — use for fixed-size slots (e.g., a 300×250 IAB MREC). When `pixel_ratios` is absent, the required image asset width is the same value (1x). When `pixel_ratios` is present, an accepted asset's intrinsic width is `width × pixel_ratio`. For multi-size flexible slots, use `sizes[]`; for responsive slots, use the min/max fields. The three size modes are mutually exclusive.", ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -1129,6 +1190,7 @@ class Params(CanonicalFormatBase): description='Logical render height in pixels. Intrinsic asset height is `height × pixel_ratio`, where the ratio defaults to 1 when `pixel_ratios` is absent. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -1136,6 +1198,7 @@ class Params(CanonicalFormatBase): description='List of accepted logical (width, height) render pairs for a multi-size flexible slot. The buyer ships an asset matching one logical size multiplied by one accepted `pixel_ratios` entry (or by 1 when `pixel_ratios` is absent). SDKs MUST treat size and density as separate axes: a 600×500 intrinsic asset at 2x satisfies a logical 300×250 size; it does not create a logical 600×500 placement. Mirrors OpenRTB `banner.format[]` semantics. Mutually exclusive with `(width, height)` and with responsive ranges.', min_length=1, ), + NoExplicitNull, ] = None pixel_ratios: Annotated[ list[PixelRatio] | None, @@ -1143,6 +1206,7 @@ class Params(CanonicalFormatBase): description='Accepted intrinsic-pixel densities for image assets, expressed as intrinsic pixels per logical render pixel (for example `[1, 2]` accepts both standard and Retina renditions). Absence means `[1]` for backward compatibility. This is an acceptance set, not a requirement to submit every rendition: one `image_main` asset satisfying any listed ratio is sufficient unless the effective `image_main` slot declares `required_pixel_ratios`. SDKs determine the effective ratio from `asset.pixel_ratio` when supplied, otherwise infer it from intrinsic asset dimensions divided by the matched logical size. Width and height ratios MUST agree.', min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -1150,6 +1214,7 @@ class Params(CanonicalFormatBase): description="Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`.", ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -1157,6 +1222,7 @@ class Params(CanonicalFormatBase): description='Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -1164,6 +1230,7 @@ class Params(CanonicalFormatBase): description='Minimum accepted height in pixels for responsive slots. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -1171,6 +1238,7 @@ class Params(CanonicalFormatBase): description='Maximum accepted height in pixels for responsive slots. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -1178,48 +1246,55 @@ class Params(CanonicalFormatBase): description="Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio.", pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None max_file_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum file size in kilobytes.', ge=1) + SchemaInt | None, Field(description='Maximum file size in kilobytes.', ge=1), NoExplicitNull ] = None image_formats: Annotated[ - list[ImageFormat] | None, Field(description='Permitted image file formats.') + list[ImageFormat] | None, Field(description='Permitted image file formats.'), NoExplicitNull ] = None ssl_required: Annotated[ StrictBool | None, Field(description='Whether the image and its trackers must be served over HTTPS.'), + NoExplicitNull, ] = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - body_text_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + body_text_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None cta_values: Annotated[ list[str] | None, Field( description="Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." ), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource | None, Field( description="Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` — replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class). `publisher_owned_reference`: buyer references an existing post or publisher-owned object via a `published_post` slot; the seller resolves and serves the referenced content after authorization/review rather than receiving uploaded bytes.\n\nNot every value is meaningful on every canonical — `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. `publisher_owned_reference` is meaningful only when the product's `slots` declaration accepts a reference asset such as `published_post`. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description="Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly — they must use build_creative (or sync_creatives with brief inputs or reference assets) so the seller produces or resolves the asset. Combined with `asset_source`, lets a product declare 'I produce assets from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`) or 'I accept existing post references, not uploaded bytes' (asset_source=`publisher_owned_reference`, buyer_asset_acceptance=`rejected`)." ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `image` only `pause` and `screensaver` are valid — the image-plus-copy contract the major pause-ad sellers ingest (seller composites the frame; typical canvas 1920×1080 or a transparent-region overlay). Other experiences route per the matrix in docs/creative/ctv-experiences.mdx.' ), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers (e.g. `qr_code` on a pause frame). Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None @@ -1247,24 +1322,28 @@ class Components1(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['image'] = 'image' params: Annotated[ @@ -1296,6 +1375,7 @@ class Params2(CanonicalFormatBase): description='Required banner width in pixels — use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -1303,6 +1383,7 @@ class Params2(CanonicalFormatBase): description='Required banner height in pixels. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -1310,6 +1391,7 @@ class Params2(CanonicalFormatBase): description='List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300×250 OR 728×90 OR 970×250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges.', min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -1317,6 +1399,7 @@ class Params2(CanonicalFormatBase): description='Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -1324,6 +1407,7 @@ class Params2(CanonicalFormatBase): description='Maximum accepted width for responsive HTML5 banners. Pair with `min_width`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -1331,6 +1415,7 @@ class Params2(CanonicalFormatBase): description='Minimum accepted height for responsive HTML5 banners. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -1338,6 +1423,7 @@ class Params2(CanonicalFormatBase): description='Maximum accepted height for responsive HTML5 banners. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None max_initial_load_kb: Annotated[ SchemaInt | None, @@ -1345,6 +1431,7 @@ class Params2(CanonicalFormatBase): description='Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile.', ge=1, ), + NoExplicitNull, ] = None max_polite_load_kb: Annotated[ SchemaInt | None, @@ -1352,12 +1439,14 @@ class Params2(CanonicalFormatBase): description='Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes.', ge=1, ), + NoExplicitNull, ] = None host_initiated_subload: Annotated[ StrictBool | None, Field( description='Whether the host page must initiate the polite-load phase. IAB-compliant banners require true.' ), + NoExplicitNull, ] = None max_animation_duration_ms: Annotated[ SchemaInt | None, @@ -1365,36 +1454,46 @@ class Params2(CanonicalFormatBase): description='Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds).', ge=0, ), + NoExplicitNull, ] = None max_cpu_load_percent: Annotated[ SchemaInt | None, Field(description='Maximum CPU load percentage during render.', ge=1, le=100), + NoExplicitNull, ] = None mraid_required: Annotated[ StrictBool | None, Field(description='Whether MRAID compatibility is required (mobile in-app).'), + NoExplicitNull, ] = None mraid_version: Annotated[ MraidVersion | None, Field(description='Required MRAID version when mraid_required is true.'), + NoExplicitNull, ] = None om_sdk_required: Annotated[ StrictBool | None, Field(description='Whether IAB Open Measurement SDK integration is required.'), + NoExplicitNull, ] = None clicktag_macro: Annotated[ - ClicktagMacro | None, Field(description='Name of the click-tag macro the bundle must use.') + ClicktagMacro | None, + Field(description='Name of the click-tag macro the bundle must use.'), + NoExplicitNull, ] = None backup_image_required: Annotated[ StrictBool | None, Field( description='Whether a backup image must accompany the zip for non-HTML5 environments.' ), + NoExplicitNull, ] = None backup_image_max_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum backup image file size in kilobytes.', ge=1) + SchemaInt | None, + Field(description='Maximum backup image file size in kilobytes.', ge=1), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None class Components2(AdCPBaseModel): @@ -1421,24 +1520,28 @@ class Components2(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['html5'] = 'html5' params: Annotated[ @@ -1469,6 +1572,7 @@ class Params3(CanonicalFormatBase): description='Required tag rendering width in pixels — use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -1476,6 +1580,7 @@ class Params3(CanonicalFormatBase): description='Required tag rendering height in pixels. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -1483,6 +1588,7 @@ class Params3(CanonicalFormatBase): description="List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges.", min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -1490,6 +1596,7 @@ class Params3(CanonicalFormatBase): description='Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -1497,6 +1604,7 @@ class Params3(CanonicalFormatBase): description='Maximum accepted width for responsive third-party tags. Pair with `min_width`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -1504,6 +1612,7 @@ class Params3(CanonicalFormatBase): description='Minimum accepted height for responsive third-party tags. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -1511,6 +1620,7 @@ class Params3(CanonicalFormatBase): description='Maximum accepted height for responsive third-party tags. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None supported_tag_types: Annotated[ list[SupportedTagType] | None, @@ -1518,6 +1628,7 @@ class Params3(CanonicalFormatBase): deprecated=True, description='Deprecated ambiguous mechanism list. Use `supported_delivery_types`; markup subtype lives on the `display_tag` asset.', ), + NoExplicitNull, ] = None supported_delivery_types: Annotated[ list[SupportedDeliveryType] | None, @@ -1525,29 +1636,35 @@ class Params3(CanonicalFormatBase): description='Closed set of delivery types this format option can traffic. `paired_redirect` means one atomic ad-request/click-through pair (Internal Redirect semantics), never independently matchable URL slots.', min_length=1, ), + NoExplicitNull, ] = None ssl_required: Annotated[ - StrictBool | None, Field(description='Whether the tag URL must be HTTPS.') + StrictBool | None, Field(description='Whether the tag URL must be HTTPS.'), NoExplicitNull ] = None max_redirect_depth: Annotated[ - SchemaInt | None, Field(description='Maximum redirect chain depth permitted.', ge=0) + SchemaInt | None, + Field(description='Maximum redirect chain depth permitted.', ge=0), + NoExplicitNull, ] = None max_response_time_ms: Annotated[ SchemaInt | None, Field(description='Maximum tag-server response time in milliseconds.', ge=1), + NoExplicitNull, ] = None backup_image_required: Annotated[ StrictBool | None, Field( description='Whether a backup image must accompany the tag for environments that cannot render the third-party tag.' ), + NoExplicitNull, ] = None - backup_image_max_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + backup_image_max_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None om_sdk_required: Annotated[ StrictBool | None, Field( description="Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." ), + NoExplicitNull, ] = None @@ -1575,24 +1692,28 @@ class Components3(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['display_tag'] = 'display_tag' params: Annotated[ @@ -1630,19 +1751,24 @@ class Params4(CanonicalFormatBase): description="Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5').", pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None min_cards: Annotated[ - SchemaInt | None, Field(description='Minimum card count (typical: 2 or 3).', ge=2) + SchemaInt | None, + Field(description='Minimum card count (typical: 2 or 3).', ge=2), + NoExplicitNull, ] = None max_cards: Annotated[ SchemaInt | None, Field(description='Maximum card count (typical: 6, 10, or 35 depending on platform).'), + NoExplicitNull, ] = None allowed_card_media_asset_types: Annotated[ list[AllowedCardMediaAssetType] | None, Field( description='Asset types each card\'s `media` field may carry. Default: [\'image\']. Polymorphic carousels (Meta) allow [\'image\', \'video\']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card\'s media payload, not the card-asset itself (which is always asset_type: "card").' ), + NoExplicitNull, ] = None allowed_card_asset_types: Annotated[ list[AllowedCardMediaAssetType] | None, @@ -1650,13 +1776,15 @@ class Params4(CanonicalFormatBase): deprecated=True, description='DEPRECATED — alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0.', ), + NoExplicitNull, ] = None - card_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - card_video_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - card_video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None + card_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + card_video_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + card_video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None primary_text_max_chars: Annotated[ SchemaInt | None, Field(description='Maximum length of the carousel-level primary text.', ge=1), + NoExplicitNull, ] = None card_headline_max_chars: Annotated[ SchemaInt | None, @@ -1664,6 +1792,7 @@ class Params4(CanonicalFormatBase): description='Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot.', ge=1, ), + NoExplicitNull, ] = None card_description_max_chars: Annotated[ SchemaInt | None, @@ -1671,8 +1800,9 @@ class Params4(CanonicalFormatBase): description='Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars).', ge=1, ), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None class Components4(AdCPBaseModel): @@ -1699,24 +1829,28 @@ class Components4(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['image_carousel'] = 'image_carousel' params: Annotated[ @@ -1751,6 +1885,7 @@ class Params5(CanonicalFormatBase): Field( description='Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed).' ), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -1758,11 +1893,12 @@ class Params5(CanonicalFormatBase): description='Aspect ratio. Inferred from orientation if omitted.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None - min_width: Annotated[SchemaInt | None, Field(ge=1)] = None - min_height: Annotated[SchemaInt | None, Field(ge=1)] = None - max_width: Annotated[SchemaInt | None, Field(ge=1)] = None - max_height: Annotated[SchemaInt | None, Field(ge=1)] = None + min_width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + min_height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None duration_ms_range: Annotated[ list[DurationMsRange | None] | None, Field( @@ -1770,6 +1906,7 @@ class Params5(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -1777,45 +1914,51 @@ class Params5(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description).', ge=1, ), + NoExplicitNull, ] = None - video_codecs: list[VideoCodec] | None = None - audio_codecs: list[AudioCodec] | None = None - containers: list[Container] | None = None - min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None - max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None + video_codecs: Annotated[list[VideoCodec] | None, NoExplicitNull] = None + audio_codecs: Annotated[list[AudioCodec] | None, NoExplicitNull] = None + containers: Annotated[list[Container] | None, NoExplicitNull] = None + min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_file_size_mb: Annotated[ SchemaInt | None, Field(description='Maximum file size, where 1 MB is exactly 1,000,000 bytes.', ge=1), - ] = None - frame_rates: list[StrictFloat] | None = None - captions: Captions | None = None - om_sdk_required: StrictBool | None = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - primary_text_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - cta_values: list[str] | None = None + NoExplicitNull, + ] = None + frame_rates: Annotated[list[StrictFloat] | None, NoExplicitNull] = None + captions: Annotated[Captions | None, NoExplicitNull] = None + om_sdk_required: Annotated[StrictBool | None, NoExplicitNull] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + primary_text_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + cta_values: Annotated[list[str] | None, NoExplicitNull] = None companion_banner_widths: Annotated[ list[CompanionBannerWidth] | None, Field(description='Permitted companion banner widths (instream video).'), + NoExplicitNull, ] = None - companion_banner_heights: list[CompanionBannerHeight] | None = None + companion_banner_heights: Annotated[list[CompanionBannerHeight] | None, NoExplicitNull] = None asset_source: Annotated[ AssetSource | None, Field( description='Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video. `publisher_owned_reference` is valid when the product accepts an existing post reference via a `published_post` slot instead of uploaded video bytes. Adopters MUST select a value appropriate to the canonical.' ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description='Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly — they must use build_creative, sync_creatives with brief inputs, or sync_creatives with an accepted reference asset so the seller produces or resolves the video.' ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `video_hosted` only `screensaver` is valid (ambient looping video the platform plays on idle). Other experiences route per the matrix in docs/creative/ctv-experiences.mdx; linear CTV video declares no experience.' ), + NoExplicitNull, ] = None @@ -1843,24 +1986,28 @@ class Components5(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['video_hosted'] = 'video_hosted' params: Annotated[ @@ -1885,9 +2032,9 @@ class Params6(CanonicalFormatBase): {'asset_group_id': 'vast_tag', 'asset_type': 'vast', 'required': True}, {'asset_group_id': 'landing_page_url', 'asset_type': 'url', 'required': False}, ] - orientation: Orientation | None = None + orientation: Annotated[Orientation | None, NoExplicitNull] = None aspect_ratio: Annotated[ - str | None, Field(pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$') + str | None, Field(pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$'), NoExplicitNull ] = None vast_versions: Annotated[ list[vast_version_1.VastVersion] | None, @@ -1895,6 +2042,7 @@ class Params6(CanonicalFormatBase): description='VAST versions accepted by this product format option. The asset still declares exactly one `vast_version`; compatibility requires membership in this set and the seller-wide execution set.', min_length=1, ), + NoExplicitNull, ] = None vast_version: Annotated[ vast_version_1.VastVersion | None, @@ -1902,25 +2050,29 @@ class Params6(CanonicalFormatBase): deprecated=True, description='Deprecated one-element alias for `vast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None media_file_requirements: Annotated[ vast_media_file_requirements.VastMediafileRequirements | None, Field( description='Technical acceptance constraints for alternative VAST MediaFile renditions. Each applicable resolved InLine linear creative needs at least one MediaFile satisfying all declared constraints.' ), + NoExplicitNull, ] = None vpaid_enabled: Annotated[ StrictBool | None, Field( description='Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads.' ), + NoExplicitNull, ] = None - vpaid_version: VpaidVersion | None = None + vpaid_version: Annotated[VpaidVersion | None, NoExplicitNull] = None simid_supported: Annotated[ StrictBool | None, Field( description='Whether the seller accepts IAB SIMID through `` on a Linear VAST creative. SIMID is not a generic VAST extension and cannot be serialized under NonLinearAds; every `ctv_ad_experience` profile therefore forbids `true`.' ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRangeItem] | None, @@ -1929,6 +2081,7 @@ class Params6(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -1936,6 +2089,7 @@ class Params6(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -1943,6 +2097,7 @@ class Params6(CanonicalFormatBase): description='Minimum placement/player width in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -1950,6 +2105,7 @@ class Params6(CanonicalFormatBase): description='Maximum placement/player width in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -1957,6 +2113,7 @@ class Params6(CanonicalFormatBase): description='Minimum placement/player height in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -1964,34 +2121,40 @@ class Params6(CanonicalFormatBase): description='Maximum placement/player height in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None creative_type: Annotated[ CreativeType | None, Field( description='Required VAST creative class: `linear` (in-stream Linear), `nonlinear` (NonLinearAds overlay-class), or `either`. Supersedes `linear_required`; when both are present `creative_type` wins, and validators treat `linear_required: true` with no `creative_type` as `linear`.' ), + NoExplicitNull, ] = None ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option is eligible to serve. On video_vast only `pause`, `screensaver`, `overlay`, `squeezeback`, and `in_scene` are valid (`menu` routes to native_in_feed or sponsored_placement), and `creative_type` MUST be `nonlinear`. Because VAST places `` only under Linear ``, `simid_supported` MUST NOT be true on any of these NonLinear profiles. Per-experience floors: `overlay` and `squeezeback` require a 10s minimum duration; `in_scene` requires a 3s minimum brand-exposure duration and forbids interactivity (`vpaid_enabled` MUST NOT be true); `pause` has no duration floor and ends on viewer or device action.' ), + NoExplicitNull, ] = None motion_level: Annotated[ motion_level_1.CreativeMotionLevel | None, Field(description='Accepted motion class for the rendered creative (AdCOM attrs 21-23).'), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers. Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None linear_required: Annotated[ StrictBool | None, Field( description='Whether the VAST creative must be linear (non-skippable in-stream). Superseded by `creative_type`; retained for pre-3.2 declarations.' ), + NoExplicitNull, ] = None skippable_after_ms: Annotated[ SchemaInt | None, @@ -1999,11 +2162,14 @@ class Params6(CanonicalFormatBase): description='When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll).', ge=0, ), + NoExplicitNull, ] = None max_wrapper_depth: Annotated[ - SchemaInt | None, Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0) + SchemaInt | None, + Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None class Components6(AdCPBaseModel): @@ -2030,24 +2196,28 @@ class Components6(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['video_vast'] = 'video_vast' params: Annotated[ @@ -2081,6 +2251,7 @@ class Params7(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -2088,48 +2259,58 @@ class Params7(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None - audio_codecs: list[AudioCodec2] | None = None - audio_sample_rates: list[AudioSampleRate] | None = None - audio_channels: list[AudioChannel] | None = None - min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None - max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None + audio_codecs: Annotated[list[AudioCodec2] | None, NoExplicitNull] = None + audio_sample_rates: Annotated[list[AudioSampleRate] | None, NoExplicitNull] = None + audio_channels: Annotated[list[AudioChannel] | None, NoExplicitNull] = None + min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_file_size_mb: Annotated[ StrictFloat | None, Field( description='Maximum hosted audio file size in decimal megabytes. Agents that proxy or cache media SHOULD advertise their effective transport ceiling here.', gt=0.0, ), + NoExplicitNull, ] = None loudness_lufs: Annotated[ StrictFloat | None, Field( description='Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values.' ), + NoExplicitNull, ] = None loudness_tolerance_db: Annotated[ StrictFloat | None, Field(description='Permitted deviation from loudness_lufs in dB.', ge=0.0), + NoExplicitNull, ] = None true_peak_dbfs: Annotated[ - StrictFloat | None, Field(description='Maximum true-peak level in dBFS (typical: -2).') + StrictFloat | None, + Field(description='Maximum true-peak level in dBFS (typical: -2).'), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource | None, Field( description="Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. `publisher_owned_reference` is valid only when the product accepts a reference asset whose publisher-owned source resolves to playable audio. `publisher_host_recorded` remains the normal audio-specific host-read value." ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description="Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly — they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted - companion_image_required: StrictBool | None = None - companion_image_aspect_ratio: str | None = None - companion_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None + companion_image_required: Annotated[StrictBool | None, NoExplicitNull] = None + companion_image_aspect_ratio: Annotated[str | None, NoExplicitNull] = None + companion_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = ( + None + ) + brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None class Components7(AdCPBaseModel): @@ -2156,24 +2337,28 @@ class Components7(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_hosted'] = 'audio_hosted' params: Annotated[ @@ -2209,46 +2394,57 @@ class Params9(CanonicalFormatBase): supported_catalog_types: Annotated[ list[catalog_type.CatalogType] | None, Field(description='Catalog types this product accepts.'), + NoExplicitNull, ] = None min_items: Annotated[ - SchemaInt | None, Field(description='Minimum catalog item count buyer must supply.', ge=1) + SchemaInt | None, + Field(description='Minimum catalog item count buyer must supply.', ge=1), + NoExplicitNull, ] = None max_items: Annotated[ - SchemaInt | None, Field(description='Maximum items considered for placement.') + SchemaInt | None, + Field(description='Maximum items considered for placement.'), + NoExplicitNull, ] = None fanout_mode: Annotated[ FanoutMode | None, Field( description='How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item.' ), + NoExplicitNull, ] = None required_catalog_fields: Annotated[ list[str] | None, Field( description="Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." ), + NoExplicitNull, ] = None supported_id_types: Annotated[ list[SupportedIdType] | None, Field(description='Catalog identifier types the placement renders against.'), + NoExplicitNull, ] = None hero_asset_supported: Annotated[ StrictBool | None, Field( description='Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern).' ), + NoExplicitNull, ] = None item_production_model: Annotated[ ItemProductionModel | None, Field( description='How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset — drops `publisher_host_recorded` because it\'s audio-specific and doesn\'t apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer\'s catalog already contains rendered assets per item; the seller composes the placement using those assets. ("Uploaded" reads slightly off for catalog-keyed items where the buyer didn\'t actively upload bytes — the catalog ingestion already supplied them — but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller\'s design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief × N catalog items → N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering.' ), + NoExplicitNull, ] = ItemProductionModel.buyer_uploaded ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `sponsored_placement`: `menu` (sponsored app/content tiles and rows whose assets derive from a catalog listing — Fire-TV-tile pattern), `squeezeback`, and `in_scene` (seller-composited brand integrations produced from the catalog/brief rather than a buyer wire creative). Asset-bundle menu heroes route to `native_in_feed`.' ), + NoExplicitNull, ] = None @@ -2276,24 +2472,28 @@ class Components9(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['sponsored_placement'] = 'sponsored_placement' params: Annotated[ @@ -2348,28 +2548,33 @@ class Params10(CanonicalFormatBase): Field( description='CTV experience this option serves. On native_in_feed `menu` and `overlay` are valid. `menu`: smart-TV home/menu surfaces where the platform assembles buyer assets (background/main image, logo/icon, copy, optional focus-triggered video); `menu_placement` selects the tile vs headline-banner variant, and catalog-derived sponsored tiles route to `sponsored_placement` instead. `overlay`: seller-composited in-stream overlays supplied as an asset bundle (video, logo, imagery, copy, activation copy) — the contract overlay sellers that do not ingest VAST tags use; VAST-ingesting sellers publish a `video_vast` sibling option instead. The wire name stays `native_in_feed` for 3.x even though neither surface is literally in-feed.' ), + NoExplicitNull, ] = None menu_placement: Annotated[ MenuPlacement | None, Field( description='Menu surface variant, mapping to OpenRTB Native `plcmttype` 1 (tile/feed) and 3 (headline banner). Valid only with `ctv_ad_experience: "menu"`.' ), + NoExplicitNull, ] = None focus_behavior: Annotated[ FocusBehavior | None, Field( description='What happens when the viewer\'s remote focus lands on the unit. `autoplay_*` requires a `video` asset; playback method maps to AdCOM playbackmethod on OpenRTB bridges. Valid only with `ctv_ad_experience: "menu"`.' ), + NoExplicitNull, ] = None motion_level: Annotated[ motion_level_1.CreativeMotionLevel | None, Field(description='Accepted motion class for the rendered unit (AdCOM attrs 21-23).'), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers (QR, deep link, send-to-device). Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None title_max_chars: Annotated[ SchemaInt | None, @@ -2377,6 +2582,7 @@ class Params10(CanonicalFormatBase): description='Maximum character length for the title slot. IAB native typical: 25 (short) to 90 (long). Buyer agents SHOULD validate ship-time title length against this.', ge=1, ), + NoExplicitNull, ] = None body_text_max_chars: Annotated[ SchemaInt | None, @@ -2384,16 +2590,19 @@ class Params10(CanonicalFormatBase): description='Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended).', ge=1, ), + NoExplicitNull, ] = None cta_max_chars: Annotated[ SchemaInt | None, Field(description='Maximum character length for the cta slot. Typical: 15–25.', ge=1), + NoExplicitNull, ] = None cta_values: Annotated[ list[str] | None, Field( description="Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." ), + NoExplicitNull, ] = None main_image_sizes: Annotated[ list[MainImageSize] | None, @@ -2401,37 +2610,45 @@ class Params10(CanonicalFormatBase): description='Accepted logical (width, height) pairs for the main_image slot. Common IAB native sizes: 1200×627 (1.91:1), 1080×1080 (1:1), 1080×1350 (4:5). When the effective main_image slot declares `pixel_ratios`, intrinsic asset dimensions are the matched logical pair multiplied by the selected ratio; absence remains 1x.', min_length=1, ), + NoExplicitNull, ] = None icon_size: Annotated[ IconSize | None, Field( description='Required logical (width, height) for the icon slot when present (typical: 80×80 or 100×100). When the effective icon slot declares `pixel_ratios`, intrinsic dimensions are multiplied by the selected ratio; absence remains 1x.' ), + NoExplicitNull, ] = None max_image_file_size_kb: Annotated[ SchemaInt | None, Field(description='Maximum file size in kilobytes for main_image and icon.', ge=1), + NoExplicitNull, ] = None image_formats: Annotated[ - list[ImageFormat1] | None, Field(description='Permitted image file formats.') + list[ImageFormat1] | None, + Field(description='Permitted image file formats.'), + NoExplicitNull, ] = None ssl_required: Annotated[ StrictBool | None, Field( description='Whether trackers, landing pages, and image URLs must be served over HTTPS.' ), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource5 | None, Field( description="Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output. `publisher_owned_reference`: buyer ships an existing published post reference; the seller resolves the post into the native presentation after authorization/review." ), + NoExplicitNull, ] = AssetSource5.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description='Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body — they must use `build_creative`, `sync_creatives` with brief inputs, or an accepted `published_post` reference so the seller produces or resolves the native bundle.' ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted @@ -2459,24 +2676,28 @@ class Components10(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['native_in_feed'] = 'native_in_feed' params: Annotated[ @@ -2570,36 +2791,37 @@ class Params11(CanonicalFormatBase): 'max': 1, }, ] - headlines_min: Annotated[SchemaInt | None, Field(ge=0)] = None - headlines_max: Annotated[SchemaInt | None, Field(ge=0)] = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - long_headlines_min: Annotated[SchemaInt | None, Field(ge=0)] = None - long_headlines_max: Annotated[SchemaInt | None, Field(ge=0)] = None - long_headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - descriptions_min: Annotated[SchemaInt | None, Field(ge=0)] = None - descriptions_max: Annotated[SchemaInt | None, Field(ge=0)] = None - description_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - images_landscape_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_landscape_max: Annotated[SchemaInt | None, Field(ge=0)] = None - images_landscape_aspect_ratio: str | None = None - images_square_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_square_max: Annotated[SchemaInt | None, Field(ge=0)] = None - images_vertical_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_vertical_max: Annotated[SchemaInt | None, Field(ge=0)] = None - videos_min: Annotated[SchemaInt | None, Field(ge=0)] = None - videos_max: Annotated[SchemaInt | None, Field(ge=0)] = None - video_min_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None - video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None - logo_min: Annotated[SchemaInt | None, Field(ge=0)] = None - logo_max: Annotated[SchemaInt | None, Field(ge=0)] = None - logo_aspect_ratios: list[str] | None = None - business_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - asset_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + headlines_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + headlines_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + long_headlines_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + long_headlines_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + long_headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + descriptions_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + descriptions_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + description_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + images_landscape_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_landscape_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_landscape_aspect_ratio: Annotated[str | None, NoExplicitNull] = None + images_square_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_square_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_vertical_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_vertical_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + videos_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + videos_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + video_min_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + logo_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + logo_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + logo_aspect_ratios: Annotated[list[str] | None, NoExplicitNull] = None + business_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + asset_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None supports_catalog_input: Annotated[ StrictBool | None, Field( description='Whether the product can additionally consume a catalog reference (e.g., PMax with product feed).' ), + NoExplicitNull, ] = None @@ -2627,24 +2849,28 @@ class Components11(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['responsive_creative'] = 'responsive_creative' params: Annotated[ @@ -2686,6 +2912,7 @@ class Params12(CanonicalFormatBase): Field( description='How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text.' ), + NoExplicitNull, ] = None max_mention_length_chars: Annotated[ SchemaInt | None, @@ -2693,6 +2920,7 @@ class Params12(CanonicalFormatBase): description='For text output: maximum length of the surface-composed mention text.', ge=1, ), + NoExplicitNull, ] = None max_mention_duration_ms: Annotated[ SchemaInt | None, @@ -2700,30 +2928,35 @@ class Params12(CanonicalFormatBase): description='For audio output: maximum duration of the spoken mention in milliseconds.', ge=1, ), + NoExplicitNull, ] = None supports_offering_reference: Annotated[ StrictBool | None, Field( description='Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context.' ), + NoExplicitNull, ] = None supports_landing_page_url: Annotated[ StrictBool | None, Field( description='Whether the surface attaches a landing page URL to the mention (citation, learn-more link).' ), + NoExplicitNull, ] = None tone_constraints: Annotated[ list[str] | None, Field( description="**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement — adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." ), + NoExplicitNull, ] = None disclosure_required: Annotated[ StrictBool | None, Field( description='Whether the surface must include an explicit sponsorship disclosure label.' ), + NoExplicitNull, ] = None @@ -2751,24 +2984,28 @@ class Components12(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['agent_placement'] = 'agent_placement' params: Annotated[ @@ -2803,6 +3040,7 @@ class Params13(CanonicalFormatBase): Field( description='Which end of the template contract the buyer feeds. `components`: buyer supplies component slots; seller renders states (no `state_canvases`/`layered_source` assets allowed). `rendered_canvases`: buyer supplies exactly one `state_canvases` image per declared state × breakpoint pair. `layered_source`: buyer ships design source (+ optional `font_files`); seller production derives states (`production_window_business_days` applies) — transitional for sellers without executable templates.' ), + NoExplicitNull, ] = SupplyMode.components slots: Annotated[ Any | None, @@ -2842,6 +3080,7 @@ class Params13(CanonicalFormatBase): Field( description='How the unit enters view, distinct from state changes. `clip_window`: canvas fixed and progressively exposed through a scrolling window (interscroller, topscroll). `scroll_parallax`: canvas moves at a different rate than content. Reveal is presentation of one canvas, not a transition; do not fabricate a second state to express it.' ), + NoExplicitNull, ] = Reveal.none transitions: Annotated[ list[Transitions | Transitions1 | Transitions2 | Transitions3 | Transitions4 | Transitions5] @@ -2850,12 +3089,14 @@ class Params13(CanonicalFormatBase): description='Bounded seller-rendered transitions between declared visual states. Required when `states` has more than one entry; MUST be omitted for single-state units. Every non-initial state MUST be reachable from `initial_state_id`. Dismissal is terminal unit behavior declared by `user_controls.dismissible`, not a hidden visual state.', min_length=1, ), + NoExplicitNull, ] = None clickthrough: Annotated[ Clickthrough | None, Field( description='Destination policy. `required` (default): manifest MUST supply `landing_page_url`. `optional`: click-optional units (in-feed brand units) may omit it. `none`: unit is non-clickable; manifests MUST NOT supply `landing_page_url` or `state_click_urls`. Per-state overrides via `state_click_urls` entries carrying `state_id`; `landing_page_url` is the fallback for unlisted states.' ), + NoExplicitNull, ] = Clickthrough.required user_controls: Annotated[ UserControls, @@ -2868,6 +3109,7 @@ class Params13(CanonicalFormatBase): Field( description='Rectangular areas constraining buyer artwork. Omitted state/breakpoint selectors apply the constraint to every canvas. For fluid or range-sized breakpoints, use percent-unit regions.' ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRange | None] | None, @@ -2876,30 +3118,34 @@ class Params13(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None - duration_ms_exact: Annotated[SchemaInt | None, Field(ge=1)] = None + duration_ms_exact: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None aspect_ratio: Annotated[ str | None, Field( description='Embedded-video aspect ratio.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None - containers: list[Container] | None = None - video_playback: VideoPlayback | None = None - max_initial_load_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + containers: Annotated[list[Container] | None, NoExplicitNull] = None + video_playback: Annotated[VideoPlayback | None, NoExplicitNull] = None + max_initial_load_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_subload_kb: Annotated[ SchemaInt | None, Field( description='Ceiling on assets loaded after the window load event (IAB LEAN subload). Pairs with `max_initial_load_kb` to mirror the New Ad Portfolio initial/subload weight pair.', ge=1, ), + NoExplicitNull, ] = None polite_load: Annotated[ StrictBool | None, Field( description="When true, non-initial assets load only after the host page's window load event (IAB LEAN subload boundary)." ), + NoExplicitNull, ] = None @@ -2927,24 +3173,28 @@ class Components13(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' params: Annotated[ @@ -2975,12 +3225,14 @@ class Params8(CanonicalFormatBase): deprecated=True, description='Deprecated one-element alias for `daast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None daast_versions: Annotated[ daast_tracker_constraints.DaastVersions | None, Field( description="Accepted DAAST versions for this format option. A tracker execution selector's daast_versions must be a nonempty subset of this set." ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRangeItem] | None, @@ -2989,6 +3241,7 @@ class Params8(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -2996,11 +3249,12 @@ class Params8(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None - linear_required: StrictBool | None = None - max_wrapper_depth: Annotated[SchemaInt | None, Field(ge=0)] = None - ssl_required: StrictBool | None = None - companion_image_required: StrictBool | None = None + linear_required: Annotated[StrictBool | None, NoExplicitNull] = None + max_wrapper_depth: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None + companion_image_required: Annotated[StrictBool | None, NoExplicitNull] = None class Components8(AdCPBaseModel): @@ -3027,24 +3281,28 @@ class Components8(AdCPBaseModel): description='Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.', ge=1, ), + NoExplicitNull, ] = None serving_policy: Annotated[ ServingPolicy | None, Field( description='Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.' ), + NoExplicitNull, ] = None canvas_constraints: Annotated[ list[canvas_constraint.CanvasConstraint] | None, Field( description='Artwork constraints applied to this component, including safe areas and seller-reserved regions.' ), + NoExplicitNull, ] = None format_option_ref: Annotated[ format_option_ref_1.FormatOptionReference | None, Field( description='Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.' ), + NoExplicitNull, ] = None format_kind: Literal['audio_daast'] = 'audio_daast' params: Annotated[ @@ -3098,4 +3356,5 @@ class CanonicalFormatCoordinatedPlacements(CanonicalFormatBase): Field( description='Manifest slots supplied once and consumed by one or more coordinated components.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/display_tag.py b/src/adcp/types/domains/formats/canonical/display_tag.py index cd4079799..ea9ef9a45 100644 --- a/src/adcp/types/domains/formats/canonical/display_tag.py +++ b/src/adcp/types/domains/formats/canonical/display_tag.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/display_tag.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ._base import CanonicalFormatBase @@ -52,6 +52,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Required tag rendering width in pixels — use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -59,6 +60,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Required tag rendering height in pixels. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -66,6 +68,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description="List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges.", min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -73,6 +76,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -80,6 +84,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Maximum accepted width for responsive third-party tags. Pair with `min_width`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -87,6 +92,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Minimum accepted height for responsive third-party tags. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -94,6 +100,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Maximum accepted height for responsive third-party tags. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None supported_tag_types: Annotated[ list[SupportedTagType] | None, @@ -101,6 +108,7 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): deprecated=True, description='Deprecated ambiguous mechanism list. Use `supported_delivery_types`; markup subtype lives on the `display_tag` asset.', ), + NoExplicitNull, ] = None supported_delivery_types: Annotated[ list[SupportedDeliveryType] | None, @@ -108,27 +116,33 @@ class CanonicalFormatDisplayTag(CanonicalFormatBase): description='Closed set of delivery types this format option can traffic. `paired_redirect` means one atomic ad-request/click-through pair (Internal Redirect semantics), never independently matchable URL slots.', min_length=1, ), + NoExplicitNull, ] = None ssl_required: Annotated[ - StrictBool | None, Field(description='Whether the tag URL must be HTTPS.') + StrictBool | None, Field(description='Whether the tag URL must be HTTPS.'), NoExplicitNull ] = None max_redirect_depth: Annotated[ - SchemaInt | None, Field(description='Maximum redirect chain depth permitted.', ge=0) + SchemaInt | None, + Field(description='Maximum redirect chain depth permitted.', ge=0), + NoExplicitNull, ] = None max_response_time_ms: Annotated[ SchemaInt | None, Field(description='Maximum tag-server response time in milliseconds.', ge=1), + NoExplicitNull, ] = None backup_image_required: Annotated[ StrictBool | None, Field( description='Whether a backup image must accompany the tag for environments that cannot render the third-party tag.' ), + NoExplicitNull, ] = None - backup_image_max_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + backup_image_max_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None om_sdk_required: Annotated[ StrictBool | None, Field( description="Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/html5.py b/src/adcp/types/domains/formats/canonical/html5.py index 055fb170c..dbfb2aa14 100644 --- a/src/adcp/types/domains/formats/canonical/html5.py +++ b/src/adcp/types/domains/formats/canonical/html5.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/html5.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ._base import CanonicalFormatBase @@ -51,6 +51,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Required banner width in pixels — use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set.', ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -58,6 +59,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Required banner height in pixels. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -65,6 +67,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300×250 OR 728×90 OR 970×250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges.', min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -72,6 +75,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -79,6 +83,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Maximum accepted width for responsive HTML5 banners. Pair with `min_width`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -86,6 +91,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Minimum accepted height for responsive HTML5 banners. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -93,6 +99,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Maximum accepted height for responsive HTML5 banners. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None max_initial_load_kb: Annotated[ SchemaInt | None, @@ -100,6 +107,7 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile.', ge=1, ), + NoExplicitNull, ] = None max_polite_load_kb: Annotated[ SchemaInt | None, @@ -107,12 +115,14 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes.', ge=1, ), + NoExplicitNull, ] = None host_initiated_subload: Annotated[ StrictBool | None, Field( description='Whether the host page must initiate the polite-load phase. IAB-compliant banners require true.' ), + NoExplicitNull, ] = None max_animation_duration_ms: Annotated[ SchemaInt | None, @@ -120,33 +130,43 @@ class CanonicalFormatHtml5Banner(CanonicalFormatBase): description='Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds).', ge=0, ), + NoExplicitNull, ] = None max_cpu_load_percent: Annotated[ SchemaInt | None, Field(description='Maximum CPU load percentage during render.', ge=1, le=100), + NoExplicitNull, ] = None mraid_required: Annotated[ StrictBool | None, Field(description='Whether MRAID compatibility is required (mobile in-app).'), + NoExplicitNull, ] = None mraid_version: Annotated[ MraidVersion | None, Field(description='Required MRAID version when mraid_required is true.'), + NoExplicitNull, ] = None om_sdk_required: Annotated[ StrictBool | None, Field(description='Whether IAB Open Measurement SDK integration is required.'), + NoExplicitNull, ] = None clicktag_macro: Annotated[ - ClicktagMacro | None, Field(description='Name of the click-tag macro the bundle must use.') + ClicktagMacro | None, + Field(description='Name of the click-tag macro the bundle must use.'), + NoExplicitNull, ] = None backup_image_required: Annotated[ StrictBool | None, Field( description='Whether a backup image must accompany the zip for non-HTML5 environments.' ), + NoExplicitNull, ] = None backup_image_max_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum backup image file size in kilobytes.', ge=1) + SchemaInt | None, + Field(description='Maximum backup image file size in kilobytes.', ge=1), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/formats/canonical/image.py b/src/adcp/types/domains/formats/canonical/image.py index dc9cf22f0..ce9900579 100644 --- a/src/adcp/types/domains/formats/canonical/image.py +++ b/src/adcp/types/domains/formats/canonical/image.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/image.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat from pydantic import ConfigDict, Field, StrictBool @@ -61,7 +61,7 @@ class CanonicalFormatImage(CanonicalFormatBase): model_config = ConfigDict( extra='allow', ) - motion_level: MotionLevel | None = None + motion_level: Annotated[MotionLevel | None, NoExplicitNull] = None slots: Annotated[ Any | None, Field( @@ -81,6 +81,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description="Logical render width in pixels — use for fixed-size slots (e.g., a 300×250 IAB MREC). When `pixel_ratios` is absent, the required image asset width is the same value (1x). When `pixel_ratios` is present, an accepted asset's intrinsic width is `width × pixel_ratio`. For multi-size flexible slots, use `sizes[]`; for responsive slots, use the min/max fields. The three size modes are mutually exclusive.", ge=1, ), + NoExplicitNull, ] = None height: Annotated[ SchemaInt | None, @@ -88,6 +89,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='Logical render height in pixels. Intrinsic asset height is `height × pixel_ratio`, where the ratio defaults to 1 when `pixel_ratios` is absent. See `width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None sizes: Annotated[ list[Size] | None, @@ -95,6 +97,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='List of accepted logical (width, height) render pairs for a multi-size flexible slot. The buyer ships an asset matching one logical size multiplied by one accepted `pixel_ratios` entry (or by 1 when `pixel_ratios` is absent). SDKs MUST treat size and density as separate axes: a 600×500 intrinsic asset at 2x satisfies a logical 300×250 size; it does not create a logical 600×500 placement. Mirrors OpenRTB `banner.format[]` semantics. Mutually exclusive with `(width, height)` and with responsive ranges.', min_length=1, ), + NoExplicitNull, ] = None pixel_ratios: Annotated[ list[PixelRatio] | None, @@ -102,6 +105,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='Accepted intrinsic-pixel densities for image assets, expressed as intrinsic pixels per logical render pixel (for example `[1, 2]` accepts both standard and Retina renditions). Absence means `[1]` for backward compatibility. This is an acceptance set, not a requirement to submit every rendition: one `image_main` asset satisfying any listed ratio is sufficient unless the effective `image_main` slot declares `required_pixel_ratios`. SDKs determine the effective ratio from `asset.pixel_ratio` when supplied, otherwise infer it from intrinsic asset dimensions divided by the matched logical size. Width and height ratios MUST agree.', min_length=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -109,6 +113,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description="Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`.", ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -116,6 +121,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -123,6 +129,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='Minimum accepted height in pixels for responsive slots. Pair with `max_height`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -130,6 +137,7 @@ class CanonicalFormatImage(CanonicalFormatBase): description='Maximum accepted height in pixels for responsive slots. Pair with `min_height`.', ge=1, ), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -137,46 +145,53 @@ class CanonicalFormatImage(CanonicalFormatBase): description="Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio.", pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None max_file_size_kb: Annotated[ - SchemaInt | None, Field(description='Maximum file size in kilobytes.', ge=1) + SchemaInt | None, Field(description='Maximum file size in kilobytes.', ge=1), NoExplicitNull ] = None image_formats: Annotated[ - list[ImageFormat] | None, Field(description='Permitted image file formats.') + list[ImageFormat] | None, Field(description='Permitted image file formats.'), NoExplicitNull ] = None ssl_required: Annotated[ StrictBool | None, Field(description='Whether the image and its trackers must be served over HTTPS.'), + NoExplicitNull, ] = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - body_text_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + body_text_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None cta_values: Annotated[ list[str] | None, Field( description="Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." ), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource | None, Field( description="Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` — replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class). `publisher_owned_reference`: buyer references an existing post or publisher-owned object via a `published_post` slot; the seller resolves and serves the referenced content after authorization/review rather than receiving uploaded bytes.\n\nNot every value is meaningful on every canonical — `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. `publisher_owned_reference` is meaningful only when the product's `slots` declaration accepts a reference asset such as `published_post`. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description="Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly — they must use build_creative (or sync_creatives with brief inputs or reference assets) so the seller produces or resolves the asset. Combined with `asset_source`, lets a product declare 'I produce assets from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`) or 'I accept existing post references, not uploaded bytes' (asset_source=`publisher_owned_reference`, buyer_asset_acceptance=`rejected`)." ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `image` only `pause` and `screensaver` are valid — the image-plus-copy contract the major pause-ad sellers ingest (seller composites the frame; typical canvas 1920×1080 or a transparent-region overlay). Other experiences route per the matrix in docs/creative/ctv-experiences.mdx.' ), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers (e.g. `qr_code` on a pause frame). Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/image_carousel.py b/src/adcp/types/domains/formats/canonical/image_carousel.py index ed5b1bade..3c9c90d93 100644 --- a/src/adcp/types/domains/formats/canonical/image_carousel.py +++ b/src/adcp/types/domains/formats/canonical/image_carousel.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/image_carousel.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ._base import CanonicalFormatBase @@ -44,19 +44,24 @@ class CanonicalFormatImageCarousel(CanonicalFormatBase): description="Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5').", pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None min_cards: Annotated[ - SchemaInt | None, Field(description='Minimum card count (typical: 2 or 3).', ge=2) + SchemaInt | None, + Field(description='Minimum card count (typical: 2 or 3).', ge=2), + NoExplicitNull, ] = None max_cards: Annotated[ SchemaInt | None, Field(description='Maximum card count (typical: 6, 10, or 35 depending on platform).'), + NoExplicitNull, ] = None allowed_card_media_asset_types: Annotated[ list[AllowedCardMediaAssetType] | None, Field( description='Asset types each card\'s `media` field may carry. Default: [\'image\']. Polymorphic carousels (Meta) allow [\'image\', \'video\']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card\'s media payload, not the card-asset itself (which is always asset_type: "card").' ), + NoExplicitNull, ] = None allowed_card_asset_types: Annotated[ list[AllowedCardMediaAssetType] | None, @@ -64,13 +69,15 @@ class CanonicalFormatImageCarousel(CanonicalFormatBase): deprecated=True, description='DEPRECATED — alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0.', ), + NoExplicitNull, ] = None - card_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - card_video_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None - card_video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None + card_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + card_video_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + card_video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None primary_text_max_chars: Annotated[ SchemaInt | None, Field(description='Maximum length of the carousel-level primary text.', ge=1), + NoExplicitNull, ] = None card_headline_max_chars: Annotated[ SchemaInt | None, @@ -78,6 +85,7 @@ class CanonicalFormatImageCarousel(CanonicalFormatBase): description='Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot.', ge=1, ), + NoExplicitNull, ] = None card_description_max_chars: Annotated[ SchemaInt | None, @@ -85,5 +93,6 @@ class CanonicalFormatImageCarousel(CanonicalFormatBase): description='Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars).', ge=1, ), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/formats/canonical/native_in_feed.py b/src/adcp/types/domains/formats/canonical/native_in_feed.py index c23c5ad89..dd3f79c61 100644 --- a/src/adcp/types/domains/formats/canonical/native_in_feed.py +++ b/src/adcp/types/domains/formats/canonical/native_in_feed.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/native_in_feed.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ...enums import activation_method @@ -103,28 +103,33 @@ class CanonicalFormatNativeInFeed(CanonicalFormatBase): Field( description='CTV experience this option serves. On native_in_feed `menu` and `overlay` are valid. `menu`: smart-TV home/menu surfaces where the platform assembles buyer assets (background/main image, logo/icon, copy, optional focus-triggered video); `menu_placement` selects the tile vs headline-banner variant, and catalog-derived sponsored tiles route to `sponsored_placement` instead. `overlay`: seller-composited in-stream overlays supplied as an asset bundle (video, logo, imagery, copy, activation copy) — the contract overlay sellers that do not ingest VAST tags use; VAST-ingesting sellers publish a `video_vast` sibling option instead. The wire name stays `native_in_feed` for 3.x even though neither surface is literally in-feed.' ), + NoExplicitNull, ] = None menu_placement: Annotated[ MenuPlacement | None, Field( description='Menu surface variant, mapping to OpenRTB Native `plcmttype` 1 (tile/feed) and 3 (headline banner). Valid only with `ctv_ad_experience: "menu"`.' ), + NoExplicitNull, ] = None focus_behavior: Annotated[ FocusBehavior | None, Field( description='What happens when the viewer\'s remote focus lands on the unit. `autoplay_*` requires a `video` asset; playback method maps to AdCOM playbackmethod on OpenRTB bridges. Valid only with `ctv_ad_experience: "menu"`.' ), + NoExplicitNull, ] = None motion_level: Annotated[ motion_level_1.CreativeMotionLevel | None, Field(description='Accepted motion class for the rendered unit (AdCOM attrs 21-23).'), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers (QR, deep link, send-to-device). Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None title_max_chars: Annotated[ SchemaInt | None, @@ -132,6 +137,7 @@ class CanonicalFormatNativeInFeed(CanonicalFormatBase): description='Maximum character length for the title slot. IAB native typical: 25 (short) to 90 (long). Buyer agents SHOULD validate ship-time title length against this.', ge=1, ), + NoExplicitNull, ] = None body_text_max_chars: Annotated[ SchemaInt | None, @@ -139,16 +145,19 @@ class CanonicalFormatNativeInFeed(CanonicalFormatBase): description='Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended).', ge=1, ), + NoExplicitNull, ] = None cta_max_chars: Annotated[ SchemaInt | None, Field(description='Maximum character length for the cta slot. Typical: 15–25.', ge=1), + NoExplicitNull, ] = None cta_values: Annotated[ list[str] | None, Field( description="Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." ), + NoExplicitNull, ] = None main_image_sizes: Annotated[ list[MainImageSize] | None, @@ -156,35 +165,41 @@ class CanonicalFormatNativeInFeed(CanonicalFormatBase): description='Accepted logical (width, height) pairs for the main_image slot. Common IAB native sizes: 1200×627 (1.91:1), 1080×1080 (1:1), 1080×1350 (4:5). When the effective main_image slot declares `pixel_ratios`, intrinsic asset dimensions are the matched logical pair multiplied by the selected ratio; absence remains 1x.', min_length=1, ), + NoExplicitNull, ] = None icon_size: Annotated[ IconSize | None, Field( description='Required logical (width, height) for the icon slot when present (typical: 80×80 or 100×100). When the effective icon slot declares `pixel_ratios`, intrinsic dimensions are multiplied by the selected ratio; absence remains 1x.' ), + NoExplicitNull, ] = None max_image_file_size_kb: Annotated[ SchemaInt | None, Field(description='Maximum file size in kilobytes for main_image and icon.', ge=1), + NoExplicitNull, ] = None image_formats: Annotated[ - list[ImageFormat] | None, Field(description='Permitted image file formats.') + list[ImageFormat] | None, Field(description='Permitted image file formats.'), NoExplicitNull ] = None ssl_required: Annotated[ StrictBool | None, Field( description='Whether trackers, landing pages, and image URLs must be served over HTTPS.' ), + NoExplicitNull, ] = None asset_source: Annotated[ AssetSource | None, Field( description="Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output. `publisher_owned_reference`: buyer ships an existing published post reference; the seller resolves the post into the native presentation after authorization/review." ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description='Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body — they must use `build_creative`, `sync_creatives` with brief inputs, or an accepted `published_post` reference so the seller produces or resolves the native bundle.' ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted diff --git a/src/adcp/types/domains/formats/canonical/responsive_creative.py b/src/adcp/types/domains/formats/canonical/responsive_creative.py index bc698e42e..208bc3186 100644 --- a/src/adcp/types/domains/formats/canonical/responsive_creative.py +++ b/src/adcp/types/domains/formats/canonical/responsive_creative.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: formats/canonical/responsive_creative.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ._base import CanonicalFormatBase @@ -94,34 +94,35 @@ class CanonicalFormatResponsiveCreative(CanonicalFormatBase): 'max': 1, }, ] - headlines_min: Annotated[SchemaInt | None, Field(ge=0)] = None - headlines_max: Annotated[SchemaInt | None, Field(ge=0)] = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - long_headlines_min: Annotated[SchemaInt | None, Field(ge=0)] = None - long_headlines_max: Annotated[SchemaInt | None, Field(ge=0)] = None - long_headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - descriptions_min: Annotated[SchemaInt | None, Field(ge=0)] = None - descriptions_max: Annotated[SchemaInt | None, Field(ge=0)] = None - description_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - images_landscape_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_landscape_max: Annotated[SchemaInt | None, Field(ge=0)] = None - images_landscape_aspect_ratio: str | None = None - images_square_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_square_max: Annotated[SchemaInt | None, Field(ge=0)] = None - images_vertical_min: Annotated[SchemaInt | None, Field(ge=0)] = None - images_vertical_max: Annotated[SchemaInt | None, Field(ge=0)] = None - videos_min: Annotated[SchemaInt | None, Field(ge=0)] = None - videos_max: Annotated[SchemaInt | None, Field(ge=0)] = None - video_min_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None - video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1)] = None - logo_min: Annotated[SchemaInt | None, Field(ge=0)] = None - logo_max: Annotated[SchemaInt | None, Field(ge=0)] = None - logo_aspect_ratios: list[str] | None = None - business_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - asset_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + headlines_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + headlines_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + long_headlines_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + long_headlines_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + long_headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + descriptions_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + descriptions_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + description_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + images_landscape_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_landscape_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_landscape_aspect_ratio: Annotated[str | None, NoExplicitNull] = None + images_square_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_square_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_vertical_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + images_vertical_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + videos_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + videos_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + video_min_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + video_max_duration_ms: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + logo_min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + logo_max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + logo_aspect_ratios: Annotated[list[str] | None, NoExplicitNull] = None + business_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + asset_image_max_file_size_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None supports_catalog_input: Annotated[ StrictBool | None, Field( description='Whether the product can additionally consume a catalog reference (e.g., PMax with product feed).' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/seller_rendered_stateful_display.py b/src/adcp/types/domains/formats/canonical/seller_rendered_stateful_display.py index cb7c76dfa..128c50722 100644 --- a/src/adcp/types/domains/formats/canonical/seller_rendered_stateful_display.py +++ b/src/adcp/types/domains/formats/canonical/seller_rendered_stateful_display.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/seller_rendered_stateful_display.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarInt, ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -65,7 +65,7 @@ class Breakpoint(AdCPBaseModel): pattern='^[a-z][a-z0-9_]*$', ), ] - width: Annotated[SchemaInt | None, Field(ge=1)] = None + width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None width_range: Annotated[ list[WidthRangeItem] | None, Field( @@ -73,14 +73,16 @@ class Breakpoint(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None width_mode: Annotated[ WidthMode | None, Field( description='Fluid width classes. `full_bleed`: edge-to-edge viewport width (IAB adhesion, topscroll). `gutter_residual`: the margin remaining beside the page content column (desktop skins). Fluid breakpoints cannot be targeted by `rendered_canvases` supply — semantic validation rejects that pairing.' ), + NoExplicitNull, ] = None - height: Annotated[SchemaInt | None, Field(ge=1)] = None + height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None height_range: Annotated[ list[HeightRangeItem] | None, Field( @@ -88,6 +90,7 @@ class Breakpoint(AdCPBaseModel): max_length=2, min_length=2, ), + NoExplicitNull, ] = None viewport_height_percent: Annotated[ StrictFloat | None, @@ -96,6 +99,7 @@ class Breakpoint(AdCPBaseModel): gt=0.0, le=100.0, ), + NoExplicitNull, ] = None canvas_aspect_ratio: Annotated[ str | None, @@ -103,6 +107,7 @@ class Breakpoint(AdCPBaseModel): description='Aspect ratio the rendered canvas maintains across the size range (IAB flexible units, ratio-bucket responsive). When both fixed `width` and `height` are declared, the ratio MUST be consistent with them.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None @@ -128,12 +133,14 @@ class State(AdCPBaseModel): Field( description='Asset group IDs rendered in this state (components mode). Makes the template executable: given components and bindings, assembly is deterministic. Every value MUST resolve to a declared slot. Omitted means all supplied component slots may render.' ), + NoExplicitNull, ] = None motion: Annotated[ Motion | None, Field( description='Whether the seller-rendered layout animates within this state (attract loops, load animations). Intra-state animation is seller-rendered; buyer canvases stay static images.' ), + NoExplicitNull, ] = Motion.static max_animation_s: Annotated[ StrictFloat | None, @@ -141,6 +148,7 @@ class State(AdCPBaseModel): description='Upper bound on intra-state animation duration. Required when `motion` is `animated`.', gt=0.0, ), + NoExplicitNull, ] = None breakpoints: Annotated[list[Breakpoint], Field(min_length=1)] close_affordance: Annotated[ @@ -225,18 +233,21 @@ class Transitions(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent | None, Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -254,6 +265,7 @@ class Transitions(AdCPBaseModel): duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -262,12 +274,14 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -276,6 +290,7 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -284,12 +299,14 @@ class Transitions(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -317,18 +334,21 @@ class Transitions7(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent | None, Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -346,6 +366,7 @@ class Transitions7(AdCPBaseModel): duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -354,12 +375,14 @@ class Transitions7(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -368,6 +391,7 @@ class Transitions7(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -376,12 +400,14 @@ class Transitions7(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -415,12 +441,14 @@ class Transitions8(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -434,10 +462,12 @@ class Transitions8(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat, @@ -460,6 +490,7 @@ class Transitions8(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -468,12 +499,14 @@ class Transitions8(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -513,12 +546,14 @@ class Transitions9(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ Literal['scroll_linked'], @@ -532,10 +567,12 @@ class Transitions9(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -544,6 +581,7 @@ class Transitions9(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference, @@ -572,6 +610,7 @@ class Transitions9(AdCPBaseModel): Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -605,12 +644,14 @@ class Transitions10(AdCPBaseModel): Field( description='Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).' ), + NoExplicitNull, ] = None direction: Annotated[ Direction | None, Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -624,10 +665,12 @@ class Transitions10(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -636,12 +679,14 @@ class Transitions10(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -650,6 +695,7 @@ class Transitions10(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -658,12 +704,14 @@ class Transitions10(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -691,6 +739,7 @@ class Transitions11(AdCPBaseModel): Field( description='Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.' ), + NoExplicitNull, ] = None media_event: Annotated[ MediaEvent, @@ -703,6 +752,7 @@ class Transitions11(AdCPBaseModel): Field( description='Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.' ), + NoExplicitNull, ] = Direction.down transition_mode: Annotated[ TransitionMode, @@ -716,10 +766,12 @@ class Transitions11(AdCPBaseModel): description='For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).', ge=0, ), + NoExplicitNull, ] = None duration_ms: Annotated[ SchemaInt | None, Field(description='Duration of a seller-rendered animated transition.', ge=0), + NoExplicitNull, ] = None scroll_threshold_percent: Annotated[ StrictFloat | None, @@ -728,12 +780,14 @@ class Transitions11(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_reference: Annotated[ ScrollReference | None, Field( description='Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.' ), + NoExplicitNull, ] = None scroll_start_percent: Annotated[ StrictFloat | None, @@ -742,6 +796,7 @@ class Transitions11(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None scroll_end_percent: Annotated[ StrictFloat | None, @@ -750,12 +805,14 @@ class Transitions11(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None preserve_playback: Annotated[ StrictBool | None, Field( description='Whether `video_main` continues without restart while the seller changes state.' ), + NoExplicitNull, ] = False @@ -813,6 +870,7 @@ class CanonicalFormatSellerRenderedStatefulDisplay(CanonicalFormatBase): Field( description='Which end of the template contract the buyer feeds. `components`: buyer supplies component slots; seller renders states (no `state_canvases`/`layered_source` assets allowed). `rendered_canvases`: buyer supplies exactly one `state_canvases` image per declared state × breakpoint pair. `layered_source`: buyer ships design source (+ optional `font_files`); seller production derives states (`production_window_business_days` applies) — transitional for sellers without executable templates.' ), + NoExplicitNull, ] = SupplyMode.components slots: Annotated[ Any | None, @@ -852,6 +910,7 @@ class CanonicalFormatSellerRenderedStatefulDisplay(CanonicalFormatBase): Field( description='How the unit enters view, distinct from state changes. `clip_window`: canvas fixed and progressively exposed through a scrolling window (interscroller, topscroll). `scroll_parallax`: canvas moves at a different rate than content. Reveal is presentation of one canvas, not a transition; do not fabricate a second state to express it.' ), + NoExplicitNull, ] = Reveal.none transitions: Annotated[ list[ @@ -862,12 +921,14 @@ class CanonicalFormatSellerRenderedStatefulDisplay(CanonicalFormatBase): description='Bounded seller-rendered transitions between declared visual states. Required when `states` has more than one entry; MUST be omitted for single-state units. Every non-initial state MUST be reachable from `initial_state_id`. Dismissal is terminal unit behavior declared by `user_controls.dismissible`, not a hidden visual state.', min_length=1, ), + NoExplicitNull, ] = None clickthrough: Annotated[ Clickthrough | None, Field( description='Destination policy. `required` (default): manifest MUST supply `landing_page_url`. `optional`: click-optional units (in-feed brand units) may omit it. `none`: unit is non-clickable; manifests MUST NOT supply `landing_page_url` or `state_click_urls`. Per-state overrides via `state_click_urls` entries carrying `state_id`; `landing_page_url` is the fallback for unlisted states.' ), + NoExplicitNull, ] = Clickthrough.required user_controls: Annotated[ UserControls, @@ -880,6 +941,7 @@ class CanonicalFormatSellerRenderedStatefulDisplay(CanonicalFormatBase): Field( description='Rectangular areas constraining buyer artwork. Omitted state/breakpoint selectors apply the constraint to every canvas. For fluid or range-sized breakpoints, use percent-unit regions.' ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRange | None] | None, @@ -888,28 +950,32 @@ class CanonicalFormatSellerRenderedStatefulDisplay(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None - duration_ms_exact: Annotated[SchemaInt | None, Field(ge=1)] = None + duration_ms_exact: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None aspect_ratio: Annotated[ str | None, Field( description='Embedded-video aspect ratio.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None - containers: list[Container] | None = None - video_playback: VideoPlayback | None = None - max_initial_load_kb: Annotated[SchemaInt | None, Field(ge=1)] = None + containers: Annotated[list[Container] | None, NoExplicitNull] = None + video_playback: Annotated[VideoPlayback | None, NoExplicitNull] = None + max_initial_load_kb: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_subload_kb: Annotated[ SchemaInt | None, Field( description='Ceiling on assets loaded after the window load event (IAB LEAN subload). Pairs with `max_initial_load_kb` to mirror the New Ad Portfolio initial/subload weight pair.', ge=1, ), + NoExplicitNull, ] = None polite_load: Annotated[ StrictBool | None, Field( description="When true, non-initial assets load only after the host page's window load event (IAB LEAN subload boundary)." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/sponsored_placement.py b/src/adcp/types/domains/formats/canonical/sponsored_placement.py index b3ab0970d..4640e87dc 100644 --- a/src/adcp/types/domains/formats/canonical/sponsored_placement.py +++ b/src/adcp/types/domains/formats/canonical/sponsored_placement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: formats/canonical/sponsored_placement.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool from ...enums import catalog_type @@ -68,44 +68,55 @@ class CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven(CanonicalFormatB supported_catalog_types: Annotated[ list[catalog_type.CatalogType] | None, Field(description='Catalog types this product accepts.'), + NoExplicitNull, ] = None min_items: Annotated[ - SchemaInt | None, Field(description='Minimum catalog item count buyer must supply.', ge=1) + SchemaInt | None, + Field(description='Minimum catalog item count buyer must supply.', ge=1), + NoExplicitNull, ] = None max_items: Annotated[ - SchemaInt | None, Field(description='Maximum items considered for placement.') + SchemaInt | None, + Field(description='Maximum items considered for placement.'), + NoExplicitNull, ] = None fanout_mode: Annotated[ FanoutMode | None, Field( description='How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item.' ), + NoExplicitNull, ] = None required_catalog_fields: Annotated[ list[str] | None, Field( description="Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." ), + NoExplicitNull, ] = None supported_id_types: Annotated[ list[SupportedIdType] | None, Field(description='Catalog identifier types the placement renders against.'), + NoExplicitNull, ] = None hero_asset_supported: Annotated[ StrictBool | None, Field( description='Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern).' ), + NoExplicitNull, ] = None item_production_model: Annotated[ ItemProductionModel | None, Field( description='How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset — drops `publisher_host_recorded` because it\'s audio-specific and doesn\'t apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer\'s catalog already contains rendered assets per item; the seller composes the placement using those assets. ("Uploaded" reads slightly off for catalog-keyed items where the buyer didn\'t actively upload bytes — the catalog ingestion already supplied them — but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller\'s design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief × N catalog items → N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering.' ), + NoExplicitNull, ] = ItemProductionModel.buyer_uploaded ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `sponsored_placement`: `menu` (sponsored app/content tiles and rows whose assets derive from a catalog listing — Fire-TV-tile pattern), `squeezeback`, and `in_scene` (seller-composited brand integrations produced from the catalog/brief rather than a buyer wire creative). Asset-bundle menu heroes route to `native_in_feed`.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/video_hosted.py b/src/adcp/types/domains/formats/canonical/video_hosted.py index b5bcd0e21..26e0d1d87 100644 --- a/src/adcp/types/domains/formats/canonical/video_hosted.py +++ b/src/adcp/types/domains/formats/canonical/video_hosted.py @@ -1,15 +1,14 @@ # generated by datamodel-codegen: # filename: formats/canonical/video_hosted.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt - from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt +from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ...enums import ctv_ad_experience as ctv_ad_experience_1 @@ -101,6 +100,7 @@ class CanonicalFormatHostedVideo(CanonicalFormatBase): Field( description='Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed).' ), + NoExplicitNull, ] = None aspect_ratio: Annotated[ str | None, @@ -108,11 +108,12 @@ class CanonicalFormatHostedVideo(CanonicalFormatBase): description='Aspect ratio. Inferred from orientation if omitted.', pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$', ), + NoExplicitNull, ] = None - min_width: Annotated[SchemaInt | None, Field(ge=1)] = None - min_height: Annotated[SchemaInt | None, Field(ge=1)] = None - max_width: Annotated[SchemaInt | None, Field(ge=1)] = None - max_height: Annotated[SchemaInt | None, Field(ge=1)] = None + min_width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + min_height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_width: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_height: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None duration_ms_range: Annotated[ list[DurationMsRange | None] | None, Field( @@ -120,6 +121,7 @@ class CanonicalFormatHostedVideo(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -127,43 +129,49 @@ class CanonicalFormatHostedVideo(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description).', ge=1, ), + NoExplicitNull, ] = None - video_codecs: list[VideoCodec] | None = None - audio_codecs: list[AudioCodec] | None = None - containers: list[Container] | None = None - min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None - max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1)] = None + video_codecs: Annotated[list[VideoCodec] | None, NoExplicitNull] = None + audio_codecs: Annotated[list[AudioCodec] | None, NoExplicitNull] = None + containers: Annotated[list[Container] | None, NoExplicitNull] = None + min_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + max_bitrate_kbps: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None max_file_size_mb: Annotated[ SchemaInt | None, Field(description='Maximum file size, where 1 MB is exactly 1,000,000 bytes.', ge=1), + NoExplicitNull, ] = None - frame_rates: list[StrictFloat] | None = None - captions: Captions | None = None - om_sdk_required: StrictBool | None = None - headline_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - primary_text_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1)] = None - cta_values: list[str] | None = None + frame_rates: Annotated[list[StrictFloat] | None, NoExplicitNull] = None + captions: Annotated[Captions | None, NoExplicitNull] = None + om_sdk_required: Annotated[StrictBool | None, NoExplicitNull] = None + headline_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + primary_text_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + brand_name_max_chars: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + cta_values: Annotated[list[str] | None, NoExplicitNull] = None companion_banner_widths: Annotated[ list[CompanionBannerWidth] | None, Field(description='Permitted companion banner widths (instream video).'), + NoExplicitNull, ] = None - companion_banner_heights: list[CompanionBannerHeight] | None = None + companion_banner_heights: Annotated[list[CompanionBannerHeight] | None, NoExplicitNull] = None asset_source: Annotated[ AssetSource | None, Field( description='Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video. `publisher_owned_reference` is valid when the product accepts an existing post reference via a `published_post` slot instead of uploaded video bytes. Adopters MUST select a value appropriate to the canonical.' ), + NoExplicitNull, ] = AssetSource.buyer_uploaded buyer_asset_acceptance: Annotated[ BuyerAssetAcceptance | None, Field( description='Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly — they must use build_creative, sync_creatives with brief inputs, or sync_creatives with an accepted reference asset so the seller produces or resolves the video.' ), + NoExplicitNull, ] = BuyerAssetAcceptance.accepted ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option serves. On `video_hosted` only `screensaver` is valid (ambient looping video the platform plays on idle). Other experiences route per the matrix in docs/creative/ctv-experiences.mdx; linear CTV video declares no experience.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/formats/canonical/video_vast.py b/src/adcp/types/domains/formats/canonical/video_vast.py index 4d746ee51..e0de48c63 100644 --- a/src/adcp/types/domains/formats/canonical/video_vast.py +++ b/src/adcp/types/domains/formats/canonical/video_vast.py @@ -1,15 +1,14 @@ # generated by datamodel-codegen: # filename: formats/canonical/video_vast.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarInt - from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt +from adcp.types._scalar import ScalarInt from pydantic import ConfigDict, Field, StrictBool from ...core import vast_media_file_requirements @@ -55,9 +54,9 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): {'asset_group_id': 'vast_tag', 'asset_type': 'vast', 'required': True}, {'asset_group_id': 'landing_page_url', 'asset_type': 'url', 'required': False}, ] - orientation: Orientation | None = None + orientation: Annotated[Orientation | None, NoExplicitNull] = None aspect_ratio: Annotated[ - str | None, Field(pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$') + str | None, Field(pattern='^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$'), NoExplicitNull ] = None vast_versions: Annotated[ list[vast_version_1.VastVersion] | None, @@ -65,6 +64,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='VAST versions accepted by this product format option. The asset still declares exactly one `vast_version`; compatibility requires membership in this set and the seller-wide execution set.', min_length=1, ), + NoExplicitNull, ] = None vast_version: Annotated[ vast_version_1.VastVersion | None, @@ -72,25 +72,29 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): deprecated=True, description='Deprecated one-element alias for `vast_versions`. Producers use either the singular legacy alias or the plural 3.2 field, never both.', ), + NoExplicitNull, ] = None media_file_requirements: Annotated[ vast_media_file_requirements.VastMediafileRequirements | None, Field( description='Technical acceptance constraints for alternative VAST MediaFile renditions. Each applicable resolved InLine linear creative needs at least one MediaFile satisfying all declared constraints.' ), + NoExplicitNull, ] = None vpaid_enabled: Annotated[ StrictBool | None, Field( description='Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads.' ), + NoExplicitNull, ] = None - vpaid_version: VpaidVersion | None = None + vpaid_version: Annotated[VpaidVersion | None, NoExplicitNull] = None simid_supported: Annotated[ StrictBool | None, Field( description='Whether the seller accepts IAB SIMID through `` on a Linear VAST creative. SIMID is not a generic VAST extension and cannot be serialized under NonLinearAds; every `ctv_ad_experience` profile therefore forbids `true`.' ), + NoExplicitNull, ] = None duration_ms_range: Annotated[ list[DurationMsRangeItem] | None, @@ -99,6 +103,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): max_length=2, min_length=2, ), + NoExplicitNull, ] = None duration_ms_exact: Annotated[ SchemaInt | None, @@ -106,6 +111,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship.', ge=1, ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, @@ -113,6 +119,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='Minimum placement/player width in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, @@ -120,6 +127,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='Maximum placement/player width in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, @@ -127,6 +135,7 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='Minimum placement/player height in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, @@ -134,34 +143,40 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='Maximum placement/player height in pixels. MediaFile rendition dimensions are declared in `media_file_requirements`.', ge=1, ), + NoExplicitNull, ] = None creative_type: Annotated[ CreativeType | None, Field( description='Required VAST creative class: `linear` (in-stream Linear), `nonlinear` (NonLinearAds overlay-class), or `either`. Supersedes `linear_required`; when both are present `creative_type` wins, and validators treat `linear_required: true` with no `creative_type` as `linear`.' ), + NoExplicitNull, ] = None ctv_ad_experience: Annotated[ ctv_ad_experience_1.CtvAdExperience | None, Field( description='CTV experience this option is eligible to serve. On video_vast only `pause`, `screensaver`, `overlay`, `squeezeback`, and `in_scene` are valid (`menu` routes to native_in_feed or sponsored_placement), and `creative_type` MUST be `nonlinear`. Because VAST places `` only under Linear ``, `simid_supported` MUST NOT be true on any of these NonLinear profiles. Per-experience floors: `overlay` and `squeezeback` require a 10s minimum duration; `in_scene` requires a 3s minimum brand-exposure duration and forbids interactivity (`vpaid_enabled` MUST NOT be true); `pause` has no duration floor and ends on viewer or device action.' ), + NoExplicitNull, ] = None motion_level: Annotated[ motion_level_1.CreativeMotionLevel | None, Field(description='Accepted motion class for the rendered creative (AdCOM attrs 21-23).'), + NoExplicitNull, ] = None activation_methods: Annotated[ list[activation_method.CreativeActivationMethod] | None, Field( description='Viewer activation mechanisms this option offers. Activations are engagement events, not impressions.' ), + NoExplicitNull, ] = None linear_required: Annotated[ StrictBool | None, Field( description='Whether the VAST creative must be linear (non-skippable in-stream). Superseded by `creative_type`; retained for pre-3.2 declarations.' ), + NoExplicitNull, ] = None skippable_after_ms: Annotated[ SchemaInt | None, @@ -169,8 +184,11 @@ class CanonicalFormatVastVideo(CanonicalFormatBase): description='When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll).', ge=0, ), + NoExplicitNull, ] = None max_wrapper_depth: Annotated[ - SchemaInt | None, Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0) + SchemaInt | None, + Field(description='Maximum VAST wrapper redirect depth permitted.', ge=0), + NoExplicitNull, ] = None - ssl_required: StrictBool | None = None + ssl_required: Annotated[StrictBool | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/attribute_definition.py b/src/adcp/types/domains/governance/attribute_definition.py index 5946ab296..6443be766 100644 --- a/src/adcp/types/domains/governance/attribute_definition.py +++ b/src/adcp/types/domains/governance/attribute_definition.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: governance/attribute_definition.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -18,6 +18,7 @@ class RegulatoryBasi(AdCPBaseModel): jurisdictions: Annotated[ list[str] | None, Field(description='ISO 3166-1 alpha-2 codes where this regulation applies.'), + NoExplicitNull, ] = None summary: Annotated[ str, Field(description='How this regulation defines or restricts the attribute.') @@ -45,28 +46,33 @@ class AttributeDefinition(AdCPBaseModel): regulatory_basis: Annotated[ list[RegulatoryBasi] | None, Field(description='Regulations that define or restrict this attribute category.'), + NoExplicitNull, ] = None includes: Annotated[ list[str] | None, Field( description="Specific data types that fall within this category (e.g., for health_data: 'medical conditions', 'disability status', 'prescription history', 'inferred health from behavioral signals')." ), + NoExplicitNull, ] = None excludes: Annotated[ list[str] | None, Field( description='Data types that might seem related but are explicitly outside this category. Helps with boundary cases.' ), + NoExplicitNull, ] = None signal_patterns: Annotated[ list[str] | None, Field( description="Common signal naming or tagging patterns that indicate this attribute (e.g., 'health:', 'condition_', 'diagnosis_'). Data providers and governance agents use these as hints when signals lack explicit restricted_attributes declarations." ), + NoExplicitNull, ] = None guidance: Annotated[ str | None, Field( description='Implementation notes. Covers edge cases, inferred vs. declared data, and common pitfalls.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/governance/audience_constraints.py b/src/adcp/types/domains/governance/audience_constraints.py index 7364e4f4e..8a77c3fcc 100644 --- a/src/adcp/types/domains/governance/audience_constraints.py +++ b/src/adcp/types/domains/governance/audience_constraints.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: governance/audience_constraints.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import audience_selector @@ -22,6 +22,7 @@ class AudienceConstraints(AdCPBaseModel): description="Desired audience criteria. The seller's targeting should align with these. Each criterion is evaluated independently — the combined targeting should satisfy at least one inclusion criterion.", min_length=1, ), + NoExplicitNull, ] = None exclude: Annotated[ list[audience_selector.AudienceSelector] | None, @@ -29,4 +30,5 @@ class AudienceConstraints(AdCPBaseModel): description="Excluded audience criteria. The seller's targeting must not overlap with these. Exclusions take precedence over inclusions. Used for protected groups, vulnerable communities, regulatory restrictions, or brand safety.", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/governance/check_governance_request.py b/src/adcp/types/domains/governance/check_governance_request.py index e5ad75148..a1a4e98cb 100644 --- a/src/adcp/types/domains/governance/check_governance_request.py +++ b/src/adcp/types/domains/governance/check_governance_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/check_governance_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictFloat, StringConstraints from ..core import brand_ref, business_entity, canonical_proposal @@ -73,6 +73,7 @@ class AudienceDistribution(AdCPBaseModel): Field( description="Description of the baseline when baseline is 'custom' (e.g., 'US adults 18+ with broadband access')." ), + NoExplicitNull, ] = None indices: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-z_]+:.+$')], StrictFloat], @@ -85,6 +86,7 @@ class AudienceDistribution(AdCPBaseModel): Field( description='Cumulative audience index values since the governed action started. Same key format as indices (dimension:value). Use for detecting sustained bias drift that may not appear in a single reporting period.' ), + NoExplicitNull, ] = None @@ -99,6 +101,7 @@ class DeliveryMetrics(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None statement_digest: Annotated[ str | None, @@ -106,6 +109,7 @@ class DeliveryMetrics(AdCPBaseModel): description='SHA-256 digest of RFC 8785 JCS over {seller_reference, delivery_metrics}, excluding statement_digest itself. The authenticated submission binds the seller to this digest.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None sequence: Annotated[ SchemaInt | None, @@ -113,19 +117,25 @@ class DeliveryMetrics(AdCPBaseModel): description='Monotonically increasing sequence for this governed action. A statement ID or sequence cannot be reused with different content.', ge=1, ), + NoExplicitNull, ] = None issued_at: Annotated[ - AwareDatetime | None, Field(description='When the seller issued the canonical statement.') + AwareDatetime | None, + Field(description='When the seller issued the canonical statement.'), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod, Field(description='Start and end timestamps for the reporting window.') ] spend: Annotated[ - StrictFloat | None, Field(description='Total spend during the reporting period.', ge=0.0) + StrictFloat | None, + Field(description='Total spend during the reporting period.', ge=0.0), + NoExplicitNull, ] = None cumulative_spend: Annotated[ StrictFloat | None, Field(description='Total spend since the governed action started.', ge=0.0), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -133,38 +143,45 @@ class DeliveryMetrics(AdCPBaseModel): description='Currency of spend fields; must match the plan and planned delivery.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt | None, Field(description='Impressions delivered during the reporting period.', ge=0), + NoExplicitNull, ] = None cumulative_impressions: Annotated[ SchemaInt | None, Field(description='Total impressions since the governed action started.', ge=0), + NoExplicitNull, ] = None geo_distribution: Annotated[ dict[str, StrictFloat] | None, Field( description='Actual geographic distribution. Keys are ISO 3166-1 alpha-2 codes, values are percentages.' ), + NoExplicitNull, ] = None channel_distribution: Annotated[ dict[str, StrictFloat] | None, Field( description='Actual channel distribution. Keys are channel enum values, values are percentages.' ), + NoExplicitNull, ] = None pacing: Annotated[ Pacing | None, Field( description='Whether delivery is ahead of, on track with, or behind the planned pace.' ), + NoExplicitNull, ] = None audience_distribution: Annotated[ AudienceDistribution | None, Field( description='Actual audience composition during the reporting period. Enables mid-flight drift detection when actual delivery skews from planned audience targeting.' ), + NoExplicitNull, ] = None @@ -183,6 +200,7 @@ class DeliveryMetrics2(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None statement_digest: Annotated[ str | None, @@ -190,6 +208,7 @@ class DeliveryMetrics2(AdCPBaseModel): description='SHA-256 digest of RFC 8785 JCS over {seller_reference, delivery_metrics}, excluding statement_digest itself. The authenticated submission binds the seller to this digest.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None sequence: Annotated[ SchemaInt | None, @@ -197,19 +216,25 @@ class DeliveryMetrics2(AdCPBaseModel): description='Monotonically increasing sequence for this governed action. A statement ID or sequence cannot be reused with different content.', ge=1, ), + NoExplicitNull, ] = None issued_at: Annotated[ - AwareDatetime | None, Field(description='When the seller issued the canonical statement.') + AwareDatetime | None, + Field(description='When the seller issued the canonical statement.'), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod, Field(description='Start and end timestamps for the reporting window.') ] spend: Annotated[ - StrictFloat | None, Field(description='Total spend during the reporting period.', ge=0.0) + StrictFloat | None, + Field(description='Total spend during the reporting period.', ge=0.0), + NoExplicitNull, ] = None cumulative_spend: Annotated[ StrictFloat | None, Field(description='Total spend since the governed action started.', ge=0.0), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -217,38 +242,45 @@ class DeliveryMetrics2(AdCPBaseModel): description='Currency of spend fields; must match the plan and planned delivery.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None impressions: Annotated[ SchemaInt | None, Field(description='Impressions delivered during the reporting period.', ge=0), + NoExplicitNull, ] = None cumulative_impressions: Annotated[ SchemaInt | None, Field(description='Total impressions since the governed action started.', ge=0), + NoExplicitNull, ] = None geo_distribution: Annotated[ dict[str, StrictFloat] | None, Field( description='Actual geographic distribution. Keys are ISO 3166-1 alpha-2 codes, values are percentages.' ), + NoExplicitNull, ] = None channel_distribution: Annotated[ dict[str, StrictFloat] | None, Field( description='Actual channel distribution. Keys are channel enum values, values are percentages.' ), + NoExplicitNull, ] = None pacing: Annotated[ Pacing | None, Field( description='Whether delivery is ahead of, on track with, or behind the planned pace.' ), + NoExplicitNull, ] = None audience_distribution: Annotated[ AudienceDistribution1 | None, Field( description='Actual audience composition during the reporting period. Enables mid-flight drift detection when actual delivery skews from planned audience targeting.' ), + NoExplicitNull, ] = None @@ -261,7 +293,7 @@ class Subject(AdCPBaseModel): agent_url: Annotated[ AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject6(AdCPBaseModel): @@ -295,8 +327,9 @@ class Subject6(AdCPBaseModel): description='Optional SHA-256 pin for the exact content or immutable snapshot identified by this resource subject. This is part of the complete typed subject identity and is distinct from AttestationReference.content_digest, which pins credential bytes.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Subject8(Subject): @@ -314,7 +347,7 @@ class Subject4(AdCPBaseModel): type: Literal['brand'] = 'brand' resource_type: Literal['https://adcontextprotocol.org/claims/subjects/signal'] = 'https://adcontextprotocol.org/claims/subjects/signal' brand: brand_ref.BrandReference - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class RuntimeAttestation(AttestationReference): @@ -337,6 +370,7 @@ class RuntimeAttestation(AttestationReference): ], title='Attestation Subject', ), + NoExplicitNull, ] = None @@ -349,6 +383,7 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): Field( description="Campaign governance plan identifier. Required on the initial intent or availability check, before a governance_context exists. Optional on subsequent checks: the governance agent derives the plan from its own signed governance_context. If both are present, the governance agent MUST reject the request when plan_id does not match the token's plan binding. Services MUST treat governance_context as authoritative and MUST NOT require a buyer to disclose plan_id. A plan is owned by the authenticated buyer principal that synchronized it; plan_id is an identifier, not an account credential." ), + NoExplicitNull, ] = None caller: Annotated[ AnyUrl, @@ -361,42 +396,49 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): Field( description="The type of financial commitment being checked. Determines which budget allocation (if any) to validate against. Defaults to 'media_buy' when omitted." ), + NoExplicitNull, ] = purchase_type_1.PurchaseType.media_buy target_agent: Annotated[ AnyUrl | None, Field( description='Exact agent URL of the downstream service that will receive the governed task. Required on intent checks and copied byte-for-byte into the signed governance_context aud claim. This routing and authorization field is not part of payload: payload remains exactly the downstream task arguments. A consultation re-check MUST use the same target_agent.' ), + NoExplicitNull, ] = None proposed_commitment: Annotated[ ProposedCommitment | None, Field( description='Task-neutral monetary amount the intent would authorize. For update_media_buy and control_media_buy this is the buyer-computed positive incremental commitment, not the post-update total. For accept_proposal it is derived from the supplied proposal commercial_terms; for buy_products it is derived from the purchase payload. Amount 0 explicitly represents a verified no-cost action. The governance agent persists this value as authoritative check state.' ), + NoExplicitNull, ] = None execution_commitment: Annotated[ ExecutionCommitment | None, Field( description='Seller-computed positive incremental commitment for a MediaBuy execution check. The seller MUST derive this atomically from its authoritative proposal or current revision and the requested operation, and the governance agent MUST reject it when it exceeds the prior intent ceiling or uses another currency.' ), + NoExplicitNull, ] = None tool: Annotated[ str | None, Field( description="The AdCP tool being checked (e.g., 'create_media_buy', 'acquire_rights', 'activate_signal'). Present on intent checks (orchestrator). The governance agent uses the presence of tool+payload to identify an intent check." ), + NoExplicitNull, ] = None payload: Annotated[ dict[str, Any] | None, Field( description='The full downstream tool arguments exactly as they will be sent to target_agent. Present on intent checks. Governance routing metadata is carried by target_agent, never injected into this object. The governance agent can inspect any field to validate against the plan.' ), + NoExplicitNull, ] = None proposal: Annotated[ canonical_proposal.CanonicalProposal | None, Field( description='Exact committed proposal being authorized for accept_proposal. The governance agent verifies proposal.terms_digest against commercial_terms and binds that digest into its decision state; the downstream payload carries the same digest without repeating the terms.' ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -406,6 +448,7 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None consultation_context: Annotated[ str | None, @@ -415,22 +458,26 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): min_length=1, pattern='^[A-Za-z0-9_.:-]+$', ), + NoExplicitNull, ] = None phase: Annotated[ governance_phase.GovernancePhase | None, Field( description="The phase of an execution-shaped governed action. Ignored for intent-shaped tool+payload requests. 'purchase': initial commitment; 'modification': update to an existing commitment; 'delivery': periodic delivery reporting. Defaults to purchase if omitted. planned_delivery_1.media_buy_id is optional for purchase and required for modification/delivery." ), + NoExplicitNull, ] = governance_phase.GovernancePhase.purchase planned_delivery: Annotated[ planned_delivery_1.PlannedDelivery | None, Field(description='What the seller will actually deliver. Present on execution checks.'), + NoExplicitNull, ] = None delivery_metrics: Annotated[ DeliveryMetrics | None, Field( description="Seller-attributed canonical delivery statement. MUST be present for 'delivery' phase. The authenticated seller binds one immutable statement_id and digest to a monotonically increasing sequence; the buyer can later submit the copy it received or an independent observation through report_plan_outcome." ), + NoExplicitNull, ] = None modification_summary: Annotated[ str | None, @@ -438,6 +485,7 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): description="Human-readable summary of what changed. SHOULD be present for 'modification' phase.", max_length=1000, ), + NoExplicitNull, ] = None runtime_attestations: Annotated[ list[RuntimeAttestation] | None, @@ -446,15 +494,17 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): max_length=10, min_length=1, ), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description='Invoice recipient from the purchase request. MUST be present when the tool payload includes invoice_recipient, so the governance agent can validate billing changes.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class RuntimeAttestation1(AttestationReference): @@ -477,6 +527,7 @@ class RuntimeAttestation1(AttestationReference): ], title='Attestation Subject', ), + NoExplicitNull, ] = None @@ -489,6 +540,7 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): Field( description="Campaign governance plan identifier. Required on the initial intent or availability check, before a governance_context exists. Optional on subsequent checks: the governance agent derives the plan from its own signed governance_context. If both are present, the governance agent MUST reject the request when plan_id does not match the token's plan binding. Services MUST treat governance_context as authoritative and MUST NOT require a buyer to disclose plan_id. A plan is owned by the authenticated buyer principal that synchronized it; plan_id is an identifier, not an account credential." ), + NoExplicitNull, ] = None caller: Annotated[ AnyUrl, @@ -501,42 +553,49 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): Field( description="The type of financial commitment being checked. Determines which budget allocation (if any) to validate against. Defaults to 'media_buy' when omitted." ), + NoExplicitNull, ] = purchase_type_1.PurchaseType.media_buy target_agent: Annotated[ AnyUrl | None, Field( description='Exact agent URL of the downstream service that will receive the governed task. Required on intent checks and copied byte-for-byte into the signed governance_context aud claim. This routing and authorization field is not part of payload: payload remains exactly the downstream task arguments. A consultation re-check MUST use the same target_agent.' ), + NoExplicitNull, ] = None proposed_commitment: Annotated[ ProposedCommitment | None, Field( description='Task-neutral monetary amount the intent would authorize. For update_media_buy and control_media_buy this is the buyer-computed positive incremental commitment, not the post-update total. For accept_proposal it is derived from the supplied proposal commercial_terms; for buy_products it is derived from the purchase payload. Amount 0 explicitly represents a verified no-cost action. The governance agent persists this value as authoritative check state.' ), + NoExplicitNull, ] = None execution_commitment: Annotated[ ExecutionCommitment | None, Field( description='Seller-computed positive incremental commitment for a MediaBuy execution check. The seller MUST derive this atomically from its authoritative proposal or current revision and the requested operation, and the governance agent MUST reject it when it exceeds the prior intent ceiling or uses another currency.' ), + NoExplicitNull, ] = None tool: Annotated[ str | None, Field( description="The AdCP tool being checked (e.g., 'create_media_buy', 'acquire_rights', 'activate_signal'). Present on intent checks (orchestrator). The governance agent uses the presence of tool+payload to identify an intent check." ), + NoExplicitNull, ] = None payload: Annotated[ dict[str, Any] | None, Field( description='The full downstream tool arguments exactly as they will be sent to target_agent. Present on intent checks. Governance routing metadata is carried by target_agent, never injected into this object. The governance agent can inspect any field to validate against the plan.' ), + NoExplicitNull, ] = None proposal: Annotated[ canonical_proposal.CanonicalProposal | None, Field( description='Exact committed proposal being authorized for accept_proposal. The governance agent verifies proposal.terms_digest against commercial_terms and binds that digest into its decision state; the downstream payload carries the same digest without repeating the terms.' ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -546,6 +605,7 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None consultation_context: Annotated[ str | None, @@ -555,22 +615,26 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): min_length=1, pattern='^[A-Za-z0-9_.:-]+$', ), + NoExplicitNull, ] = None phase: Annotated[ governance_phase.GovernancePhase | None, Field( description="The phase of an execution-shaped governed action. Ignored for intent-shaped tool+payload requests. 'purchase': initial commitment; 'modification': update to an existing commitment; 'delivery': periodic delivery reporting. Defaults to purchase if omitted. planned_delivery_1.media_buy_id is optional for purchase and required for modification/delivery." ), + NoExplicitNull, ] = governance_phase.GovernancePhase.purchase planned_delivery: Annotated[ planned_delivery_1.PlannedDelivery | None, Field(description='What the seller will actually deliver. Present on execution checks.'), + NoExplicitNull, ] = None delivery_metrics: Annotated[ DeliveryMetrics2 | None, Field( description="Seller-attributed canonical delivery statement. MUST be present for 'delivery' phase. The authenticated seller binds one immutable statement_id and digest to a monotonically increasing sequence; the buyer can later submit the copy it received or an independent observation through report_plan_outcome." ), + NoExplicitNull, ] = None modification_summary: Annotated[ str | None, @@ -578,6 +642,7 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): description="Human-readable summary of what changed. SHOULD be present for 'modification' phase.", max_length=1000, ), + NoExplicitNull, ] = None runtime_attestations: Annotated[ list[RuntimeAttestation1] | None, @@ -586,15 +651,17 @@ class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2): max_length=10, min_length=1, ), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description='Invoice recipient from the purchase request. MUST be present when the tool payload includes invoice_recipient, so the governance agent can validate billing changes.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class CheckGovernanceRequest(AdcpRequest, CheckGovernanceRequest3): diff --git a/src/adcp/types/domains/governance/check_governance_response.py b/src/adcp/types/domains/governance/check_governance_response.py index 95abdfd38..81cc41cd2 100644 --- a/src/adcp/types/domains/governance/check_governance_response.py +++ b/src/adcp/types/domains/governance/check_governance_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/check_governance_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat, model_validator from ..core import context as context_1 @@ -103,6 +103,7 @@ class ActionBinding(AdCPBaseModel): description='SHA-256 digest of the domain-defined canonical action preimage.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -121,17 +122,21 @@ class Finding(AdCPBaseModel): Field( description="ID of the policy that triggered this finding. May reference a registry policy (with source: registry) or a bespoke inline policy (with source: inline). Bespoke policy_ids are unique within their authoring container; use source_plan_id when findings aggregate across multiple plans (e.g., portfolio evaluations). When the violation traces to a producer-tagged surface (feature-requirement, creative-feature-result, or validation-result feature) carrying policy_id, governance agents SHOULD echo that policy_id here for end-to-end traceability, and MUST NOT invent a policy_id that wasn't present on the originating surface. See /docs/governance/policy-attribution." ), + NoExplicitNull, ] = None source_plan_id: Annotated[ str | None, Field( description="For portfolio or aggregated evaluations where findings draw on bespoke policies from multiple member plans: identifies the plan whose policy triggered this finding. Omit when the finding's policy_id is unambiguous within the response context (e.g., single-plan check_governance)." ), + NoExplicitNull, ] = None severity: escalation_severity.EscalationSeverity explanation: Annotated[str, Field(description='Human-readable description of the issue.')] details: Annotated[ - dict[str, Any] | None, Field(description='Structured details for programmatic consumption.') + dict[str, Any] | None, + Field(description='Structured details for programmatic consumption.'), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, @@ -140,12 +145,14 @@ class Finding(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None uncertainty_reason: Annotated[ str | None, Field( description="Explanation of why confidence is below 1.0 (e.g., 'Targeting includes regions that partially overlap jurisdiction boundaries'). Present when confidence is below a governance-agent-defined threshold." ), + NoExplicitNull, ] = None attestation_reference_digest: Annotated[ str | None, @@ -153,6 +160,7 @@ class Finding(AdCPBaseModel): description='When this finding relies on or reports a runtime attestation evaluation, the reference_digest of the exact AttestationReference in runtime_attestation_evaluations[]. Findings MUST NOT copy credential bodies or treat a presenter assertion as verified evidence.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -194,12 +202,14 @@ def _status_to_verdict(cls, data: Any) -> Any: Field( description='Check shape that produced the verdict. Required for the cross-role governance_enforcement contract. Its presence selects the modern verdict-specific response rules; its absence selects the deprecated legacy 3.x compatibility shape. Intent checks may return conditions; execution checks are binary approved or denied.' ), + NoExplicitNull, ] = None plan_id: Annotated[ str | None, Field( description='Plan identifier echoed on an initial plan-addressed check. Optional on continuation checks addressed by governance_context; services do not need this value and MUST treat the token binding as authoritative.' ), + NoExplicitNull, ] = None explanation: Annotated[ str, Field(description='Human-readable explanation of the governance decision.') @@ -209,12 +219,14 @@ def _status_to_verdict(cls, data: Any) -> Any: Field( description="Specific issues found during the governance check. Present when verdict is 'denied' or 'conditions'. MAY also be present on 'approved' for informational findings (e.g., budget approaching limit)." ), + NoExplicitNull, ] = None conditions: Annotated[ list[Condition] | None, Field( description="Intent-phase counterproposal. Present only when verdict is 'conditions'. It does not authorize execution and MUST NOT be returned for execution or lifecycle checks. Each field path is rooted at the complete check_governance request arguments, so both payload.* and proposed_commitment.* can be addressed. After applying conditions, the caller MUST re-call check_governance with the adjusted parameters and receive approved before proceeding." ), + NoExplicitNull, ] = None consultation_context: Annotated[ str | None, @@ -224,42 +236,49 @@ def _status_to_verdict(cls, data: Any) -> Any: min_length=1, pattern='^[A-Za-z0-9_.:-]+$', ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description="When this approval expires. In the cross-role shape, present only when verdict is 'approved'. Deprecated legacy conditions responses may also carry it for 3.x compatibility. The caller must act before this time or re-call check_governance. A lapsed approval is no approval." ), + NoExplicitNull, ] = None next_check: Annotated[ AwareDatetime | None, Field( description='When the seller should next call check_governance with delivery metrics. Present when the governance agent expects ongoing delivery reporting.' ), + NoExplicitNull, ] = None delivery_statement: Annotated[ DeliveryStatement | None, Field( description='Canonical seller-attributed delivery statement retained by governance. Present on delivery execution checks. The buyer binds any later observation to this exact statement through report_plan_outcome.' ), + NoExplicitNull, ] = None categories_evaluated: Annotated[ list[str] | None, Field( description="Governance categories evaluated during this check. Each value is an **agent-internal** label (e.g., `budget_authority`, `regulatory_compliance`, or any internal-reviewer key the agent's policy model defines) — not a protocol-level enum. Since one governance agent per account composes all specialist review behind its single endpoint, `categories_evaluated` is how that internal decomposition surfaces to auditors. Consumers MUST treat values as opaque labels for display and audit, not as a machine-level contract." ), + NoExplicitNull, ] = None policies_evaluated: Annotated[ list[str] | None, Field( description="Policy IDs evaluated during this check. Includes registry policy IDs (resolved via the policy registry) and any inline `policy_id`s declared in the plan's `custom_policies`." ), + NoExplicitNull, ] = None mode: Annotated[ governance_mode.GovernanceMode | None, Field( description='Governance enforcement mode active when this check was evaluated. Allows counterparties, regulators, and auditors to distinguish whether a finding blocked execution (enforce) or was logged silently (audit).' ), + NoExplicitNull, ] = None runtime_attestation_evaluations: Annotated[ list[RuntimeAttestationEvaluation] | None, @@ -268,6 +287,7 @@ def _status_to_verdict(cls, data: Any) -> Any: max_length=10, min_length=1, ), + NoExplicitNull, ] = None runtime_attestation_binding_digest: Annotated[ str | None, @@ -275,6 +295,7 @@ def _status_to_verdict(cls, data: Any) -> Any: description='SHA-256 of RFC 8785 JCS({ evaluations: runtime_attestation_evaluations, findings: attestation_bound_findings }), where attestation_bound_findings is the response findings[] subset carrying attestation_reference_digest, preserved in response order. Required whenever runtime_attestation_evaluations is present. The governance_context JWS carries this exact value as runtime_attestation_binding_digest; get_plan_audit_logs retains ordered {reference, evaluation} pairs so auditors can first recompute every reference_digest and then prove which evaluations and findings the signed decision relied on.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -284,6 +305,7 @@ def _status_to_verdict(cls, data: Any) -> Any: min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/get_plan_audit_logs_request.py b/src/adcp/types/domains/governance/get_plan_audit_logs_request.py index c64043c8a..9602dd4c3 100644 --- a/src/adcp/types/domains/governance/get_plan_audit_logs_request.py +++ b/src/adcp/types/domains/governance/get_plan_audit_logs_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/get_plan_audit_logs_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, model_validator from ..core import context as context_1 @@ -29,6 +30,7 @@ class GetPlanAuditLogsRequest(AdcpRequest, AdcpVersionEnvelope): description='Plan IDs to retrieve. For a single plan, pass a one-element array. Plans uniquely scope account and operator; do not include a separate `account` field — the governance agent resolves account from each plan. Including `account` is rejected by `additionalProperties: false`.', min_length=1, ), + NoExplicitNull, ] = None portfolio_plan_ids: Annotated[ list[str] | None, @@ -36,6 +38,7 @@ class GetPlanAuditLogsRequest(AdcpRequest, AdcpVersionEnvelope): description='Portfolio plan IDs. The governance agent expands each to its member_plan_ids and returns combined audit data.', min_length=1, ), + NoExplicitNull, ] = None governance_contexts: Annotated[ list[str] | None, @@ -43,6 +46,7 @@ class GetPlanAuditLogsRequest(AdcpRequest, AdcpVersionEnvelope): description='Filter audit entries by governance context. Returns only checks and outcomes that share these governance contexts, enabling lifecycle tracing across purchase types.', min_length=1, ), + NoExplicitNull, ] = None purchase_types: Annotated[ list[purchase_type.PurchaseType] | None, @@ -50,12 +54,15 @@ class GetPlanAuditLogsRequest(AdcpRequest, AdcpVersionEnvelope): description="Filter audit entries by purchase type. Returns only checks and outcomes matching these purchase types (e.g., ['rights_license'] to see all rights activity).", min_length=1, ), + NoExplicitNull, ] = None include_entries: Annotated[ - StrictBool | None, Field(description='Include the full audit trail. Default: false.') + StrictBool | None, + Field(description='Include the full audit trail. Default: false.'), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> GetPlanAuditLogsRequest: diff --git a/src/adcp/types/domains/governance/get_plan_audit_logs_response.py b/src/adcp/types/domains/governance/get_plan_audit_logs_response.py index 5e41b33ee..ed50527f8 100644 --- a/src/adcp/types/domains/governance/get_plan_audit_logs_response.py +++ b/src/adcp/types/domains/governance/get_plan_audit_logs_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/get_plan_audit_logs_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import attestation_evaluation, attestation_reference @@ -38,58 +38,70 @@ class Budget(AdCPBaseModel): extra='forbid', ) authorized: Annotated[ - StrictFloat | None, Field(description='Total authorized budget from the plan.') + StrictFloat | None, + Field(description='Total authorized budget from the plan.'), + NoExplicitNull, ] = None - accounting_mode: AccountingMode | None = None + accounting_mode: Annotated[AccountingMode | None, NoExplicitNull] = None committed: Annotated[ StrictFloat | None, Field( description='Legacy 3.x alias for net_committed after verified headroom-restoring adjustments.' ), + NoExplicitNull, ] = None gross_committed: Annotated[ StrictFloat | None, Field( description='Total authoritative commitment recorded from completed outcomes before adjustments.' ), + NoExplicitNull, ] = None adjustments_reported: Annotated[ StrictFloat | None, Field( description='Sum of all append-only adjustment amounts, including audit-only refunds, credits, and makegoods.' ), + NoExplicitNull, ] = None adjustments_verified: Annotated[ StrictFloat | None, Field( description='Sum of buyer-accepted economic reductions in plan currency; accepted makegoods contribute zero.' ), + NoExplicitNull, ] = None net_cost: Annotated[ StrictFloat | None, Field( description='Gross commitment minus verified economic reductions, independent of whether accounting_mode restores headroom.' ), + NoExplicitNull, ] = None headroom_restored: Annotated[ StrictFloat | None, Field( description='Sum of verified adjustments eligible to restore reusable headroom under accounting_mode.' ), + NoExplicitNull, ] = None net_committed: Annotated[ StrictFloat | None, Field(description='Current plan obligation after verified headroom-restoring adjustments.'), + NoExplicitNull, ] = None ledger_committed: Annotated[ StrictFloat | None, Field(description='Current amount charged against plan headroom under accounting_mode.'), + NoExplicitNull, + ] = None + remaining: Annotated[ + StrictFloat | None, Field(description='Authorized minus committed.'), NoExplicitNull ] = None - remaining: Annotated[StrictFloat | None, Field(description='Authorized minus committed.')] = ( - None - ) utilization_pct: Annotated[ - StrictFloat | None, Field(description='Committed as a percentage of authorized.') + StrictFloat | None, + Field(description='Committed as a percentage of authorized.'), + NoExplicitNull, ] = None @@ -98,10 +110,12 @@ class ChannelAllocation(AdCPBaseModel): extra='forbid', ) committed: Annotated[ - StrictFloat | None, Field(description='Budget committed to this channel.') + StrictFloat | None, Field(description='Budget committed to this channel.'), NoExplicitNull ] = None pct: Annotated[ - StrictFloat | None, Field(description="Channel's share of the authorized total budget.") + StrictFloat | None, + Field(description="Channel's share of the authorized total budget."), + NoExplicitNull, ] = None @@ -109,14 +123,15 @@ class Statuses(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - approved: SchemaInt | None = None - denied: SchemaInt | None = None - conditions: SchemaInt | None = None + approved: Annotated[SchemaInt | None, NoExplicitNull] = None + denied: Annotated[SchemaInt | None, NoExplicitNull] = None + conditions: Annotated[SchemaInt | None, NoExplicitNull] = None human_reviewed: Annotated[ SchemaInt | None, Field( description='Supplementary count of checks that went through internal human review. These checks are also counted in approved or denied.' ), + NoExplicitNull, ] = None @@ -129,9 +144,10 @@ class Escalation(AdCPBaseModel): resolution: Annotated[ str | None, Field(description="How it was resolved (e.g., 'approved_by_human', 'rejected_by_human')."), + NoExplicitNull, ] = None resolved_at: Annotated[ - AwareDatetime | None, Field(description='ISO 8601 resolution timestamp.') + AwareDatetime | None, Field(description='ISO 8601 resolution timestamp.'), NoExplicitNull ] = None @@ -152,6 +168,7 @@ class Thresholds(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None escalation_rate_min: Annotated[ StrictFloat | None, @@ -160,10 +177,12 @@ class Thresholds(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None auto_approval_rate_max: Annotated[ StrictFloat | None, Field(description='Maximum acceptable auto-approval rate.', ge=0.0, le=1.0), + NoExplicitNull, ] = None human_override_rate_max: Annotated[ StrictFloat | None, @@ -172,6 +191,7 @@ class Thresholds(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None @@ -182,16 +202,19 @@ class DriftMetrics(AdCPBaseModel): escalation_rate: Annotated[ StrictFloat | None, Field(description='Fraction of checks that resulted in escalation.', ge=0.0, le=1.0), + NoExplicitNull, ] = None escalation_rate_trend: Annotated[ EscalationRateTrend | None, Field(description="Direction of escalation rate over the plan's lifetime."), + NoExplicitNull, ] = None auto_approval_rate: Annotated[ StrictFloat | None, Field( description='Fraction of checks approved without human intervention.', ge=0.0, le=1.0 ), + NoExplicitNull, ] = None human_override_rate: Annotated[ StrictFloat | None, @@ -200,6 +223,7 @@ class DriftMetrics(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None mean_confidence: Annotated[ StrictFloat | None, @@ -208,12 +232,14 @@ class DriftMetrics(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None thresholds: Annotated[ Thresholds | None, Field( description='Organization-defined thresholds for drift metrics. When a metric crosses its threshold, the governance agent SHOULD include a finding on the next check. Set by the organization in governance agent configuration, echoed here for visibility.' ), + NoExplicitNull, ] = None @@ -222,34 +248,42 @@ class Summary(AdCPBaseModel): extra='forbid', ) checks_performed: Annotated[ - SchemaInt | None, Field(description='Total governance checks performed.') + SchemaInt | None, Field(description='Total governance checks performed.'), NoExplicitNull ] = None outcomes_reported: Annotated[ - SchemaInt | None, Field(description='Total outcomes reported.') + SchemaInt | None, Field(description='Total outcomes reported.'), NoExplicitNull ] = None adjustments_reported: Annotated[ - SchemaInt | None, Field(description='Total append-only adjustment records reported.') + SchemaInt | None, + Field(description='Total append-only adjustment records reported.'), + NoExplicitNull, ] = None adjustments_verified: Annotated[ SchemaInt | None, Field( description='Count of adjustment records accepted by the plan owner, including makegoods.' ), + NoExplicitNull, ] = None statuses: Annotated[ - Statuses | None, Field(description='Count of each governance check status.') + Statuses | None, Field(description='Count of each governance check status.'), NoExplicitNull ] = None findings_count: Annotated[ - SchemaInt | None, Field(description='Total findings across all checks and outcomes.') + SchemaInt | None, + Field(description='Total findings across all checks and outcomes.'), + NoExplicitNull, ] = None escalations: Annotated[ - list[Escalation] | None, Field(description='All escalations and their resolutions.') + list[Escalation] | None, + Field(description='All escalations and their resolutions.'), + NoExplicitNull, ] = None drift_metrics: Annotated[ DriftMetrics | None, Field( description='Aggregate governance metrics for detecting oversight drift. A declining escalation rate may indicate well-calibrated governance or eroding human oversight -- surfacing the trend lets the organization make that judgment.' ), + NoExplicitNull, ] = None @@ -269,12 +303,13 @@ class Finding(AdCPBaseModel): extra='forbid', ) category_id: str - policy_id: str | None = None + policy_id: Annotated[str | None, NoExplicitNull] = None source_plan_id: Annotated[ str | None, Field( description='Plan whose policy triggered this finding when the audit entry aggregates findings across plans.' ), + NoExplicitNull, ] = None severity: escalation_severity.EscalationSeverity explanation: str @@ -283,13 +318,15 @@ class Finding(AdCPBaseModel): Field( description='Structured finding details retained exactly as emitted by check_governance.' ), + NoExplicitNull, ] = None - confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0)] = None + confidence: Annotated[StrictFloat | None, Field(ge=0.0, le=1.0), NoExplicitNull] = None uncertainty_reason: Annotated[ str | None, Field( description='Reason the finding carries less than full confidence, retained exactly as emitted by check_governance.' ), + NoExplicitNull, ] = None attestation_reference_digest: Annotated[ str | None, @@ -297,6 +334,7 @@ class Finding(AdCPBaseModel): description='reference_digest of the runtime attestation evaluation this finding relied on.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None @@ -367,19 +405,22 @@ class Delivery(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None source: Annotated[ Source | None, Field( description='Whether the buyer is forwarding the seller statement it received or reporting an independently measured observation. A forwarded seller statement is not independent evidence.' ), + NoExplicitNull, ] = None - observed_at: AwareDatetime | None = None + observed_at: Annotated[AwareDatetime | None, NoExplicitNull] = None seller_statement_id: Annotated[ str | None, Field( description='Seller statement identifier as received by the buyer. Required for seller_statement_copy.' ), + NoExplicitNull, ] = None seller_statement_digest: Annotated[ str | None, @@ -387,23 +428,27 @@ class Delivery(AdCPBaseModel): description='Digest received with the seller statement. Governance compares it to the seller-authenticated canonical digest.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod | None, Field(description='Start and end timestamps for the reporting window.'), + NoExplicitNull, ] = None cumulative_spend: Annotated[ StrictFloat | None, Field(description='Buyer-attributed cumulative spend for the governed action.', ge=0.0), + NoExplicitNull, ] = None - currency: Annotated[str | None, Field(pattern='^[A-Z]{3}$')] = None + currency: Annotated[str | None, Field(pattern='^[A-Z]{3}$'), NoExplicitNull] = None period_closed: Annotated[ StrictBool | None, Field( description='Buyer-declared operational closure of this governance reporting period. When true, governance freezes the period after recording this observation. An unresolved mismatch becomes closed_unresolved and no longer blocks later governance. Closure is not final billing or commercial settlement.' ), + NoExplicitNull, ] = False - impressions: Annotated[SchemaInt | None, Field(ge=0)] = None + impressions: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class DeliveryReconciliationStatus(StrEnum): @@ -463,29 +508,36 @@ class GovernedAction(AdCPBaseModel): adjustments_reported: Annotated[ StrictFloat | None, Field(description='Sum of all append-only adjustment amounts for this action.'), + NoExplicitNull, ] = None adjustments_verified: Annotated[ - StrictFloat | None, Field(description='Buyer-accepted economic reductions for this action.') + StrictFloat | None, + Field(description='Buyer-accepted economic reductions for this action.'), + NoExplicitNull, ] = None net_cost: Annotated[ StrictFloat | None, Field(description='Gross action commitment minus verified economic reductions.'), + NoExplicitNull, ] = None headroom_restored: Annotated[ StrictFloat | None, Field( description='Verified adjustments eligible to restore headroom under the plan accounting mode.' ), + NoExplicitNull, ] = None net_committed: Annotated[ StrictFloat | None, Field(description='Current ledger obligation for this action after restored headroom.'), + NoExplicitNull, ] = None - seller_reported_spend: Annotated[StrictFloat | None, Field(ge=0.0)] = None - buyer_observed_spend: Annotated[StrictFloat | None, Field(ge=0.0)] = None + seller_reported_spend: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + buyer_observed_spend: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None delivery_reporting_period: Annotated[ DeliveryReportingPeriod | None, Field(description='Current seller statement period summarized for this governed action.'), + NoExplicitNull, ] = None conservative_exposure: Annotated[ StrictFloat | None, @@ -493,13 +545,17 @@ class GovernedAction(AdCPBaseModel): description='Greater of seller-reported and buyer-observed spend while evidence is unresolved.', ge=0.0, ), + NoExplicitNull, + ] = None + delivery_reconciliation_status: Annotated[ + DeliveryReconciliationStatus | None, NoExplicitNull ] = None - delivery_reconciliation_status: DeliveryReconciliationStatus | None = None delivery_period_state: Annotated[ DeliveryPeriodState | None, Field( description='Whether the current governance reporting period remains actionable. Closure is not final billing.' ), + NoExplicitNull, ] = None check_count: Annotated[ SchemaInt, Field(description='Number of governance checks performed for this action.') @@ -509,6 +565,7 @@ class GovernedAction(AdCPBaseModel): Field( description="The seller's identifier for the resource (e.g., media_buy_id, rights_grant_id). Present when reported via report_plan_outcome." ), + NoExplicitNull, ] = None @@ -532,88 +589,105 @@ class Entry(AdCPBaseModel): Field( description='Plan this entry belongs to. Present when querying multiple plans or a portfolio.' ), + NoExplicitNull, ] = None caller: Annotated[ AnyUrl | None, Field( description='URL of the agent that made the request. Resolved from the credentials used on the governance callback.' ), + NoExplicitNull, + ] = None + tool: Annotated[ + str | None, Field(description='The AdCP tool (present for check entries).'), NoExplicitNull ] = None - tool: Annotated[str | None, Field(description='The AdCP tool (present for check entries).')] = ( - None - ) verdict: Annotated[ governance_decision.GovernanceDecision | None, Field( description='Governance verdict (present for check entries). Renamed from `status` in 3.1 alongside check-governance-response for vocabulary consistency.' ), + NoExplicitNull, ] = None check_type: Annotated[ CheckType | None, Field( description='Whether the check was an intent check (orchestrator) or execution check (seller). Inferred from the fields present on the original check request. Present for check entries.' ), + NoExplicitNull, ] = None mode: Annotated[ governance_mode.GovernanceMode | None, Field( description="Governance mode active at the moment this specific check was evaluated. Governance agents SHOULD populate this field on check entries, recording the mode from their runtime configuration at the moment check_governance was processed — not derived from a plan field. This is a per-check value: if the operator changes mode between checks for the same governed action, each entry records the mode active for that entry. A future `governed_actions[].mode` field would describe the action's current mode, which may differ from the most recent entry's `mode` if the plan has since been re-synced. Absent for outcome entries and for pre-3.1 governance agents that do not surface mode on audit responses." ), + NoExplicitNull, ] = None explanation: Annotated[ str | None, Field( description='Human-readable explanation of the governance decision (present for check entries).' ), + NoExplicitNull, ] = None policies_evaluated: Annotated[ list[str] | None, Field( description="Policy IDs evaluated during this check. Includes registry policy IDs (resolved via the policy registry) and any inline `policy_id`s declared in the plan's `custom_policies`. Present for check entries." ), + NoExplicitNull, ] = None categories_evaluated: Annotated[ list[str] | None, Field( description="Governance categories evaluated (e.g., 'budget_authority', 'regulatory_compliance'). Present for check entries." ), + NoExplicitNull, ] = None findings: Annotated[ list[Finding] | None, Field( description='Findings from this check or outcome. Same structure as check_governance response findings.' ), + NoExplicitNull, ] = None delivery_statement: Annotated[ DeliveryStatement | None, Field(description='Canonical seller statement retained on a delivery check entry.'), + NoExplicitNull, ] = None outcome: Annotated[ outcome_type.OutcomeType | None, Field(description='Outcome type (present for outcome entries).'), + NoExplicitNull, ] = None error: Annotated[ reported_outcome_error.ReportedOutcomeError | None, Field( description='Buyer-attributed error retained from a failed report_plan_outcome call. This is a copy or classification supplied by the reporter, not authenticated seller evidence.' ), + NoExplicitNull, ] = None outcome_id: Annotated[ - str | None, Field(description='Source completed outcome (present for adjustment entries).') + str | None, + Field(description='Source completed outcome (present for adjustment entries).'), + NoExplicitNull, ] = None seller_adjustment_id: Annotated[ str | None, Field( description='Seller-issued source adjustment identifier (present for adjustment entries).' ), + NoExplicitNull, ] = None adjustment_type: Annotated[ AdjustmentType | None, Field(description='Commercial adjustment type (present for adjustment entries).'), + NoExplicitNull, ] = None amount: Annotated[ Amount | None, Field(description='Adjustment amount and currency (present for adjustment entries).'), + NoExplicitNull, ] = None headroom_restored: Annotated[ StrictFloat | None, @@ -621,16 +695,22 @@ class Entry(AdCPBaseModel): description='Current-obligation headroom restored by this adjustment. Zero for audit-only types.', ge=0.0, ), + NoExplicitNull, + ] = None + reason: Annotated[ + str | None, Field(description='Seller-supplied adjustment reason.'), NoExplicitNull ] = None - reason: Annotated[str | None, Field(description='Seller-supplied adjustment reason.')] = None effective_at: Annotated[ - AwareDatetime | None, Field(description="When the seller's adjustment became effective.") + AwareDatetime | None, + Field(description="When the seller's adjustment became effective."), + NoExplicitNull, ] = None committed_budget: Annotated[ StrictFloat | None, Field( description='Governance-authorized budget reserved (present for completed outcome entries).' ), + NoExplicitNull, ] = None reported_committed_budget: Annotated[ StrictFloat | None, @@ -638,21 +718,26 @@ class Entry(AdCPBaseModel): description='Caller-reported seller amount retained for reconciliation. Never ledger authority.', ge=0.0, ), + NoExplicitNull, ] = None seller_reference: Annotated[ - str | None, Field(description='Seller resource identifier retained on an outcome entry.') + str | None, + Field(description='Seller resource identifier retained on an outcome entry.'), + NoExplicitNull, ] = None delivery: Annotated[ Delivery | None, Field( description='Buyer-attributed observation retained as audit evidence on a delivery outcome. It never overwrites the seller statement.' ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, Field( description='Governance context for this entry (present for check and outcome entries).' ), + NoExplicitNull, ] = None plan_hash: Annotated[ str | None, @@ -660,6 +745,7 @@ class Entry(AdCPBaseModel): description='Audit-layer binding to the plan revision this attestation was evaluated over — base64url_no_pad(SHA-256(JCS(plan_payload))) per Plan binding and audit in the campaign-governance specification. Present on check entries. Auditors and buyer-side compliance verify by recomputing over the retained plan revision and byte-comparing the decoded 32-byte digests.', pattern='^[A-Za-z0-9_-]{43}$', ), + NoExplicitNull, ] = None runtime_attestations: Annotated[ list[RuntimeAttestation] | None, @@ -668,6 +754,7 @@ class Entry(AdCPBaseModel): max_length=10, min_length=1, ), + NoExplicitNull, ] = None runtime_attestation_binding_digest: Annotated[ str | None, @@ -675,24 +762,32 @@ class Entry(AdCPBaseModel): description="Binding digest copied from the check response and signed governance_context. Auditors recompute it from this entry's ordered runtime_attestations[].evaluation values plus findings carrying attestation_reference_digest after verifying each paired reference digest.", pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None purchase_type: Annotated[ - purchase_type_1.PurchaseType | None, Field(description='Purchase type for this entry.') + purchase_type_1.PurchaseType | None, + Field(description='Purchase type for this entry.'), + NoExplicitNull, ] = None outcome_status: Annotated[ - str | None, Field(description='Outcome status (present for outcome entries).') + str | None, + Field(description='Outcome status (present for outcome entries).'), + NoExplicitNull, + ] = None + delivery_reconciliation_status: Annotated[ + DeliveryReconciliationStatus | None, NoExplicitNull ] = None - delivery_reconciliation_status: DeliveryReconciliationStatus | None = None delivery_period_state: Annotated[ DeliveryPeriodState | None, Field(description='Operational governance-window state for this delivery observation.'), + NoExplicitNull, ] = None - adjustment_state: AdjustmentState | None = None - verified_amount: Annotated[StrictFloat | None, Field(ge=0.0)] = None - evidence: Evidence | None = None - reviewed_by: AnyUrl | None = None - reviewed_at: AwareDatetime | None = None - review_reason: str | None = None + adjustment_state: Annotated[AdjustmentState | None, NoExplicitNull] = None + verified_amount: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + evidence: Annotated[Evidence | None, NoExplicitNull] = None + reviewed_by: Annotated[AnyUrl | None, NoExplicitNull] = None + reviewed_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + review_reason: Annotated[str | None, NoExplicitNull] = None class Plan(AdCPBaseModel): @@ -706,11 +801,13 @@ class Plan(AdCPBaseModel): channel_allocation: Annotated[ dict[str, ChannelAllocation] | None, Field(description='Current channel mix. Keyed by channel ID.'), + NoExplicitNull, ] = None summary: Annotated[Summary, Field(description='Aggregate validation and outcome statistics.')] entries: Annotated[ list[Entry] | None, Field(description='Ordered audit trail. Only present when include_entries is true.'), + NoExplicitNull, ] = None governed_actions: Annotated[ list[GovernedAction], @@ -725,5 +822,5 @@ class GetPlanAuditLogsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelo extra='allow', ) plans: Annotated[list[Plan], Field(description='Audit data for each requested plan.')] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/policy_category_definition.py b/src/adcp/types/domains/governance/policy_category_definition.py index 248d8c0ac..5123d5d3a 100644 --- a/src/adcp/types/domains/governance/policy_category_definition.py +++ b/src/adcp/types/domains/governance/policy_category_definition.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: governance/policy_category_definition.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..enums import restricted_attribute @@ -31,12 +31,15 @@ class RegulatoryFramework(AdCPBaseModel): jurisdictions: Annotated[ list[str] | None, Field(description='ISO 3166-1 alpha-2 codes where this framework applies.'), + NoExplicitNull, ] = None summary: Annotated[ str, Field(description='Brief summary of what the framework requires or prohibits.') ] policy_ids: Annotated[ - list[str] | None, Field(description='Registry policy IDs that implement this framework.') + list[str] | None, + Field(description='Registry policy IDs that implement this framework.'), + NoExplicitNull, ] = None @@ -66,40 +69,47 @@ class PolicyCategoryDefinition(AdCPBaseModel): description='Registry-defined distinctions within the category that materially affect policy applicability or seller acceptance. Facets refine a category without creating a new top-level policy category.', min_length=1, ), + NoExplicitNull, ] = None regulatory_frameworks: Annotated[ list[RegulatoryFramework] | None, Field( description='Key regulations and standards grouped under this category. Governance agents use this to resolve specific policies from the registry.' ), + NoExplicitNull, ] = None restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, Field( description="Restricted attribute categories that regulations in this category prohibit for targeting. Governance agents enforce these when the category is active on a plan — if a plan declares policy_categories: ['fair_housing'], the governance agent restricts targeting on these attributes." ), + NoExplicitNull, ] = None requires_human_review: Annotated[ StrictBool | None, Field( description='When true, any plan declaring this category MUST set plan.human_review_required = true. Use for regulatory regimes that mandate human oversight under GDPR Art 22 or EU AI Act Annex III — fair_housing, fair_lending, fair_employment, pharmaceutical_advertising, and similar high-risk categories. Category-level setting applies to all policies and plans referencing it; policies can override on policy-entry.requires_human_review. Effective immediately regardless of individual policy `effective_date` fields.' ), + NoExplicitNull, ] = False industries: Annotated[ list[str] | None, Field( description="Industries where this category commonly applies (e.g., 'pharmaceutical' for age_restricted). Governance agents MAY suggest relevant categories when a plan's brand industry matches but no policy_categories are declared." ), + NoExplicitNull, ] = None guidance: Annotated[ str | None, Field( description='Implementation notes for governance agents. Edge cases, disambiguation, and common pitfalls.' ), + NoExplicitNull, ] = None related_categories: Annotated[ list[str] | None, Field( description="Categories that frequently co-occur (e.g., 'children_directed' often appears with 'age_restricted')." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/governance/policy_entry.py b/src/adcp/types/domains/governance/policy_entry.py index ebf6dff1f..5ef9b3597 100644 --- a/src/adcp/types/domains/governance/policy_entry.py +++ b/src/adcp/types/domains/governance/policy_entry.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/policy_entry.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..core import ext as ext_1 @@ -33,7 +33,7 @@ class Issuer(AdCPBaseModel): pattern='^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$', ), ] - name: Annotated[str | None, Field(min_length=1)] = None + name: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class Exemplar(AdCPBaseModel): @@ -54,9 +54,12 @@ class Exemplars(AdCPBaseModel): pass_: Annotated[ list[Exemplar] | None, Field(alias='pass', description='Scenarios that comply with this policy.'), + NoExplicitNull, ] = None fail: Annotated[ - list[Exemplar] | None, Field(description='Scenarios that violate this policy.') + list[Exemplar] | None, + Field(description='Scenarios that violate this policy.'), + NoExplicitNull, ] = None @@ -75,27 +78,33 @@ class PolicyEntry(AdCPBaseModel): Field( description="Origin of this policy. 'registry' = published to the shared AdCP policy registry with full regulatory metadata. 'inline' = authored bespoke for a specific standards configuration, plan, or portfolio. Defaults to 'inline'. Governance agents MUST set 'registry' when publishing to the registry. Within AdCP *task* payloads (every `$ref` to this schema in a request or response), the field is always 'inline' — registry entries are served by the policy registry API, not embedded in task traffic. The x-entity annotation on `policy_id` assumes the task-payload invariant; if a future task schema adopts registry-publishing, split the annotation accordingly (see issue #2685)." ), + NoExplicitNull, ] = Source.inline version: Annotated[ str | None, Field( description='Semver version string (e.g., "1.0.0"). Incremented when policy content changes. Optional for inline bespoke policies — defaults to "1.0.0". SHOULD be provided for registry-published policies.' ), + NoExplicitNull, ] = None name: Annotated[ str | None, Field( description='Human-readable name (e.g., "UK HFSS Restrictions"). Optional for inline bespoke policies — servers MAY default to policy_id.' ), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Brief summary of what this policy covers.', max_length=500) + str | None, + Field(description='Brief summary of what this policy covers.', max_length=500), + NoExplicitNull, ] = None category: Annotated[ policy_category.PolicyCategory | None, Field( description='The nature of the obligation: regulation (legal requirement) or standard (best practice). Optional for inline bespoke policies — defaults to "standard".' ), + NoExplicitNull, ] = None enforcement: Annotated[ policy_enforcement.PolicyEnforcementLevel, @@ -108,69 +117,82 @@ class PolicyEntry(AdCPBaseModel): Field( description='When true, plans subject to this policy MUST set plan.human_review_required = true. Use for policies that mandate human oversight of decisions affecting data subjects — e.g., GDPR Article 22 (solely automated decisions with legal or similarly significant effects) and EU AI Act Annex III high-risk categories (credit, insurance pricing, recruitment, housing allocation). Governance agents MUST escalate any plan action whose resolved policies include requires_human_review: true. Unlike `enforcement`, this flag applies as soon as the policy is resolved — it is NOT gated by `effective_date`. Art 22 GDPR and similar foundational obligations may predate an AI-Act-specific effective date; the human-review requirement fires regardless.' ), + NoExplicitNull, ] = False jurisdictions: Annotated[ list[str] | None, Field( description='ISO 3166-1 alpha-2 country codes where this policy applies. Empty array means the policy is not jurisdiction-specific.' ), + NoExplicitNull, ] = None region_aliases: Annotated[ dict[str, list[str]] | None, Field( description='Named groups of jurisdictions for convenience (e.g., {"EU": ["AT","BE","BG",...]}). Governance agents expand aliases when matching against a plan\'s target jurisdictions.' ), + NoExplicitNull, ] = None policy_categories: Annotated[ list[str] | None, Field( description='Regulatory categories this policy belongs to (e.g., ["children_directed", "age_restricted"]). Used for automatic matching against a campaign plan\'s declared policy_categories. A single policy can belong to multiple categories.' ), + NoExplicitNull, ] = None channels: Annotated[ list[channels_1.MediaChannel] | None, Field( description='Advertising channels this policy applies to. If omitted or null, the policy applies to all channels.' ), + NoExplicitNull, ] = None governance_domains: Annotated[ list[governance_domain.GovernanceDomain] | None, Field( description='Governance sub-domains this policy applies to. Determines which types of governance agents can declare registry:{policy_id} features. For example, a policy with domains ["creative", "property"] can be declared as a feature by both creative and property governance agents.' ), + NoExplicitNull, ] = None effective_date: Annotated[ date | None, Field( description='ISO 8601 date when the regulation or standard takes effect. Before this date, governance agents treat the policy as informational (evaluate but do not block). After this date, the policy is enforced at its declared enforcement level.' ), + NoExplicitNull, ] = None sunset_date: Annotated[ date | None, Field( description='ISO 8601 date when the regulation or standard is no longer enforced. After this date, governance agents stop evaluating this policy. Omit if the policy has no expiration.' ), + NoExplicitNull, ] = None source_url: Annotated[ - AnyUrl | None, Field(description='Link to the source regulation, standard, or legislation.') + AnyUrl | None, + Field(description='Link to the source regulation, standard, or legislation.'), + NoExplicitNull, ] = None source_name: Annotated[ str | None, Field( description='Name of the issuing body (e.g., "UK Food Standards Agency", "US Federal Trade Commission").' ), + NoExplicitNull, ] = None issuer: Annotated[ Issuer | None, Field( description='Machine-readable identity of the regulator, standards body, or platform operator that issued the policy. Registry publishers SHOULD provide this when independently versioned issuer policies must be distinguished.' ), + NoExplicitNull, ] = None acceptance_profile: Annotated[ acceptance_policy_profile.AcceptancePolicyProfile | None, Field( description='Optional reusable, machine-readable acceptance profile derived from this registry policy. Registry publishers MUST bind policy_refs to exact versions. Sellers adopt a profile explicitly; registry publication alone does not make it authoritative for a seller.' ), + NoExplicitNull, ] = None policy: Annotated[ str, @@ -184,11 +206,13 @@ class PolicyEntry(AdCPBaseModel): Field( description='Implementation notes for governance agent developers. Not used in evaluation prompts.' ), + NoExplicitNull, ] = None exemplars: Annotated[ Exemplars | None, Field( description='Calibration examples for governance agents, following the Content Standards pattern.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/policy_ref.py b/src/adcp/types/domains/governance/policy_ref.py index cf714b4c3..3021b0f21 100644 --- a/src/adcp/types/domains/governance/policy_ref.py +++ b/src/adcp/types/domains/governance/policy_ref.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: governance/policy_ref.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -25,10 +25,12 @@ class PolicyReference(AdCPBaseModel): Field( description='Pin a specific policy version (semver). If omitted, the current version is used.' ), + NoExplicitNull, ] = None config: Annotated[ dict[str, Any] | None, Field( description="Brand-specific parameter overrides for configurable policies. The accepted shape depends on the policy's config_schema." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/governance/report_plan_adjustment_request.py b/src/adcp/types/domains/governance/report_plan_adjustment_request.py index fcb5eeb10..d77016249 100644 --- a/src/adcp/types/domains/governance/report_plan_adjustment_request.py +++ b/src/adcp/types/domains/governance/report_plan_adjustment_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/report_plan_adjustment_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -95,15 +95,19 @@ class ReportPlanAdjustmentRequest(AdcpRequest, AdcpVersionEnvelope): description='Completed outcome whose authoritative commitment is being adjusted.', min_length=1, ), + NoExplicitNull, ] = None adjustment_id: Annotated[ - str | None, Field(description='Adjustment to accept or dispute. Required for review.') + str | None, + Field(description='Adjustment to accept or dispute. Required for review.'), + NoExplicitNull, ] = None decision: Annotated[ Decision | None, Field( description='Buyer review decision. Acceptance is blocked while delivery evidence for the governed action is disputed in an open governance period; a historical closed_unresolved period is audit evidence, not a billing determination.' ), + NoExplicitNull, ] = None seller_reference: Annotated[ str | None, @@ -112,6 +116,7 @@ class ReportPlanAdjustmentRequest(AdcpRequest, AdcpVersionEnvelope): max_length=255, min_length=1, ), + NoExplicitNull, ] = None seller_adjustment_id: Annotated[ str | None, @@ -120,15 +125,19 @@ class ReportPlanAdjustmentRequest(AdcpRequest, AdcpVersionEnvelope): max_length=255, min_length=1, ), + NoExplicitNull, ] = None adjustment_type: Annotated[ AdjustmentType | None, Field( description='Commercial meaning. Verified decommitments restore headroom in every mode; verified refunds and credits restore it only in verified_net_cost mode; makegoods never restore cash headroom.' ), + NoExplicitNull, ] = None amount: Annotated[ - Amount | None, Field(description='Positive adjustment amount in the plan currency.') + Amount | None, + Field(description='Positive adjustment amount in the plan currency.'), + NoExplicitNull, ] = None reason: Annotated[ str | None, @@ -137,15 +146,19 @@ class ReportPlanAdjustmentRequest(AdcpRequest, AdcpVersionEnvelope): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None effective_at: Annotated[ - AwareDatetime | None, Field(description="When the seller's adjustment became effective.") + AwareDatetime | None, + Field(description="When the seller's adjustment became effective."), + NoExplicitNull, ] = None evidence: Annotated[ Evidence | None, Field( description='Integrity-bound commercial source record supplied by the seller. Its evidence_type must correspond to adjustment_type.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -156,5 +169,5 @@ class ReportPlanAdjustmentRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/report_plan_adjustment_response.py b/src/adcp/types/domains/governance/report_plan_adjustment_response.py index 2ce2ee41d..1b5b443e7 100644 --- a/src/adcp/types/domains/governance/report_plan_adjustment_response.py +++ b/src/adcp/types/domains/governance/report_plan_adjustment_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/report_plan_adjustment_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..core import context as context_1 @@ -86,6 +86,7 @@ class ReportPlanAdjustmentResponse(AdcpResponse, AdcpVersionEnvelope): Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/report_plan_outcome_request.py b/src/adcp/types/domains/governance/report_plan_outcome_request.py index 9ce76b49d..053d3bf36 100644 --- a/src/adcp/types/domains/governance/report_plan_outcome_request.py +++ b/src/adcp/types/domains/governance/report_plan_outcome_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/report_plan_outcome_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import context as context_1 @@ -25,7 +25,7 @@ class Package(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - budget: Annotated[StrictFloat | None, Field(ge=0.0)] = None + budget: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None class Source(StrEnum): @@ -65,6 +65,7 @@ class Delivery(AdCPBaseModel): Field( description='Seller statement identifier as received by the buyer. Required for seller_statement_copy.' ), + NoExplicitNull, ] = None seller_statement_digest: Annotated[ str | None, @@ -72,6 +73,7 @@ class Delivery(AdCPBaseModel): description='Digest received with the seller statement. Governance compares it to the seller-authenticated canonical digest.', pattern='^sha256:[a-f0-9]{64}$', ), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod, Field(description='Start and end timestamps for the reporting window.') @@ -86,8 +88,9 @@ class Delivery(AdCPBaseModel): Field( description='Buyer-declared operational closure of this governance reporting period. When true, governance freezes the period after recording this observation. An unresolved mismatch becomes closed_unresolved and no longer blocks later governance. Closure is not final billing or commercial settlement. Only the authenticated plan owner may set this to true; the governance agent MUST reject it from any other authenticated reporter.' ), + NoExplicitNull, ] = False - impressions: Annotated[SchemaInt | None, Field(ge=0)] = None + impressions: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class SellerResponse(AdCPBaseModel): @@ -100,6 +103,7 @@ class SellerResponse(AdCPBaseModel): description="The seller's identifier for the created resource (e.g., media_buy_id, rights_grant_id, deployment_id). Not interpreted by the governance agent — included in audit logs for human-readable traceability alongside the opaque governance_context.", max_length=255, ), + NoExplicitNull, ] = None committed_budget: Annotated[ StrictFloat | None, @@ -107,19 +111,24 @@ class SellerResponse(AdCPBaseModel): description='Buyer-reported seller amount retained for reconciliation and audit. It is never ledger authority: the governance agent derives the reserved commitment from its own approved intent check, or from the matching purchase execution check when one exists. A report above that authorized amount is rejected; a lower report does not restore headroom.', ge=0.0, ), + NoExplicitNull, ] = None packages: Annotated[ list[Package] | None, Field(description='Confirmed packages with actual budget and targeting.'), + NoExplicitNull, ] = None planned_delivery: Annotated[ planned_delivery_1.PlannedDelivery | None, Field( description="What the seller said it will deliver. When seller-side governance is not configured, this is the governance agent's only view of the seller's delivery parameters." ), + NoExplicitNull, ] = None creative_deadline: Annotated[ - AwareDatetime | None, Field(description='ISO 8601 deadline for creative submission.') + AwareDatetime | None, + Field(description='ISO 8601 deadline for creative submission.'), + NoExplicitNull, ] = None @@ -138,6 +147,7 @@ class ReportPlanOutcomeRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='The check_id from check_governance. Required for completed and failed outcomes and for buyer delivery observations. A delivery observation names the exact seller delivery check whose canonical statement is being compared.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -153,23 +163,27 @@ class ReportPlanOutcomeRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="The type of financial commitment this outcome is for. Must equal the original approved intent's purchase_type. Determines which budget allocation (if any) to charge against. Defaults to 'media_buy' when omitted." ), + NoExplicitNull, ] = purchase_type_1.PurchaseType.media_buy outcome: Annotated[outcome_type.OutcomeType, Field(description='Outcome type.')] seller_response: Annotated[ SellerResponse | None, Field(description="The seller's full response. Required when outcome is 'completed'."), + NoExplicitNull, ] = None delivery: Annotated[ Delivery | None, Field( description='Buyer-attributed observation compared with the canonical seller delivery statement identified by check_id. This evidence never overwrites seller evidence or creates a second commitment. A conflict produces an explicit disputed reconciliation state while the operational period is open; the plan owner may close it without asserting final billing truth.' ), + NoExplicitNull, ] = None error: Annotated[ reported_outcome_error.ReportedOutcomeError | None, Field( description='Buyer-attributed error associated with a failed seller interaction. Required when outcome is failed; classification_source=seller_response_copy preserves what the buyer received without claiming seller-attested provenance.' ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -179,6 +193,7 @@ class ReportPlanOutcomeRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/report_plan_outcome_response.py b/src/adcp/types/domains/governance/report_plan_outcome_response.py index b58d97148..986fd42de 100644 --- a/src/adcp/types/domains/governance/report_plan_outcome_response.py +++ b/src/adcp/types/domains/governance/report_plan_outcome_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/report_plan_outcome_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator, StrictBool, StrictFloat from ..core import context as context_1 @@ -46,7 +46,9 @@ class Finding(AdCPBaseModel): ] explanation: Annotated[str, Field(description='Human-readable description of the issue.')] details: Annotated[ - dict[str, Any] | None, Field(description='Structured details for programmatic consumption.') + dict[str, Any] | None, + Field(description='Structured details for programmatic consumption.'), + NoExplicitNull, ] = None @@ -57,9 +59,12 @@ class PlanSummary(AdCPBaseModel): total_committed: Annotated[ StrictFloat | None, Field(description='Total budget committed across all campaigns in the plan.'), + NoExplicitNull, ] = None budget_remaining: Annotated[ - StrictFloat | None, Field(description='Authorized budget minus total committed.') + StrictFloat | None, + Field(description='Authorized budget minus total committed.'), + NoExplicitNull, ] = None @@ -87,34 +92,40 @@ def _status_to_outcome_state(cls, data: Any) -> Any: Field( description="Budget committed from this outcome. Present for 'completed' and 'failed' outcomes." ), + NoExplicitNull, ] = None delivery_reconciliation_status: Annotated[ DeliveryReconciliationStatus | None, Field( description='Comparison state between the buyer-attributed observation and canonical seller statement. Present for delivery outcomes. A disputed state blocks adjustment verification while the period is open. measurement_variance records a buyer-measured cumulative_spend that differs from the seller statement; it never blocks verification — the higher amount bounds conservative exposure and decommitments instead. closed_unresolved preserves the discrepancy after operational closure without claiming a final billing result.' ), + NoExplicitNull, ] = None delivery_period_state: Annotated[ DeliveryPeriodState | None, Field( description='Operational state of the governance reporting period. Closure freezes governance evidence for that period but is not billing settlement.' ), + NoExplicitNull, ] = None findings: Annotated[ list[Finding] | None, Field(description="Issues detected. Present only when outcome_state is 'findings'."), + NoExplicitNull, ] = None plan_summary: Annotated[ PlanSummary | None, Field( description="Updated plan budget state. Present for 'completed' and 'failed' outcomes." ), + NoExplicitNull, ] = None replayed: Annotated[ StrictBool | None, Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/reported_outcome_error.py b/src/adcp/types/domains/governance/reported_outcome_error.py index 38574a17f..4a76d4dba 100644 --- a/src/adcp/types/domains/governance/reported_outcome_error.py +++ b/src/adcp/types/domains/governance/reported_outcome_error.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: governance/reported_outcome_error.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Dict -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, RootModel, StrictBool, StrictFloat, StringConstraints @@ -74,24 +74,27 @@ class ReportedOutcomeError(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - code: Annotated[str | None, Field(max_length=64, min_length=1)] = None - message: Annotated[str | None, Field(max_length=4000)] = None - field: Annotated[str | None, Field(max_length=1000)] = None - suggestion: Annotated[str | None, Field(max_length=4000)] = None - recovery: Recovery | None = None + code: Annotated[str | None, Field(max_length=64, min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=4000), NoExplicitNull] = None + field: Annotated[str | None, Field(max_length=1000), NoExplicitNull] = None + suggestion: Annotated[str | None, Field(max_length=4000), NoExplicitNull] = None + recovery: Annotated[Recovery | None, NoExplicitNull] = None details: Annotated[ BoundedObject | None, Field( description='Bounded structured seller error details. Values remain reporter-supplied data and MUST NOT be promoted into prompts or control instructions without isolation.' ), + NoExplicitNull, ] = None classification_source: Annotated[ ClassificationSource | None, Field( description='seller_response_copy means the buyer forwards what it received; it is still not independently authenticated seller evidence.' ), + NoExplicitNull, ] = None ext: Annotated[ BoundedObject | None, Field(description='Bounded extension envelope for forward-compatible seller fields.'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/governance/sync_plans_request.py b/src/adcp/types/domains/governance/sync_plans_request.py index cafe37117..bbc9df3e5 100644 --- a/src/adcp/types/domains/governance/sync_plans_request.py +++ b/src/adcp/types/domains/governance/sync_plans_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/sync_plans_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import brand_ref @@ -31,7 +31,9 @@ class Allocations(AdCPBaseModel): extra='forbid', ) amount: Annotated[ - StrictFloat | None, Field(description='Maximum budget for this purchase type.', ge=0.0) + StrictFloat | None, + Field(description='Maximum budget for this purchase type.', ge=0.0), + NoExplicitNull, ] = None max_pct: Annotated[ StrictFloat | None, @@ -40,6 +42,7 @@ class Allocations(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None @@ -54,10 +57,12 @@ class Budget(AdCPBaseModel): Field( description='Controls which verified adjustments restore reusable plan headroom. gross_commitment allows verified decommitments only; verified_net_cost also allows buyer-accepted refunds and credits. Both modes retain the original commitment in anti-fragmentation history and expose economic net cost separately.' ), + NoExplicitNull, ] = AccountingMode.gross_commitment per_seller_max_pct: Annotated[ StrictFloat | None, Field(description='Maximum percentage of budget that can go to a single seller.'), + NoExplicitNull, ] = None reallocation_threshold: Annotated[ StrictFloat, @@ -71,12 +76,14 @@ class Budget(AdCPBaseModel): Field( description='Set to true to allow the orchestrator to reallocate without any limit up to `total`. Mutually exclusive with `reallocation_threshold`. Use this for deliberate full-autonomy declarations rather than setting `reallocation_threshold: total` (which silently tightens when `total` changes).' ), + NoExplicitNull, ] = None allocations: Annotated[ dict[purchase_type.PurchaseType, Allocations] | None, Field( description='Optional budget partition across purchase types. Keys are purchase-type enum values (media_buy, rights_license, signal_activation, creative_services). When present, the governance agent validates spend against both the total and the per-type allocation. When absent, all spend counts against the single total regardless of purchase type.' ), + NoExplicitNull, ] = None @@ -91,10 +98,12 @@ class Budget2(AdCPBaseModel): Field( description='Controls which verified adjustments restore reusable plan headroom. gross_commitment allows verified decommitments only; verified_net_cost also allows buyer-accepted refunds and credits. Both modes retain the original commitment in anti-fragmentation history and expose economic net cost separately.' ), + NoExplicitNull, ] = AccountingMode.gross_commitment per_seller_max_pct: Annotated[ StrictFloat | None, Field(description='Maximum percentage of budget that can go to a single seller.'), + NoExplicitNull, ] = None reallocation_threshold: Annotated[ StrictFloat | None, @@ -102,6 +111,7 @@ class Budget2(AdCPBaseModel): description='Amount above which budget reallocations require human escalation. The orchestrator can reallocate spend across sellers, channels, or purchase types up to this threshold per change without asking a human. Set equal to `total` for effectively unlimited reallocation; set to 0 to require approval for every reallocation. Separate from `plan.human_review_required`, which governs decisions affecting data subjects (targeting, creative, delivery) under GDPR Art 22 / EU AI Act Annex III. Denominated in `budget.currency`.', ge=0.0, ), + NoExplicitNull, ] = None reallocation_unlimited: Annotated[ Literal[True], @@ -114,6 +124,7 @@ class Budget2(AdCPBaseModel): Field( description='Optional budget partition across purchase types. Keys are purchase-type enum values (media_buy, rights_license, signal_activation, creative_services). When present, the governance agent validates spend against both the total and the per-type allocation. When absent, all spend counts against the single total regardless of purchase type.' ), + NoExplicitNull, ] = None @@ -121,8 +132,8 @@ class MixTargets(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min_pct: StrictFloat | None = None - max_pct: StrictFloat | None = None + min_pct: Annotated[StrictFloat | None, NoExplicitNull] = None + max_pct: Annotated[StrictFloat | None, NoExplicitNull] = None class Channels(AdCPBaseModel): @@ -132,14 +143,17 @@ class Channels(AdCPBaseModel): required: Annotated[ list[channels_1.MediaChannel] | None, Field(description='Channels that must be included in the media mix.'), + NoExplicitNull, ] = None allowed: Annotated[ list[channels_1.MediaChannel] | None, Field(description='Channels the orchestrator may use.'), + NoExplicitNull, ] = None mix_targets: Annotated[ dict[str, MixTargets] | None, Field(description='Target allocation ranges per channel, keyed by channel ID.'), + NoExplicitNull, ] = None @@ -173,18 +187,21 @@ class Delegation(AdCPBaseModel): Field( description="Maximum budget this agent can commit. When omitted, the agent can commit up to the plan's total budget." ), + NoExplicitNull, ] = None markets: Annotated[ list[str] | None, Field( description='ISO 3166-1/3166-2 codes this agent is authorized for. When omitted, the agent can operate in all plan markets.' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='When this delegation expires. After expiration, the governance agent denies actions from this agent.' ), + NoExplicitNull, ] = None @@ -205,18 +222,21 @@ class Portfolio(AdCPBaseModel): total_budget_cap: Annotated[ TotalBudgetCap | None, Field(description='Maximum aggregate budget across all member plans.'), + NoExplicitNull, ] = None shared_policy_ids: Annotated[ list[str] | None, Field( description='Registry policy IDs enforced across all member plans, regardless of individual brand configuration.' ), + NoExplicitNull, ] = None shared_exclusions: Annotated[ list[policy_entry.PolicyEntry] | None, Field( description="Bespoke exclusion policies applied across all member plans, using the same shape as registry entries. Authored typically as enforcement: must policies with exclusion language in the policy text (e.g., 'No advertising on properties owned by competitor holding companies')." ), + NoExplicitNull, ] = None @@ -242,6 +262,7 @@ class Plan(AdCPBaseModel): channels: Annotated[ Channels | None, Field(description='Channel constraints. If omitted, all channels are allowed.'), + NoExplicitNull, ] = None flight: Annotated[ Flight, @@ -255,6 +276,7 @@ class Plan(AdCPBaseModel): description="ISO 3166-1 alpha-2 country codes for authorized markets (e.g., ['US', 'GB']). The governance agent rejects governed actions targeting outside these countries and resolves applicable policies by matching against policy jurisdictions.", min_length=1, ), + NoExplicitNull, ] = None regions: Annotated[ list[str] | None, @@ -262,12 +284,14 @@ class Plan(AdCPBaseModel): description="ISO 3166-2 subdivision codes for authorized sub-national markets (e.g., ['US-MA', 'US-CA']). When present, the governance agent restricts governed actions to these specific regions rather than the full country. Use for plans limited to specific states or provinces (e.g., cannabis in legal states). Policy resolution matches against both the subdivision and its parent country.", min_length=1, ), + NoExplicitNull, ] = None policy_ids: Annotated[ list[str] | None, Field( description="Registry policy IDs to enforce for this plan. The governance agent resolves full policy definitions from the registry and evaluates actions against them. Intersected with the plan's countries/regions to activate only geographically relevant policies." ), + NoExplicitNull, ] = None policy_categories: Annotated[ list[str] | None, @@ -275,12 +299,14 @@ class Plan(AdCPBaseModel): description="Regulatory categories that apply to this campaign. Determines which policy regimes the governance agent enforces (e.g., 'children_directed' activates COPPA/AADC, 'political_advertising' activates disclosure requirements). The governance agent resolves categories to specific policies based on the plan's jurisdictions. When omitted, governance agents MAY infer categories from the brand's industries and the plan's objectives. Values are registry-defined category IDs (intentionally freeform strings, not an enum — new categories are added as regulations evolve).", min_length=1, ), + NoExplicitNull, ] = None audience: Annotated[ audience_constraints.AudienceConstraints | None, Field( description='Audience targeting constraints. Defines who the campaign should reach (include) and must not reach (exclude). The governance agent evaluates seller targeting against these constraints.' ), + NoExplicitNull, ] = None restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, @@ -288,6 +314,7 @@ class Plan(AdCPBaseModel): description='Personal data categories that must not be used for targeting in this campaign. Applies horizontally across all audience criteria. Used for EU DSA Article 26 compliance (prohibits targeting on GDPR Article 9 special categories) and similar regulations. The governance agent flags any audience targeting that references these attributes.', min_length=1, ), + NoExplicitNull, ] = None restricted_attributes_custom: Annotated[ list[str] | None, @@ -295,6 +322,7 @@ class Plan(AdCPBaseModel): description="Additional restricted attributes not covered by the restricted-attribute enum. Freeform strings for jurisdiction-specific or brand-specific restrictions beyond GDPR Article 9 categories (e.g., 'financial_status', 'immigration_status'). Governance agents use semantic matching for these.", min_length=1, ), + NoExplicitNull, ] = None min_audience_size: Annotated[ SchemaInt | None, @@ -302,18 +330,21 @@ class Plan(AdCPBaseModel): description='Minimum audience segment size. Prevents micro-targeting by ensuring segments meet a k-anonymity threshold. Applies to the estimated combined (intersection) audience when multiple criteria are used, not just individual criterion sizes. The governance agent validates this by querying published signal metadata or seller-reported segment sizes. When segment size data is unavailable, the governance agent SHOULD produce a finding with reduced confidence rather than silently passing.', ge=1, ), + NoExplicitNull, ] = None human_review_required: Annotated[ StrictBool | None, Field( description='When true, the governance agent MUST escalate any action on this plan for human review before execution — regardless of budget. Required by regulations that prohibit solely automated decisions affecting data subjects (GDPR Art 22, EU AI Act Annex III): credit, insurance pricing, recruitment, housing allocation. Governance agents MUST set this to true automatically when any resolved policy or policy_category has requires_human_review: true. Distinct from budget.reallocation_threshold, which only governs budget reallocation autonomy — human_review_required covers decisions affecting individuals (targeting, creative, delivery).' ), + NoExplicitNull, ] = False custom_policies: Annotated[ list[policy_entry.PolicyEntry] | None, Field( description='Bespoke policies specific to this campaign, using the same shape as registry entries. Each policy has a policy_id, enforcement (must|should), and natural-language policy text. Governance findings reference policy_id to identify which policy triggered. For quick authoring, omit version/name/category — servers default them.' ), + NoExplicitNull, ] = None approved_sellers: Annotated[ list[AnyUrl] | None, @@ -324,14 +355,16 @@ class Plan(AdCPBaseModel): Field( description="Agents authorized to execute against this plan. Each delegation scopes an agent's authority by budget, markets, and expiration. The governance agent validates that the requesting agent matches a delegation before approving actions." ), + NoExplicitNull, ] = None portfolio: Annotated[ Portfolio | None, Field( description='Portfolio-level governance constraints. When present, this plan acts as a portfolio plan that governs member plans. Portfolio plans define cross-brand constraints that no individual brand plan can override.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class SyncPlansRequest(AdcpRequest, AdcpVersionEnvelope): @@ -348,5 +381,5 @@ class SyncPlansRequest(AdcpRequest, AdcpVersionEnvelope): ), ] plans: Annotated[list[Plan], Field(description='One or more campaign plans to sync.')] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/governance/sync_plans_response.py b/src/adcp/types/domains/governance/sync_plans_response.py index 09558cb01..032f79543 100644 --- a/src/adcp/types/domains/governance/sync_plans_response.py +++ b/src/adcp/types/domains/governance/sync_plans_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/sync_plans_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -62,6 +62,7 @@ class ResolvedPolicy(AdCPBaseModel): Field( description="Why this policy was included (e.g., 'Matched jurisdiction US and policy category pharmaceutical_advertising')." ), + NoExplicitNull, ] = None @@ -78,13 +79,16 @@ class Plan(AdCPBaseModel): ] version: Annotated[SchemaInt, Field(description='Plan version (increments on each sync).')] categories: Annotated[ - list[Category] | None, Field(description='Validation categories active for this plan.') + list[Category] | None, + Field(description='Validation categories active for this plan.'), + NoExplicitNull, ] = None resolved_policies: Annotated[ list[ResolvedPolicy] | None, Field( description='Policies the governance agent will enforce for this plan. Includes explicitly referenced policies from the brand compliance configuration and auto-applied policies matched by jurisdiction or policy category. Present when the governance agent supports policy resolution.' ), + NoExplicitNull, ] = None @@ -98,6 +102,7 @@ class SyncPlansResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/manifest_schema.py b/src/adcp/types/domains/manifest_schema.py index 0f4783a5e..e2148cfd2 100644 --- a/src/adcp/types/domains/manifest_schema.py +++ b/src/adcp/types/domains/manifest_schema.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: manifest.schema.json -# timestamp: 2026-10-04T18:54:18+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictBool @@ -85,6 +85,7 @@ class ErrorCodes(AdCPBaseModel): added_in: Annotated[ str | None, Field(description='Semver of the AdCP release that introduced this code. Optional.'), + NoExplicitNull, ] = None @@ -101,6 +102,7 @@ class Specialisms(AdCPBaseModel): title: Annotated[ str | None, Field(description="Human-readable title. Sourced from index.yaml's `title` field."), + NoExplicitNull, ] = None entry_point_tools: Annotated[ list[str], @@ -163,7 +165,9 @@ class LegacyFallback2(AdCPBaseModel): extra='forbid', ) tool: Annotated[ - ToolName | None, Field(description='Legacy peer tool targeted by the adapter.') + ToolName | None, + Field(description='Legacy peer tool targeted by the adapter.'), + NoExplicitNull, ] = None mode: Annotated[ Literal['none'], @@ -196,18 +200,21 @@ class Tools(AdCPBaseModel): max_length=240, min_length=1, ), + NoExplicitNull, ] = None operation_family: Annotated[ str | None, Field( description='Stable logical operation identity used for authorization, idempotency equivalence, task recovery, and webhook identity. Aliases in the same family share this value.' ), + NoExplicitNull, ] = None idempotency_requirement: Annotated[ IdempotencyRequirement | None, Field( description='Whether callers must, may, or cannot supply idempotency_key for this tool. Optional keys, when supplied, receive the same replay guarantees as required keys.' ), + NoExplicitNull, ] = None request_schema: SchemaPath response_schema: SchemaPath @@ -223,6 +230,7 @@ class Tools(AdCPBaseModel): description="How an SDK exposing this tool's current API may satisfy it against a peer that only exposes a legacy tool. This is caller-side adaptation metadata, not authorization or idempotency equivalence.", discriminator='mode', ), + NoExplicitNull, ] = None superseded_by: Annotated[ list[ToolName] | None, @@ -230,24 +238,28 @@ class Tools(AdCPBaseModel): description='Current tool names that replace this deprecated facade. This does not imply that each replacement is a one-call alias.', min_length=1, ), + NoExplicitNull, ] = None specialisms: Annotated[ list[str] | None, Field( description='Specialism IDs that include this tool in their required_tools (or via inherited scenarios). Lets SDKs answer "if I claim specialism X, which tools must I implement?" without re-deriving from the compliance cache.' ), + NoExplicitNull, ] = None added_in: Annotated[ str | None, Field( description='Semver of the AdCP release that introduced this tool. Optional; absent means "present since 1.0".' ), + NoExplicitNull, ] = None deprecated_in: Annotated[ str | None, Field( description='Semver of the AdCP release that deprecated this tool. Absent means active.' ), + NoExplicitNull, ] = None @@ -270,7 +282,9 @@ class AdcpManifest(AdCPBaseModel): extra='forbid', ) field_schema: Annotated[ - str | None, Field(alias='$schema', description='Reference to this manifest meta-schema.') + str | None, + Field(alias='$schema', description='Reference to this manifest meta-schema.'), + NoExplicitNull, ] = None adcp_version: Annotated[ str, diff --git a/src/adcp/types/domains/media_buy/accept_proposal_request.py b/src/adcp/types/domains/media_buy/accept_proposal_request.py index 5c5c68266..2096c59c6 100644 --- a/src/adcp/types/domains/media_buy/accept_proposal_request.py +++ b/src/adcp/types/domains/media_buy/accept_proposal_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/accept_proposal_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..core import canonical_account_ref @@ -36,7 +36,7 @@ class IoAcceptance(AdCPBaseModel): io_id: Annotated[str, Field(min_length=1)] accepted_at: AwareDatetime signatory: Annotated[str, Field(max_length=250, min_length=1)] - signature_id: Annotated[str | None, Field(min_length=1)] = None + signature_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class Status(StrEnum): @@ -64,6 +64,7 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -73,6 +74,7 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$') @@ -85,6 +87,7 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None account: canonical_account_ref.CanonicalAccountReference proposal_id: Annotated[str, Field(min_length=1)] @@ -100,6 +103,7 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Execution amount when the committed proposal defines scalable percentages or constraints rather than a fixed total.' ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -107,6 +111,7 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): description="Optional hard aggregate daily spend ceiling applied when the committed proposal is accepted. It constrains execution without changing the proposal's negotiated pricing.", ge=0.0, ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, @@ -114,22 +119,31 @@ class AcceptProposalRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional shared IANA cap-day timezone override. Requires buyer_timezone_override support. When omitted, budget_capping.timezone_basis selects Account.timezone or fixed_timezone.', min_length=1, ), + NoExplicitNull, + ] = None + io_acceptance: Annotated[IoAcceptance | None, NoExplicitNull] = None + purchase_order_ref: Annotated[ + str | None, Field(max_length=255, min_length=1), NoExplicitNull + ] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - io_acceptance: IoAcceptance | None = None - purchase_order_ref: Annotated[str | None, Field(max_length=255, min_length=1)] = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None reporting_webhook: Annotated[ reporting_webhook_1.ReportingWebhook | None, Field( description='Optional reporting delivery configuration established atomically when the proposal is accepted. This is execution metadata and does not alter the accepted commercial terms digest.' ), + NoExplicitNull, ] = None opportunity: Annotated[ Opportunity | None, Field( description='Optional planning-cycle closure. Success infers closed with accepted_with_seller when status is omitted. If the proposal carries opportunity_id, a supplied ID MUST match; the accepted proposal preserves that association.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/accept_proposal_response.py b/src/adcp/types/domains/media_buy/accept_proposal_response.py index 6f82cf86b..bdee58017 100644 --- a/src/adcp/types/domains/media_buy/accept_proposal_response.py +++ b/src/adcp/types/domains/media_buy/accept_proposal_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/accept_proposal_response.json -# timestamp: 2026-10-04T04:34:38+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import canonical_media_buy_action @@ -54,9 +54,9 @@ class AcceptProposalResponse2(AdCPBaseModel): ) status: Literal['failed'] = 'failed' errors: Annotated[list[error.Error], Field(min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class AcceptProposalResponse3(AdCPBaseModel): @@ -65,11 +65,11 @@ class AcceptProposalResponse3(AdCPBaseModel): ) status: Literal['submitted'] = 'submitted' task_id: Annotated[str, Field(min_length=1)] - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class AcceptProposalResponse6(AdcpResponse, AcceptProposalResponse2, AcceptProposalResponse4): @@ -87,6 +87,7 @@ class Warning(Warning_1): description='Closed warning vocabulary for the negotiated AdCP release. A non-blocking condition observed while an operation still succeeded. New values are added through normal protocol releases; buyers validate against the negotiated release.', title='Warning Code', ), + NoExplicitNull, ] = None @@ -118,9 +119,10 @@ class AcceptProposalResponse1(AdCPBaseModel): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None revision: Annotated[SchemaInt, Field(ge=1)] - media_buy_status: media_buy_status_1.MediaBuyStatus | None = None + media_buy_status: Annotated[media_buy_status_1.MediaBuyStatus | None, NoExplicitNull] = None confirmed_at: AwareDatetime | None = None accepted_proposal: AcceptedProposal purchase_bindings: Annotated[ @@ -137,10 +139,11 @@ class AcceptProposalResponse1(AdCPBaseModel): description='Non-blocking observations about this completed commitment. The MediaBuy was still created or amended exactly as represented. Continuing conditions also appear as indicators on get_media_buys.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class AcceptProposalResponse5(AdcpResponse, AcceptProposalResponse1, AcceptProposalResponse4): diff --git a/src/adcp/types/domains/media_buy/acceptance_context.py b/src/adcp/types/domains/media_buy/acceptance_context.py index 82430b235..b6f255279 100644 --- a/src/adcp/types/domains/media_buy/acceptance_context.py +++ b/src/adcp/types/domains/media_buy/acceptance_context.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/acceptance_context.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -24,7 +24,7 @@ class Subject(AdCPBaseModel): extra='forbid', ) subject_category: Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')] - subject_facets: Annotated[list[SubjectFacet] | None, Field(min_length=1)] = None + subject_facets: Annotated[list[SubjectFacet] | None, Field(min_length=1), NoExplicitNull] = None class AdvertiserRole(SubjectFacet): @@ -44,15 +44,20 @@ class AcceptanceContext(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - subjects: Annotated[list[Subject] | None, Field(min_length=1)] = None - advertiser_roles: Annotated[list[AdvertiserRole] | None, Field(min_length=1)] = None - advertiser_industry: advertiser_industry_1.AdvertiserIndustry | None = None + subjects: Annotated[list[Subject] | None, Field(min_length=1), NoExplicitNull] = None + advertiser_roles: Annotated[ + list[AdvertiserRole] | None, Field(min_length=1), NoExplicitNull + ] = None + advertiser_industry: Annotated[ + advertiser_industry_1.AdvertiserIndustry | None, NoExplicitNull + ] = None advertiser_jurisdictions: Annotated[ list[AdvertiserJurisdiction] | None, Field( description='Jurisdictions in which the advertiser is established or legally organized. This is distinct from where an ad will be delivered.', min_length=1, ), + NoExplicitNull, ] = None delivery_jurisdictions: Annotated[ list[DeliveryJurisdiction] | None, @@ -60,5 +65,6 @@ class AcceptanceContext(AdCPBaseModel): description="Jurisdictions in which the proposed advertising will be delivered. Seller acceptance rules' jurisdictions and jurisdiction_groups match this field.", min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/acceptance_policy_catalog.py b/src/adcp/types/domains/media_buy/acceptance_policy_catalog.py index 26446d982..44f2aa74a 100644 --- a/src/adcp/types/domains/media_buy/acceptance_policy_catalog.py +++ b/src/adcp/types/domains/media_buy/acceptance_policy_catalog.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/acceptance_policy_catalog.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, model_validator from ..core import ext as ext_1 @@ -21,9 +21,11 @@ class AcceptancePolicyCatalog(AdCPBaseModel): extra='forbid', ) catalog_version: Annotated[str, Field(min_length=1)] - generated_at: AwareDatetime | None = None + generated_at: Annotated[AwareDatetime | None, NoExplicitNull] = None profiles: Annotated[ - list[acceptance_policy_profile.AcceptancePolicyProfile] | None, Field(min_length=1) + list[acceptance_policy_profile.AcceptancePolicyProfile] | None, + Field(min_length=1), + NoExplicitNull, ] = None registry_profiles: Annotated[ list[acceptance_policy_profile_ref.RegistryAcceptancePolicyProfileReference] | None, @@ -31,8 +33,9 @@ class AcceptancePolicyCatalog(AdCPBaseModel): description='Exact reusable profiles adopted from the shared policy registry. Resolution failure is unknown, never allowed. A seller adds a distinct local profile to narrow a registry profile.', min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> AcceptancePolicyCatalog: diff --git a/src/adcp/types/domains/media_buy/acceptance_policy_profile.py b/src/adcp/types/domains/media_buy/acceptance_policy_profile.py index 63d64d68b..51c7f9cb6 100644 --- a/src/adcp/types/domains/media_buy/acceptance_policy_profile.py +++ b/src/adcp/types/domains/media_buy/acceptance_policy_profile.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/acceptance_policy_profile.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T12:09:21+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StringConstraints @@ -66,16 +66,18 @@ class Scope(AdCPBaseModel): ) subject_categories: Annotated[list[SubjectCategory], Field(min_length=1)] applies_to: Annotated[list[AppliesToEnum], Field(min_length=1)] - jurisdictions: Annotated[list[Jurisdiction] | None, Field(min_length=1)] = None + jurisdictions: Annotated[list[Jurisdiction] | None, Field(min_length=1), NoExplicitNull] = None jurisdiction_groups: Annotated[ list[JurisdictionGroup] | None, Field(description="Named groups declared in this profile's region_aliases.", min_length=1), + NoExplicitNull, ] = None all_jurisdictions: Annotated[ Literal[True] | None, Field( description='Explicitly states that the scope is not limited by delivery jurisdiction.' ), + NoExplicitNull, ] = None @@ -114,13 +116,15 @@ class AcceptancePolicyProfile(AdCPBaseModel): Field( description='The boundary within which a complete profile claims exhaustiveness. It is informative for partial profiles and mandatory for complete profiles.' ), + NoExplicitNull, ] = None region_aliases: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z][A-Z0-9_-]*$')], list[RegionAliase]] | None, Field( description='Profile-local named country groups. Rules may reference only keys declared here; consumers expand them before matching.' ), + NoExplicitNull, ] = None - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None rules: Annotated[list[acceptance_policy_rule.AcceptancePolicyRule], Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/acceptance_policy_requirement.py b/src/adcp/types/domains/media_buy/acceptance_policy_requirement.py index 978f25770..7715e6fad 100644 --- a/src/adcp/types/domains/media_buy/acceptance_policy_requirement.py +++ b/src/adcp/types/domains/media_buy/acceptance_policy_requirement.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/acceptance_policy_requirement.json -# timestamp: 2026-10-04T00:53:27+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -19,8 +19,8 @@ class AcceptancePolicyRequirement1(AdCPBaseModel): extra='forbid', ) kind: Literal['category_declaration'] = 'category_declaration' - declaration: Annotated[str | None, Field(min_length=1)] = None - description: Annotated[str | None, Field(min_length=1)] = None + declaration: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement2(AdCPBaseModel): @@ -28,8 +28,8 @@ class AcceptancePolicyRequirement2(AdCPBaseModel): extra='forbid', ) kind: Literal['advertiser_verification'] = 'advertiser_verification' - verification_scheme: Annotated[str | None, Field(min_length=1)] = None - description: Annotated[str | None, Field(min_length=1)] = None + verification_scheme: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class Criterion(ScalarStr): @@ -49,7 +49,7 @@ class AcceptancePolicyRequirement3(AdCPBaseModel): min_length=1, ), ] - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement4(AdCPBaseModel): @@ -64,7 +64,7 @@ class AcceptancePolicyRequirement4(AdCPBaseModel): min_length=1, ), ] - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement5(AdCPBaseModel): @@ -72,8 +72,8 @@ class AcceptancePolicyRequirement5(AdCPBaseModel): extra='forbid', ) kind: Literal['certification'] = 'certification' - credential: Annotated[str | None, Field(min_length=1)] = None - description: Annotated[str | None, Field(min_length=1)] = None + credential: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement6(AdCPBaseModel): @@ -81,8 +81,8 @@ class AcceptancePolicyRequirement6(AdCPBaseModel): extra='forbid', ) kind: Literal['license'] = 'license' - credential: Annotated[str | None, Field(min_length=1)] = None - description: Annotated[str | None, Field(min_length=1)] = None + credential: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement7(AdCPBaseModel): @@ -90,7 +90,7 @@ class AcceptancePolicyRequirement7(AdCPBaseModel): extra='forbid', ) kind: Literal['prior_authorization'] = 'prior_authorization' - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement8(AdCPBaseModel): @@ -98,7 +98,7 @@ class AcceptancePolicyRequirement8(AdCPBaseModel): extra='forbid', ) kind: Literal['account_setup'] = 'account_setup' - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement9(AdCPBaseModel): @@ -106,7 +106,7 @@ class AcceptancePolicyRequirement9(AdCPBaseModel): extra='forbid', ) kind: Literal['sales_assisted'] = 'sales_assisted' - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement10(AdCPBaseModel): @@ -114,9 +114,9 @@ class AcceptancePolicyRequirement10(AdCPBaseModel): extra='forbid', ) kind: Literal['disclosure'] = 'disclosure' - format: Annotated[str | None, Field(min_length=1)] = None - placement: Annotated[str | None, Field(min_length=1)] = None - description: Annotated[str | None, Field(min_length=1)] = None + format: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + placement: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement12(AdCPBaseModel): @@ -152,8 +152,9 @@ class AcceptancePolicyRequirement14(AdCPBaseModel): description='Deprecated in AdCP 3.2 and removed in AdCP 4.0. This named-format restriction is retained for 3.x compatibility; new policies identify canonical format options in description or an extension until a registry-stable format-option identity is standardized.', min_length=1, ), + NoExplicitNull, ] = None - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement15(AdCPBaseModel): @@ -161,9 +162,9 @@ class AcceptancePolicyRequirement15(AdCPBaseModel): extra='forbid', ) kind: Literal['time_restriction'] = 'time_restriction' - starts_at: AwareDatetime | None = None - ends_at: AwareDatetime | None = None - description: Annotated[str | None, Field(min_length=1)] = None + starts_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + ends_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement16(AdCPBaseModel): @@ -171,7 +172,7 @@ class AcceptancePolicyRequirement16(AdCPBaseModel): extra='forbid', ) kind: Literal['transparency_reporting'] = 'transparency_reporting' - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement11(AdCPBaseModel): @@ -180,9 +181,9 @@ class AcceptancePolicyRequirement11(AdCPBaseModel): ) kind: Literal['targeting_restriction'] = 'targeting_restriction' restricted_attributes: Annotated[ - list[restricted_attribute.RestrictedAttribute] | None, Field(min_length=1) + list[restricted_attribute.RestrictedAttribute] | None, Field(min_length=1), NoExplicitNull ] = None - description: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None class AcceptancePolicyRequirement17(AdCPBaseModel): @@ -192,7 +193,7 @@ class AcceptancePolicyRequirement17(AdCPBaseModel): kind: Literal['custom'] = 'custom' id: Annotated[str, Field(pattern='^[a-z][a-z0-9_.:-]*$')] description: Annotated[str, Field(min_length=1)] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None AcceptancePolicyRequirement = Annotated[ diff --git a/src/adcp/types/domains/media_buy/acceptance_policy_rule.py b/src/adcp/types/domains/media_buy/acceptance_policy_rule.py index 3186497ba..7832dfdd8 100644 --- a/src/adcp/types/domains/media_buy/acceptance_policy_rule.py +++ b/src/adcp/types/domains/media_buy/acceptance_policy_rule.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/acceptance_policy_rule.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -73,6 +73,7 @@ class AcceptancePolicyRule(AdCPBaseModel): description='Facet IDs defined by the selected policy category. Omission means the rule applies to every facet in the category.', min_length=1, ), + NoExplicitNull, ] = None advertiser_roles: Annotated[ list[AdvertiserRole] | None, @@ -80,6 +81,7 @@ class AcceptancePolicyRule(AdCPBaseModel): description='Registry-extensible roles such as political_actor, election_authority, government_entity, news_publisher, or commercial_advertiser.', min_length=1, ), + NoExplicitNull, ] = None jurisdictions: Annotated[ list[Jurisdiction] | None, @@ -87,6 +89,7 @@ class AcceptancePolicyRule(AdCPBaseModel): description='Delivery jurisdictions where this rule applies. Omission means every jurisdiction served by the seller.', min_length=1, ), + NoExplicitNull, ] = None jurisdiction_groups: Annotated[ list[JurisdictionGroup] | None, @@ -94,11 +97,14 @@ class AcceptancePolicyRule(AdCPBaseModel): description="Named country groups declared by the containing profile's region_aliases. Unknown group IDs invalidate the profile; they never match permissively.", min_length=1, ), + NoExplicitNull, ] = None applies_to: Annotated[list[AppliesToEnum], Field(min_length=1)] disposition: Disposition requirements: Annotated[ - list[acceptance_policy_requirement.AcceptancePolicyRequirement] | None, Field(min_length=1) + list[acceptance_policy_requirement.AcceptancePolicyRequirement] | None, + Field(min_length=1), + NoExplicitNull, ] = None policy_ids: Annotated[ list[PolicyId] | None, @@ -106,6 +112,7 @@ class AcceptancePolicyRule(AdCPBaseModel): description='Registry policies that define the exact obligations behind this coarse rule.', min_length=1, ), + NoExplicitNull, ] = None description: Annotated[ str | None, @@ -114,7 +121,8 @@ class AcceptancePolicyRule(AdCPBaseModel): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None - effective_at: AwareDatetime | None = None - expires_at: AwareDatetime | None = None - ext: ext_1.ExtensionObject | None = None + effective_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + expires_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/build_creative_async_response_input_required.py b/src/adcp/types/domains/media_buy/build_creative_async_response_input_required.py index 2114f2a69..b40857dac 100644 --- a/src/adcp/types/domains/media_buy/build_creative_async_response_input_required.py +++ b/src/adcp/types/domains/media_buy/build_creative_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/build_creative_async_response_input_required.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -26,13 +26,16 @@ class BuildCreativeInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional validation errors or warnings explaining why input is required.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/build_creative_async_response_submitted.py b/src/adcp/types/domains/media_buy/build_creative_async_response_submitted.py index d4f0712e5..90af4a5f1 100644 --- a/src/adcp/types/domains/media_buy/build_creative_async_response_submitted.py +++ b/src/adcp/types/domains/media_buy/build_creative_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/build_creative_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,12 +36,14 @@ class BuildCreativeSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Generative build queued; typical turnaround 3–5 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/build_creative_async_response_working.py b/src/adcp/types/domains/media_buy/build_creative_async_response_working.py index 58a3cfee1..7fcf897ea 100644 --- a/src/adcp/types/domains/media_buy/build_creative_async_response_working.py +++ b/src/adcp/types/domains/media_buy/build_creative_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/build_creative_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -18,17 +18,24 @@ class BuildCreativeWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step or phase of the operation (e.g., 'generating_assets', 'resolving_macros', 'rendering_preview')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/build_creative_request.py b/src/adcp/types/domains/media_buy/build_creative_request.py index 6a3c5a0f4..aeb9136ac 100644 --- a/src/adcp/types/domains/media_buy/build_creative_request.py +++ b/src/adcp/types/domains/media_buy/build_creative_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/build_creative_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -62,6 +62,7 @@ class SignalCondition4(AdCPBaseModel): Field( description="Optional opaque resolved-segment handle for this fan-out condition — the RESOLVED condition identity, distinct from signal_ref's DEFINITION identity. When get_signals or a product signal_targeting_options entry exposed a signal_agent_segment_id for the targeted signal, echo it here verbatim so the produced creative's signal_condition carries the resolved-segment identity that the trafficking-compatibility check (SIGNAL_TARGETING_INCOMPATIBLE) matches on exactly. Providers MAY namespace it (e.g. provider_a:weather:rain_realtime vs provider_b:precip:high) so cross-provider conditions stay distinct without a shared taxonomy registry; treat as opaque, do not parse the namespace for business logic. Prefer it over reconstructing condition identity from categorical values — categorical {signal_ref,value} identity is the weaker fallback, appropriate only for inherently-categorical signals with no resolved handle, and never a cross-provider equivalence claim." ), + NoExplicitNull, ] = None @@ -88,6 +89,7 @@ class VariantAxis(AdCPBaseModel): Field( description='The transformer `config` param `field` this axis sweeps. REQUIRED when `dimension` is `transformer_config`; `values[]` are then interpreted as values for `config[field]`. Omit for other dimensions.' ), + NoExplicitNull, ] = None values: Annotated[ list[Any] | None, @@ -95,10 +97,12 @@ class VariantAxis(AdCPBaseModel): description='Caller-fixed set of values for the axis (e.g. ["isaac", "sara"] for dimension `voice`). When present, len(values) is the variant count and is authoritative over max_variants.', min_length=1, ), + NoExplicitNull, ] = None label: Annotated[ str | None, Field(description='Human-readable description of the axis, especially for `custom`.'), + NoExplicitNull, ] = None @@ -119,11 +123,14 @@ class PreviewInput(AdCPBaseModel): ), ] macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to use for this preview variant') + dict[str, str] | None, + Field(description='Macro values to use for this preview variant'), + NoExplicitNull, ] = None context_description: Annotated[ str | None, Field(description='Natural language description of the context for AI-generated content'), + NoExplicitNull, ] = None @@ -134,6 +141,7 @@ class SignalCondition1(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -141,6 +149,7 @@ class SignalCondition1(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[Literal['binary'], Field(description='Discriminator for binary signals')] = 'binary' value: Annotated[ @@ -158,6 +167,7 @@ class SignalCondition2(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -165,6 +175,7 @@ class SignalCondition2(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['categorical'], Field(description='Discriminator for categorical signals') @@ -185,6 +196,7 @@ class SignalCondition3(AdCPBaseModel): signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field(description='The signal to target. New targeting constraints SHOULD use signal_ref.'), + NoExplicitNull, ] = None signal_id: Annotated[ signal_id_1.SignalId | None, @@ -192,6 +204,7 @@ class SignalCondition3(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.', ), + NoExplicitNull, ] = None value_type: Annotated[ Literal['numeric'], Field(description='Discriminator for numeric signals') @@ -201,12 +214,14 @@ class SignalCondition3(AdCPBaseModel): Field( description="Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided. Should be >= signal's range.min if defined." ), + NoExplicitNull, ] = None max_value: Annotated[ StrictFloat | None, Field( description="Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided. Should be <= signal's range.max if defined." ), + NoExplicitNull, ] = None @@ -237,60 +252,70 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None message: Annotated[ str | None, Field( description='Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative. For refinement, this describes the desired changes.' ), + NoExplicitNull, ] = None creative_manifest: Annotated[ creative_manifest_1.CreativeManifest | None, Field( description='Creative manifest to transform or generate from. On the canonical 3.2 path it carries `format_kind`, optional `format_option_ref`, and the required input assets. For transformation (for example resizing or reformatting), this is the complete creative to adapt. When creative_id is provided, the agent resolves the creative from its library and this field is ignored.' ), + NoExplicitNull, ] = None creative_representation_set: Annotated[ creative_representation_set_1.CreativeRepresentationSet | None, Field( description="Complete creative revision containing equivalent trafficking representations, of which exactly one is selected for this seller-bound output. This mode is accepted only by the destination sales agent and requires representation_destination plus representation_selection_strategy. The target capability selects the seller's build route; representation_destination supplies the binding inventory contract. The resolver verifies revision_content_digest against the complete set, retains every representation unchanged, selects exactly one compatible representation, and returns a manifest carrying representation_selection. If macro_values is present, selection happens first and binding affects only the derived output; the retained representation set and its revision binding never change. When none is compatible, the request fails with CREATIVE_REPRESENTATION_UNRESOLVED and one representation_rejections entry per candidate." ), + NoExplicitNull, ] = None representation_destination: Annotated[ representation_destination_1.RepresentationDestination | None, Field( description='Seller-owned product and effective format context for representation resolution. Required only with creative_representation_set and meaningful only when this endpoint is the destination sales agent.' ), + NoExplicitNull, ] = None representation_selection_strategy: Annotated[ representation_selection_strategy_1.RepresentationSelectionStrategy | None, Field( description='Deterministic strategy to apply after compatibility filtering. Required with creative_representation_set and MUST be advertised by creative.representation_resolution.strategies.' ), + NoExplicitNull, ] = None creative_id: Annotated[ str | None, Field( description="Reference to a creative in the agent's library. The creative agent resolves this to a manifest from its library. Use this instead of creative_manifest when retrieving an existing creative for tag generation or format adaptation." ), + NoExplicitNull, ] = None concept_id: Annotated[ str | None, Field( description='Creative concept containing the creative. Creative agents SHOULD assign globally unique creative_id values; when they cannot guarantee uniqueness, concept_id is REQUIRED to disambiguate.' ), + NoExplicitNull, ] = None media_buy_id: Annotated[ str | None, Field( description='Media buy identifier for tag generation context. When the creative agent is also the ad server, this provides the trafficking context needed to generate placement-specific tags (e.g., CM360 placement ID). Not needed when tags are generated at the creative level (most creative platforms).' ), + NoExplicitNull, ] = None package_id: Annotated[ str | None, Field( description='Package identifier within the media buy. Used with media_buy_id when the creative agent needs line-item-level context for tag generation. Omit to get a tag not scoped to a specific package.' ), + NoExplicitNull, ] = None target_format_id: Annotated[ format_id.FormatReferenceStructuredObject | None, @@ -298,6 +323,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format selector. Use `target_capability_id` with a value advertised in `get_adcp_capabilities.creative.supported_formats[].capability_id`.', ), + NoExplicitNull, ] = None target_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -307,6 +333,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None target_capability_id: Annotated[ str | None, @@ -314,6 +341,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Canonical 3.2 single-output selector. Matches exactly one `get_adcp_capabilities.creative.supported_formats[].capability_id` advertised by this creative agent. The matched entry supplies the canonical `format` declaration used to validate inputs and the returned manifest. Mutually exclusive with `target_capability_ids` and the deprecated target_format_id fields.', pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None target_capability_ids: Annotated[ list[TargetCapabilityId] | None, @@ -322,36 +350,42 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None transformer_id: Annotated[ str | None, Field( description="Selects an account-scoped transformer (discovered via list_transformers) to perform the build. One transformer per call. When present, the build uses this transformer and target_capability_id/target_capability_ids select which of its outputs to produce — they MUST be a subset of the transformer's output_capability_ids. Deprecated target_format_id fields use the legacy output_format_ids compatibility path. Render configuration goes in `config`." ), + NoExplicitNull, ] = None config: Annotated[ dict[str, Any] | None, Field( description='Typed render configuration for the selected transformer, keyed by each param\'s `field` (from the transformer\'s params[] in list_transformers). Example: { "voice": "isaac", "speaking_rate": 1.1, "mastering_preset": "podcast" }. The agent MUST validate `config` against the transformer\'s live params for this account and reject unrecognized keys and out-of-range / non-enumerated values with a field-attributed error (e.g. `config.voice`) rather than silently ignoring them — config drives a paid render. Genuinely vendor-specific or experimental knobs not declared as params belong in `ext`, not here. (The schema leaves this object open because legal keys are dynamic per transformer; strict validation is a normative agent obligation.) When `refine_from_build_variant_id` is set, `config` is applied as a DELTA over the parent leaf\'s config.' ), + NoExplicitNull, ] = None refine_from_build_variant_id: Annotated[ str | None, Field( description='Refine a previously produced variant and return new lineage-linked variants. The transformer and target capability are inherited from the parent leaf. A refinement request MUST omit transformer_id, target_capability_id(s), and deprecated target_format_id(s); changing transformer or output format is a new transformation build, not refinement. Requires creative.supports_refinement.' ), + NoExplicitNull, ] = None mode: Annotated[ Mode | None, Field( description="`execute` (default) produces and bills the creative(s). `estimate` is a DRY RUN: the agent produces nothing and bills nothing, and returns a BuildCreativeEstimate with a projected cost band (cost_low/cost_high) computed against THIS request's actual inputs (script length, brief, catalog size, max_creatives × max_variants) — the band the buyer cannot derive itself, since per_unit gives the rate but not the unit count. Requires the agent to advertise `creative.supports_spend_controls`; otherwise rejected with `UNSUPPORTED_FEATURE`." ), + NoExplicitNull, ] = Mode.execute max_spend: Annotated[ MaxSpend | None, Field( description='Hard per-call spend ceiling. The agent produces leaves until the NEXT leaf would push the run\'s aggregate vendor_cost over `amount`, then STOPS and returns the partial BuildCreativeVariantSuccess produced so far with `budget_status: "capped"` (every returned leaf is real, trafficable, and billed — nothing produced is discarded; the leaf shortfall is `leaves_returned` < `leaves_total`). If even the first leaf would exceed the cap, the call fails with BUDGET_CAP_REACHED. `currency` MUST match the rate card\'s currency (the agent does not FX-convert) or the request is rejected with INVALID_REQUEST (error.field `max_spend.currency`). Requires `creative.supports_spend_controls`. Caps a SINGLE call — to bound a refinement loop, track aggregate vendor_cost across calls and stop issuing them (buyer responsibility in this revision). max_spend bounds only build-time vendor_cost: CPM-priced builds (estimate basis `cpm_deferred`) have build-time vendor_cost 0 and accrue at serve time, so max_spend never engages for them — bound a CPM fan-out with max_creatives instead.' ), + NoExplicitNull, ] = None max_creatives: Annotated[ SchemaInt | None, @@ -359,6 +393,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Caps how many DISTINCT creatives to produce along the catalog/item fan-out axis — one creative per catalog item. Use it to sample a large catalog (e.g. send 150 job openings, set max_creatives: 5 to preview five). Distinct from item_limit, which caps how many catalog items a SINGLE creative consumes (DCO-style). Omitted with a catalog input means one creative per item up to the catalog/format bound; omitted without a catalog collapses to a single creative. Large fan-outs may return asynchronously. Mutually exclusive with `refine_from_build_variant_id` (refinement targets one prior creative, not a catalog fan-out). Supported only when the agent advertises `creative.multiplicity.supports_catalog_fanout`; values above `max_creatives_limit` are clamped. Pair with `max_spend` to bound the bill of a large fan-out.', ge=1, ), + NoExplicitNull, ] = None signal_conditions: Annotated[ list[SignalCondition] | None, @@ -366,6 +401,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description="Advisory keep-all PRODUCTION axis: produce one distinct creative group per signal condition, each kept and trafficked with its own signal targeting (e.g. a rain creative AND a sun creative). Sibling to max_creatives (catalog axis), NOT a variant_axis value (which is choose-among). Each item reuses SignalTargeting (value_type-discriminated binary/categorical/numeric over signal_ref) so the produced group's signal_condition resolves condition identity through the SAME schema the sales-side package targeting uses, plus an optional signal_agent_segment_id carrying the RESOLVED-segment identity (vs signal_ref's definition identity) — echo a provider-exposed handle verbatim; it is the primary trafficking-compatibility key, with categorical signal_ref+value as the weaker fallback. Per #5280 this is an ADVISORY context pointer — it informs production and MUST NOT hard-block at the build_creative layer; trafficking-compatibility (a sun creative MUST NOT serve into rain-targeted packages) is enforced reject-at-trafficking on the sales side (SIGNAL_TARGETING_INCOMPATIBLE), not here. Triggers the BuildCreativeVariantSuccess shape. Supported only when the agent advertises creative.multiplicity.supports_signal_fanout; condition counts above max_signal_conditions_limit are CLAMPED (not rejected), consistent with max_creatives. Composes with max_creatives (catalog × conditions cross-product) and max_variants (variants per group).", min_length=1, ), + NoExplicitNull, ] = None max_variants: Annotated[ SchemaInt | None, @@ -373,48 +409,56 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Caps how many ALTERNATIVES to produce per creative (different voices, themes, best-of-N, etc.). Default 1 preserves single-output behavior. Each variant is a real, independently-billed build (you pay for all produced); the buyer keeps one or many. When variant_axis.values[] is provided, its length is authoritative over max_variants. Resolutions/quality tiers are NOT variants — request them as additional target formats.', ge=1, ), + NoExplicitNull, ] = 1 variant_axis: Annotated[ VariantAxis | None, Field( description='Declares the dimension along which variants differ. When `values` is provided, the agent produces exactly one variant per value (e.g. an A/B of two voices). When only `dimension` is provided, the agent chooses up to max_variants variants along that dimension (e.g. best-of-N, themes).' ), + NoExplicitNull, ] = None keep_mode: Annotated[ KeepMode | None, Field( description='Advisory hint for how the buyer intends to use the variants. `keep_one` (best-of-N) and `keep_some` signal the agent to set `recommended`/`rank` on returned variants. Advisory only — it does not change what is returned or billed; every produced variant is returned and charged. Keeping is a client act of trafficking the chosen build_variant_id(s).' ), + NoExplicitNull, ] = KeepMode.keep_all selection_strategy: Annotated[ creative_selection_strategy.CreativeSelectionStrategy | None, Field( description='Governs HOW the agent samples when max_creatives < items_total (folds #5262). audience_relevance draws its ranking input from the SAME signal_ref pointers in signal_conditions / package targeting — NOT a parallel signals[] array. proximity takes a location input (geo shape TBD — WG open). inventory_priority is seller-side catalog metadata (margin/overstock/promo; no buyer input). random is the status-quo default. Per-creative selection ordering surfaces on the existing rank / recommended fields of creatives[].variants[], not a new selection_rank. Advisory; absent => agent default (random).' ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference | None, Field( description='Account reference for pricing and billing. When present, the creative agent applies account-specific pricing from the rate card, records the build against the account for billing, and can enforce account-level quotas or entitlements. Required by creative agents that charge for their services.' ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference for creative generation. Resolved to full brand identity (colors, logos, tone) at execution time.' ), + NoExplicitNull, ] = None quality: Annotated[ creative_quality.CreativeQuality | None, Field( description="Quality tier for generation. 'draft' produces fast, lower-fidelity output for iteration and review. 'production' produces full-quality output for final delivery. If omitted, the creative agent uses its own default. For non-generative transforms (e.g., format resizing), creative agents MAY ignore this field." ), + NoExplicitNull, ] = None evaluator: Annotated[ evaluator_spec.EvaluatorSpec | None, Field( description="Optional advisory evaluator (buyer-attached pointer, #5280) declaring how produced variants should be evaluated and ranked — the rank-side of the get_creative_features feature oracle. Experimental (x-status: experimental): the whole evaluator surface is new and unfrozen, and requires creative.supports_evaluator, which sellers MUST pair with `creative.evaluator` in experimental_features. Drives the producing agent's gate-then-rank pipeline over its best_of_n exploration: per leaf, evaluate (the chosen form) → optionally GATE (`evaluator.feature_requirement[]`, drop fails — internal pruning of which leaves the agent recommends, never an AdCP-layer block of an already-produced billable leaf) → RANK survivors (`evaluator.rank_by`, an explicit {feature_id, direction} ordering). Feature discovery uses get_adcp_capabilities governance.creative_features for rank_by, feature_requirement, and eval.features[]; evaluator_id is a pre-provisioned/account-arranged preset, not an ID discovered from that catalog. Populates a per-leaf `eval` block of creative-feature values (creative-feature-result[]) when supports_evaluator. When the evaluator names an external agent (`evaluator.feature_agent.agent_url` or the agent-form `agent_url`), that agent MUST appear in the seller's `creative_policy.accepted_verifiers[]` (the same allowlist #5280 established for provenance verify_agent); an off-list agent is rejected with `EVALUATOR_AGENT_NOT_ACCEPTED`. The outbound evaluator call authenticates on the transport (request signing/JWKS, mTLS, or a pre-provisioned static credential); credentials and caller-supplied trust material MUST NOT appear in evaluator, context, ext, or creative payload fields, and credential- or trust-material keys should be rejected with `CREDENTIAL_IN_ARGS`. With no `feature_requirement`, evaluation is advisory only and does not change what is produced or billed; an unreachable/unknown on-list agent degrades to seller-default ranking (advisory errors[] note), not a failure. Requires creative.supports_evaluator; otherwise ignored." ), + NoExplicitNull, ] = None item_limit: Annotated[ SchemaInt | None, @@ -422,12 +466,14 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description="Maximum number of catalog items a SINGLE creative consumes when generating (DCO-style — e.g. how many items fill one carousel/feed creative). When a catalog asset contains more items than this limit, the creative agent selects the top items based on relevance or catalog ordering. When item_limit exceeds the format's max_items, the creative agent SHOULD use the lesser of the two. Ignored when the manifest contains no catalog assets. Distinct from `max_creatives`, which fans OUT across catalog items to produce one distinct creative per item.", ge=1, ), + NoExplicitNull, ] = None include_preview: Annotated[ StrictBool | None, Field( description="When true, requests the creative agent to include preview renders in the response alongside the manifest. Agents that support this return a 'preview' object in the response using the same structure as preview_creative. Agents that do not support inline preview simply omit the field. This avoids a separate preview_creative round trip for platforms that generate previews as a byproduct of building." ), + NoExplicitNull, ] = None preview_inputs: Annotated[ list[PreviewInput] | None, @@ -435,24 +481,28 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): description='Input sets for preview generation when include_preview is true. Supported with a single target_capability_id; multi-capability requests generate one default preview per output. Deprecated target-format selectors retain equivalent compatibility behavior.', min_length=1, ), + NoExplicitNull, ] = None preview_quality: Annotated[ creative_quality.CreativeQuality | None, Field( description="Render quality for inline preview when include_preview is true. 'draft' produces fast, lower-fidelity renderings. 'production' produces full-quality renderings. Independent of the build quality parameter — you can build at draft quality and preview at production quality, or vice versa. If omitted, the creative agent uses its own default. Ignored when include_preview is false or omitted." ), + NoExplicitNull, ] = None preview_output_format: Annotated[ preview_output_format_1.PreviewOutputFormat | None, Field( description="Output format for preview renders when include_preview is true. 'url' returns preview_url (iframe-embeddable URL), 'html' returns preview_html (raw HTML). Ignored when include_preview is false or omitted." ), + NoExplicitNull, ] = preview_output_format_1.PreviewOutputFormat.url macro_values: Annotated[ dict[str, str] | None, Field( description="Raw concrete values offered for build-time binding, keyed by AdCP universal semantic (for example CLICK_URL or CACHEBUSTER). With declarations, a value binds only a verified-universal `resolve_value` occurrence performed_by `creative_agent`, using that declaration's exact context and encoding; callers MUST NOT pre-encode it. The selected `creative.supported_formats[]` route's macro_resolution_capabilities is the binding build/preview capability set; seller-wide and product sets apply only on the sales execution path. Values never short-circuit `translate_to_native`: translation emits its target declaration and the complete target capability chain remains required. For creative_representation_set, selection happens before binding and the complete representation set remains byte-identical. Without declarations, the 3.x legacy path remains: creative agents may translate recognized AdCP tokens using the existing `translateUniversalMacros` contract, preserve omitted placeholders for the sales agent, and ignore unknown keys. Existing unmapped/frozen-consent diagnostics remain required." ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -468,6 +518,7 @@ class BuildCreativeRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Optional webhook configuration for async terminal completion/failure notifications on build_creative. Meaningful only when the request enters the async lifecycle and returns a Submitted envelope. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change response timing semantics: agents MUST NOT route a request through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; requests that can be completed inline still return the synchronous success shape.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/build_creative_response.py b/src/adcp/types/domains/media_buy/build_creative_response.py index 46eea9ddf..e7f676575 100644 --- a/src/adcp/types/domains/media_buy/build_creative_response.py +++ b/src/adcp/types/domains/media_buy/build_creative_response.py @@ -202,7 +202,7 @@ class BuildCreativeResponse5(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope class BuildCreativeResponse6(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/media_buy/buy_products_request.py b/src/adcp/types/domains/media_buy/buy_products_request.py index 325f68938..148df0d5d 100644 --- a/src/adcp/types/domains/media_buy/buy_products_request.py +++ b/src/adcp/types/domains/media_buy/buy_products_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/buy_products_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import ( @@ -65,6 +65,7 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -74,6 +75,7 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$') @@ -86,6 +88,7 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None account: Annotated[ canonical_account_ref.CanonicalAccountReference, @@ -98,12 +101,14 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Brand source required when account is ID-only. Omit when account already contains brand and operator.' ), + NoExplicitNull, ] = None advertiser_industry: Annotated[ advertiser_industry_1.AdvertiserIndustry | None, Field( description='Industry classification for this campaign. Sellers may infer it from the resolved brand manifest when omitted.' ), + NoExplicitNull, ] = None feed_version: Annotated[ str, @@ -118,21 +123,24 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): description='list_products pricing_version containing the accepted rate. Buyers MUST include this whenever list_products returned one; omission means the seller does not version pricing separately.', min_length=1, ), + NoExplicitNull, ] = None purchases: Annotated[list[product_purchase_input.ProductPurchaseInput], Field(min_length=1)] - total_budget: TotalBudget | None = None + total_budget: Annotated[TotalBudget | None, NoExplicitNull] = None daily_budget_cap: Annotated[ StrictFloat | None, Field( description="Optional hard aggregate daily spend ceiling in total_budget.currency or the media buy's derived currency. It bounds the shared daily spend pool without allocating or reserving amounts for purchases.", ge=0.0, ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Optional max-impression cap using one counter across purchases. Buyers send it only when aggregate_frequency_capping is advertised. Every selected product must declare compatible media_buy_support; otherwise the purchase is rejected atomically with UNSUPPORTED_FEATURE before any mutation, never clamped.' ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, @@ -140,8 +148,11 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional IANA timezone override shared by every aggregate and purchase daily cap. Requires buyer_timezone_override support; otherwise rejected with UNSUPPORTED_FEATURE. When omitted, budget_capping.timezone_basis selects Account.timezone or fixed_timezone.', min_length=1, ), + NoExplicitNull, + ] = None + budget_allocation: Annotated[ + canonical_budget_allocation.CanonicalBudgetAllocation | None, NoExplicitNull ] = None - budget_allocation: canonical_budget_allocation.CanonicalBudgetAllocation | None = None start_time: start_timing.StartTiming end_time: AwareDatetime pacing: Annotated[ @@ -149,28 +160,38 @@ class BuyProductsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Aggregate media-buy pacing. In seller-optimized allocation, a seller declaring media_buy.features.seller_optimized_budget MUST accept omission and `even`; it MAY reject `asap` or `front_loaded` with UNSUPPORTED_FEATURE (error.field `pacing`) before any provider mutation and MUST NOT silently coerce them to `even`. Fixed-allocation semantics are unchanged.' ), + NoExplicitNull, + ] = None + bidding: Annotated[bidding_policy.BiddingPolicy | None, NoExplicitNull] = None + paused: Annotated[StrictBool | None, NoExplicitNull] = False + purchase_order_ref: Annotated[ + str | None, Field(max_length=255, min_length=1), NoExplicitNull ] = None - bidding: bidding_policy.BiddingPolicy | None = None - paused: StrictBool | None = False - purchase_order_ref: Annotated[str | None, Field(max_length=255, min_length=1)] = None - agency_estimate_number: Annotated[str | None, Field(max_length=100)] = None + agency_estimate_number: Annotated[str | None, Field(max_length=100), NoExplicitNull] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field(description='Authorized per-buy billing entity override.'), + NoExplicitNull, + ] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None reporting_webhook: Annotated[ reporting_webhook_1.ReportingWebhook | None, Field( description='Optional reporting delivery configuration established atomically with the MediaBuy. This is execution metadata and is not part of the immutable product pricing terms.' ), + NoExplicitNull, ] = None opportunity: Annotated[ Opportunity | None, Field( description='Optional planning-cycle closure for a direct product purchase. Success infers closed with accepted_with_seller when status is omitted; an explicit status MUST carry that same closure.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/buy_products_response.py b/src/adcp/types/domains/media_buy/buy_products_response.py index 5a09de7ab..2db9dd14b 100644 --- a/src/adcp/types/domains/media_buy/buy_products_response.py +++ b/src/adcp/types/domains/media_buy/buy_products_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/buy_products_response.json -# timestamp: 2026-10-04T04:34:38+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import canonical_media_buy_action @@ -54,9 +54,9 @@ class BuyProductsResponse2(AdCPBaseModel): ) status: Literal['failed'] = 'failed' errors: Annotated[list[error.Error], Field(min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class BuyProductsResponse3(AdCPBaseModel): @@ -65,11 +65,11 @@ class BuyProductsResponse3(AdCPBaseModel): ) status: Literal['submitted'] = 'submitted' task_id: Annotated[str, Field(min_length=1)] - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class BuyProductsResponse6(AdcpResponse, BuyProductsResponse2, BuyProductsResponse4): @@ -87,6 +87,7 @@ class Warning(Warning_1): description='Closed warning vocabulary for the negotiated AdCP release. A non-blocking condition observed while an operation still succeeded. New values are added through normal protocol releases; buyers validate against the negotiated release.', title='Warning Code', ), + NoExplicitNull, ] = None @@ -118,9 +119,10 @@ class BuyProductsResponse1(AdCPBaseModel): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None revision: Annotated[SchemaInt, Field(ge=1)] - media_buy_status: media_buy_status_1.MediaBuyStatus | None = None + media_buy_status: Annotated[media_buy_status_1.MediaBuyStatus | None, NoExplicitNull] = None confirmed_at: AwareDatetime | None = None accepted_proposal: AcceptedProposal purchase_bindings: Annotated[ @@ -137,10 +139,11 @@ class BuyProductsResponse1(AdCPBaseModel): description='Non-blocking observations about this completed commitment. The MediaBuy was still created or amended exactly as represented. Continuing conditions also appear as indicators on get_media_buys.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class BuyProductsResponse5(AdcpResponse, BuyProductsResponse1, BuyProductsResponse4): diff --git a/src/adcp/types/domains/media_buy/change_term.py b/src/adcp/types/domains/media_buy/change_term.py index 538ea99ed..a63956f46 100644 --- a/src/adcp/types/domains/media_buy/change_term.py +++ b/src/adcp/types/domains/media_buy/change_term.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/change_term.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, model_validator @@ -42,12 +42,14 @@ class MediaBuyChangeTerm(AdCPBaseModel): description='Non-terminal MediaBuy statuses in which this negotiated right may be exercised. When absent, the right applies in every non-terminal status where the canonical action itself is meaningful. This field describes contractual lifecycle scope; available_actions[] remains authoritative for the current instant.', min_length=1, ), + NoExplicitNull, ] = None processing_sla: Annotated[ sla_window.SlaWindow | None, Field( description='Binding elapsed-time acknowledgement and completion commitment. Sellers account for weekends and non-working periods when declaring the maximum.' ), + NoExplicitNull, ] = None conditions: Annotated[ list[Condition] | None, @@ -55,12 +57,14 @@ class MediaBuyChangeTerm(AdCPBaseModel): description='Opaque stable condition identifiers defined by terms_ref or bilateral commercial documentation. Implementations compare identifiers; they MUST NOT execute or interpret them as instructions.', min_length=1, ), + NoExplicitNull, ] = None constraints: Annotated[ change_term_constraints.MediaBuyChangeTermConstraints | None, Field( description='Portable bounds that buyer and seller SDKs can preflight. Omission means no machine-readable bound was promised; opaque conditions remain unevaluated.' ), + NoExplicitNull, ] = None terms_ref: Annotated[ str | None, @@ -69,6 +73,7 @@ class MediaBuyChangeTerm(AdCPBaseModel): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None description: Annotated[ str | None, @@ -77,8 +82,9 @@ class MediaBuyChangeTerm(AdCPBaseModel): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _validate_constraint_action(self) -> MediaBuyChangeTerm: diff --git a/src/adcp/types/domains/media_buy/change_term_constraints.py b/src/adcp/types/domains/media_buy/change_term_constraints.py index af8157059..d4f93a7d5 100644 --- a/src/adcp/types/domains/media_buy/change_term_constraints.py +++ b/src/adcp/types/domains/media_buy/change_term_constraints.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/change_term_constraints.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat, model_validator from ..core import duration @@ -20,15 +20,19 @@ class MediaBuyChangeTermConstraints3(AdCPBaseModel): max_additions: Annotated[ SchemaInt | None, Field(description='Maximum packages that may be added by one exercise of the right.', ge=0), + NoExplicitNull, ] = None max_removals: Annotated[ SchemaInt | None, Field( description='Maximum packages that may be removed by one exercise of the right.', ge=0 ), + NoExplicitNull, ] = None max_result_count: Annotated[ - SchemaInt | None, Field(description='Maximum active package count after the change.', ge=0) + SchemaInt | None, + Field(description='Maximum active package count after the change.', ge=0), + NoExplicitNull, ] = None @@ -57,6 +61,7 @@ class MediaBuyChangeTermConstraints1(AdCPBaseModel): Field( description='Maximum absolute amount by which the affected budget may change in the direction named by the action.' ), + NoExplicitNull, ] = None max_delta_percent: Annotated[ StrictFloat | None, @@ -64,12 +69,17 @@ class MediaBuyChangeTermConstraints1(AdCPBaseModel): description='Maximum percentage change relative to the current committed value. Values above 100 are valid for increases greater than the current value.', ge=0.0, ), + NoExplicitNull, ] = None min_result_amount: Annotated[ - Money | None, Field(description='Minimum resulting committed value after the change.') + Money | None, + Field(description='Minimum resulting committed value after the change.'), + NoExplicitNull, ] = None max_result_amount: Annotated[ - Money | None, Field(description='Maximum resulting committed value after the change.') + Money | None, + Field(description='Maximum resulting committed value after the change.'), + NoExplicitNull, ] = None @@ -90,20 +100,24 @@ class MediaBuyChangeTermConstraints2(AdCPBaseModel): Field( description='Maximum extension, shortening, or shift in the direction named by the action.' ), + NoExplicitNull, ] = None earliest_result: Annotated[ AwareDatetime | None, Field(description='Earliest resulting start or end timestamp accepted for this action.'), + NoExplicitNull, ] = None latest_result: Annotated[ AwareDatetime | None, Field(description='Latest resulting start or end timestamp accepted for this action.'), + NoExplicitNull, ] = None minimum_notice: Annotated[ duration.Duration | None, Field( description='Minimum elapsed notice before the requested flight change may take effect.' ), + NoExplicitNull, ] = None @@ -124,9 +138,10 @@ class MediaBuyChangeTermConstraints4(AdCPBaseModel): Field( description='Minimum elapsed notice before pause, resume, cancellation, or another operational action may take effect.' ), + NoExplicitNull, ] = None - earliest_effective_at: AwareDatetime | None = None - latest_effective_at: AwareDatetime | None = None + earliest_effective_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + latest_effective_at: Annotated[AwareDatetime | None, NoExplicitNull] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/commercial_terms.py b/src/adcp/types/domains/media_buy/commercial_terms.py index e9eff5bf8..e77ed00f9 100644 --- a/src/adcp/types/domains/media_buy/commercial_terms.py +++ b/src/adcp/types/domains/media_buy/commercial_terms.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/commercial_terms.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat, model_validator from ..core import ( @@ -40,8 +40,8 @@ class CancellationTerms(AdCPBaseModel): extra='forbid', ) effective_at: AwareDatetime - fee: Fee | None = None - reason: Annotated[str | None, Field(max_length=500, min_length=1)] = None + fee: Annotated[Fee | None, NoExplicitNull] = None + reason: Annotated[str | None, Field(max_length=500, min_length=1), NoExplicitNull] = None class ReportingCommitment(AdCPBaseModel): @@ -64,6 +64,7 @@ class CommercialTerms(AdCPBaseModel): description='Wholesale product feed version against which direct published offers were accepted. Omitted when the seller authored terms outside a wholesale snapshot.', min_length=1, ), + NoExplicitNull, ] = None source_pricing_version: Annotated[ str | None, @@ -71,9 +72,12 @@ class CommercialTerms(AdCPBaseModel): description='Pricing-layer version against which published rates were accepted.', min_length=1, ), + NoExplicitNull, ] = None brand: brand_key.BrandKey - advertiser_industry: advertiser_industry_1.AdvertiserIndustry | None = None + advertiser_industry: Annotated[ + advertiser_industry_1.AdvertiserIndustry | None, NoExplicitNull + ] = None purchases: Annotated[ list[product_purchase.ProductPurchase], Field( @@ -83,19 +87,21 @@ class CommercialTerms(AdCPBaseModel): ] start_time: start_timing.StartTiming end_time: AwareDatetime - total_budget: TotalBudget | None = None + total_budget: Annotated[TotalBudget | None, NoExplicitNull] = None daily_budget_cap: Annotated[ StrictFloat | None, Field( description='Hard aggregate daily spend ceiling accepted as part of these terms. It bounds total spend without creating purchase allocations.', ge=0.0, ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Hard MediaBuy-level cap accepted as part of these terms. One counter aggregates exposures across every purchase; purchase targeting caps remain independently binding.' ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, @@ -103,32 +109,40 @@ class CommercialTerms(AdCPBaseModel): description='Shared IANA calendar-day boundary for aggregate and purchase daily caps in these terms.', min_length=1, ), + NoExplicitNull, + ] = None + budget_allocation: Annotated[ + canonical_budget_allocation.CanonicalBudgetAllocation | None, NoExplicitNull ] = None - budget_allocation: canonical_budget_allocation.CanonicalBudgetAllocation | None = None - pacing: pacing_1.Pacing | None = None + pacing: Annotated[pacing_1.Pacing | None, NoExplicitNull] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description="Media-buy bidding policy. A proposal answering criteria.outcome_target.cost_per states here the cost the seller can plan to, which the buyer adopts on acceptance: the requested strength, and an amount greater than or equal to the ask (the ask when the seller can forecast goal volume under it within the buyer's budget, otherwise the lowest such amount), denominated in the purchases' pricing currency, which equals cost_per.currency. It is an execution control, not an expected price; when the planned spend at that amount is below total_budget, forecast points carry metrics.spend. See outcome-target.json for goal binding." ), + NoExplicitNull, + ] = None + invoice_recipient: Annotated[business_entity.BusinessEntity | None, NoExplicitNull] = None + purchase_order_ref: Annotated[ + str | None, Field(max_length=255, min_length=1), NoExplicitNull ] = None - invoice_recipient: business_entity.BusinessEntity | None = None - purchase_order_ref: Annotated[str | None, Field(max_length=255, min_length=1)] = None - agency_estimate_number: Annotated[str | None, Field(max_length=100)] = None + agency_estimate_number: Annotated[str | None, Field(max_length=100), NoExplicitNull] = None reporting_commitments: Annotated[ list[ReportingCommitment] | None, Field( description='Binding reporting contract keyed by position in purchases. Amendments preserve prior entries and add metrics with effective_at; seller-assigned package IDs live in the execution binding, outside this digest.', min_length=1, ), + NoExplicitNull, ] = None - cancellation_terms: CancellationTerms | None = None + cancellation_terms: Annotated[CancellationTerms | None, NoExplicitNull] = None change_terms: Annotated[ list[change_term.MediaBuyChangeTerm] | None, Field( description='Binding buyer change rights included in the commercial envelope and therefore covered by terms_digest. Entries are uniquely keyed by action. When this field is present, an omitted action is not a negotiated change right. Omission of the entire field means legacy-unspecified rights, not a prohibition.', min_length=1, ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/control_media_buy_request.py b/src/adcp/types/domains/media_buy/control_media_buy_request.py index e1a100112..161005b50 100644 --- a/src/adcp/types/domains/media_buy/control_media_buy_request.py +++ b/src/adcp/types/domains/media_buy/control_media_buy_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/control_media_buy_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat, model_validator from ..core import bidding_policy, canonical_account_ref, canonical_budget_allocation @@ -44,6 +44,7 @@ class ControlMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -53,6 +54,7 @@ class ControlMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$') @@ -74,16 +76,20 @@ class ControlMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None - paused: StrictBool | None = None + paused: Annotated[StrictBool | None, NoExplicitNull] = None canceled: Annotated[ Literal[True] | None, Field( description='Exercise an already-accepted unilateral cancellation right. A cancellation requiring seller agreement is requested by refining the accepted proposal.' ), + NoExplicitNull, ] = None - cancellation_reason: Annotated[str | None, Field(max_length=500, min_length=1)] = None - total_budget: TotalBudget | None = None + cancellation_reason: Annotated[ + str | None, Field(max_length=500, min_length=1), NoExplicitNull + ] = None + total_budget: Annotated[TotalBudget | None, NoExplicitNull] = None daily_budget_cap: Annotated[ StrictFloat | None, Field( @@ -104,12 +110,15 @@ class ControlMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, ), ] = None - budget_allocation: canonical_budget_allocation.CanonicalBudgetAllocation | None = None + budget_allocation: Annotated[ + canonical_budget_allocation.CanonicalBudgetAllocation | None, NoExplicitNull + ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Replace aggregate media-buy pacing. In seller-optimized allocation, a seller declaring media_buy.features.seller_optimized_budget MUST accept omission and `even`; it MAY reject `asap` or `front_loaded` with UNSUPPORTED_FEATURE (error.field `pacing`) before any provider mutation and MUST NOT silently coerce them to `even`. Fixed-allocation semantics are unchanged.' ), + NoExplicitNull, ] = None bidding: bidding_policy.BiddingPolicy | None = None packages: Annotated[ @@ -118,12 +127,17 @@ class ControlMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='Operational patches keyed by package_id. Each package_id MUST appear at most once; sellers reject duplicate IDs atomically.', min_length=1, ), + NoExplicitNull, + ] = None + reporting_webhook: Annotated[reporting_webhook_1.ReportingWebhook | None, NoExplicitNull] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - reporting_webhook: reporting_webhook_1.ReportingWebhook | None = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> ControlMediaBuyRequest: diff --git a/src/adcp/types/domains/media_buy/control_media_buy_response.py b/src/adcp/types/domains/media_buy/control_media_buy_response.py index 1636b92ad..4571fdcc1 100644 --- a/src/adcp/types/domains/media_buy/control_media_buy_response.py +++ b/src/adcp/types/domains/media_buy/control_media_buy_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/control_media_buy_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -38,9 +38,9 @@ class ControlMediaBuyResponse2(AdcpResponse, AdCPBaseModel): ) status: Literal['failed'] = 'failed' errors: Annotated[list[error.Error], Field(min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class ControlMediaBuyResponse3(AdcpResponse, AdCPBaseModel): @@ -49,11 +49,11 @@ class ControlMediaBuyResponse3(AdcpResponse, AdCPBaseModel): ) status: Literal['submitted'] = 'submitted' task_id: Annotated[str, Field(min_length=1)] - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class Warning(Warning_1): @@ -73,20 +73,23 @@ class ControlMediaBuyResponse1(AdcpResponse, AdCPBaseModel): status: Literal['completed'] = 'completed' media_buy_id: Annotated[str, Field(min_length=1)] revision: Annotated[SchemaInt, Field(ge=1)] - media_buy_status: media_buy_status_1.MediaBuyStatus | None = None + media_buy_status: Annotated[media_buy_status_1.MediaBuyStatus | None, NoExplicitNull] = None implementation_date: AwareDatetime | None = None - affected_package_ids: list[AffectedPackageId] | None = None - available_actions: list[canonical_media_buy_action.CanonicalMediaBuyAction] | None = None + affected_package_ids: Annotated[list[AffectedPackageId] | None, NoExplicitNull] = None + available_actions: Annotated[ + list[canonical_media_buy_action.CanonicalMediaBuyAction] | None, NoExplicitNull + ] = None warnings: Annotated[ list[Warning] | None, Field( description='Non-blocking observations about this completed control. The control was still applied exactly as represented. Continuing conditions also appear as indicators on get_media_buys.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None ControlMediaBuyResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/create_media_buy_async_response_input_required.py b/src/adcp/types/domains/media_buy/create_media_buy_async_response_input_required.py index 414b45bfb..77d81f7d5 100644 --- a/src/adcp/types/domains/media_buy/create_media_buy_async_response_input_required.py +++ b/src/adcp/types/domains/media_buy/create_media_buy_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/create_media_buy_async_response_input_required.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -25,13 +25,16 @@ class CreateMediaBuyInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional validation errors or warnings for debugging purposes. Helps explain why input is required.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/create_media_buy_async_response_submitted.py b/src/adcp/types/domains/media_buy/create_media_buy_async_response_submitted.py index ff9d898ff..828ebd37f 100644 --- a/src/adcp/types/domains/media_buy/create_media_buy_async_response_submitted.py +++ b/src/adcp/types/domains/media_buy/create_media_buy_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/create_media_buy_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,12 +36,14 @@ class CreateMediaBuySubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Awaiting IO signature from sales team; typical turnaround 2–4 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/create_media_buy_async_response_working.py b/src/adcp/types/domains/media_buy/create_media_buy_async_response_working.py index d7feb5362..7cdcd9893 100644 --- a/src/adcp/types/domains/media_buy/create_media_buy_async_response_working.py +++ b/src/adcp/types/domains/media_buy/create_media_buy_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/create_media_buy_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -18,14 +18,20 @@ class CreateMediaBuyWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/create_media_buy_request.py b/src/adcp/types/domains/media_buy/create_media_buy_request.py index da945f09b..2d168f6c8 100644 --- a/src/adcp/types/domains/media_buy/create_media_buy_request.py +++ b/src/adcp/types/domains/media_buy/create_media_buy_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/create_media_buy_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from collections.abc import Sequence from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat, model_validator from ..core import account_ref, bidding_policy, brand_ref @@ -69,6 +69,7 @@ class IoAcceptance(AdCPBaseModel): Field( description='Reference to the electronic signature from the signing service, when signing_url was used' ), + NoExplicitNull, ] = None @@ -114,6 +115,7 @@ class ArtifactWebhook(AdCPBaseModel): description='Optional client-provided token for webhook validation. Echoed back in webhook payload to validate request authenticity.', min_length=16, ), + NoExplicitNull, ] = None authentication: Annotated[ Authentication, @@ -133,6 +135,7 @@ class ArtifactWebhook(AdCPBaseModel): Field( description="For batched delivery, how often to push artifacts. Required when delivery_mode is 'batched'." ), + NoExplicitNull, ] = None sampling_rate: Annotated[ StrictFloat | None, @@ -141,6 +144,7 @@ class ArtifactWebhook(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None @@ -165,6 +169,7 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -181,6 +186,7 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='DEPRECATED on seller-facing requests. New buyers send the approved governance_context on the protocol envelope; the seller forwards that opaque context and does not need the plan identifier. If both are present, the governance agent MUST reject a mismatch. Removed in 4.0.', ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference, @@ -193,18 +199,21 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="ID of the exact committed proposal snapshot to execute. With total_budget, the publisher creates packages using the proposal's fixed percentages or seller-optimized constraints. Mutually exclusive: provide packages or proposal_id, not both. AdCP 3.2 request_proposals and ordinary refine_proposals revisions issue drafts; refine_proposals action finalize creates the executable committed hold. Sellers reject draft, declined, or previously executed snapshots, while exact retries with the original idempotency key replay historical success. Changed commercial terms are issued under a new proposal_id, so no separate proposal version is required." ), + NoExplicitNull, ] = None opportunity: Annotated[ Opportunity | None, Field( description='Optional planning-cycle closure. Sellers infer successful proposal execution as closed with close_reason accepted_with_seller when status is omitted; when status is present it MUST be closed with that reason. If the proposal was issued under an opportunity_id, a supplied ID MUST match it.' ), + NoExplicitNull, ] = None total_budget: Annotated[ TotalBudget | None, Field( description='Hard aggregate lifetime budget for the media buy. Required when executing a proposal and for seller-optimized explicit packages. Optional in fixed explicit-package mode; when present there, amount MUST equal the sum of package budgets. For a fixed proposal, the publisher applies allocation percentages to this amount. For a seller-optimized proposal or explicit buy, packages draw dynamically from this shared total.' ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -212,12 +221,14 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional hard aggregate daily spend ceiling in the media-buy currency. It limits total spend without allocating package amounts. Package caps are subordinate and need not sum to it. Requires advertised media_buy budget-capping scope; otherwise rejected with UNSUPPORTED_FEATURE.', ge=0.0, ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Optional max-impression cap using one counter across explicit packages. Requires advertised aggregate_frequency_capping and compatible media_buy_support on every product; otherwise sellers MUST reject with UNSUPPORTED_FEATURE before any mutation and MUST NOT silently drop, soften, or clamp the cap. Omit when executing proposal_id because accepted terms are authoritative.' ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, @@ -225,12 +236,14 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional shared IANA day boundary override for all caps. Requires buyer_timezone_override; otherwise rejected with UNSUPPORTED_FEATURE. When omitted, budget_capping.timezone_basis selects Account.timezone or the advertised fixed_timezone.', min_length=1, ), + NoExplicitNull, ] = None budget_allocation: Annotated[ budget_allocation_1.BudgetAllocation | None, Field( description='How budget is allocated across explicit packages. Omission means fixed allocation (legacy-compatible). Buyer agents SHOULD send budget_allocation explicitly ({mode: "fixed"} or seller_optimized) rather than rely on omission, and when the principal\'s instruction does not determine whether the budget is one shared pool across packages or split per package, SHOULD ask the principal rather than guess. seller_optimized requires advertised media_buy.features.seller_optimized_budget; inside it, package budget caps, min_spend_target, and package pacing each require their own advertised sub-capability (seller_optimized_package_budgets, seller_optimized_min_spend_targets, seller_optimized_package_pacing), otherwise the request is rejected with UNSUPPORTED_FEATURE before any over-subscription validation. In proposal mode the committed proposal supplies this configuration and callers MUST omit it here.' ), + NoExplicitNull, ] = None packages: Annotated[ Sequence[package_request.PackageRequest] | None, @@ -238,6 +251,7 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='Array of package configurations. Required when not using proposal_id. Mutually exclusive: provide packages or proposal_id, not both. Fixed allocation requires budget on every package. Seller-optimized allocation permits package budget to be omitted or to act as a hard cap. When executing a proposal, omit packages; the seller derives them from the committed proposal.', min_length=1, ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference, @@ -250,20 +264,25 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Industry classification for this specific campaign. A brand may operate across multiple industries (brand.json industries field), but each media buy targets one. For example, a consumer health company running a wellness campaign sends 'healthcare.wellness', not 'cpg'. Sellers map this to platform-native codes (e.g., Spotify ADV categories, LinkedIn industry IDs). When omitted, sellers may infer from the brand manifest's industries field." ), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description="Override the account's default billing entity for this specific buy. When provided, the seller invoices this entity instead. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request." ), + NoExplicitNull, ] = None io_acceptance: Annotated[ IoAcceptance | None, Field( description="Acceptance of an insertion order from a committed proposal. Required when the proposal's insertion_order has requires_signature: true. References the io_id from the proposal's insertion_order." ), + NoExplicitNull, + ] = None + po_number: Annotated[ + str | None, Field(description='Purchase order number for tracking'), NoExplicitNull ] = None - po_number: Annotated[str | None, Field(description='Purchase order number for tracking')] = None name: Annotated[ str | None, Field( @@ -272,6 +291,7 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None agency_estimate_number: Annotated[ str | None, @@ -279,6 +299,7 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description="Agency estimate or authorization number. Primary financial reference for broadcast buys — links the order to the agency's media plan and billing system. Travels with the order and creative traffic identifiers through the transaction lifecycle.", max_length=100, ), + NoExplicitNull, ] = None start_time: start_timing.StartTiming end_time: Annotated[ @@ -289,37 +310,43 @@ class CreateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Aggregate pacing strategy for the media-buy budget across the media-buy flight. This controls how much the buy spends over time. Package pacing is subordinate and influences which package receives the aggregate spend; package pacing MUST NOT cause aggregate delivery to exceed this strategy. Defaults to even when total_budget is present. When executing a proposal that carries pacing, omit this field or send the identical value; the seller MUST reject a conflicting override with TERMS_REJECTED. In a seller-optimized buy, a seller declaring media_buy.features.seller_optimized_budget MUST accept omission and `even`; it MAY reject `asap` or `front_loaded` with UNSUPPORTED_FEATURE (error.field `pacing`) before any provider mutation and MUST NOT silently coerce them to `even`. Pacing carried by the seller's own committed proposal is not subject to that rejection. Fixed-allocation semantics are unchanged." ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description="Complete media-buy bidding default inherited by packages that omit package.bidding. `{automatic:true}` records an explicit automatic policy. In seller-optimized mode, cost_per/roas bind to the primary budget_allocation_1.optimization_goals goal. In fixed mode, inherited cost_per is valid only when inheriting package primary-goal result units are compatible; inherited roas requires value-bearing primary goals. Every monetary field uses total_budget.currency or the single currency derived for the media buy, and every affected pricing option MUST declare that currency. Sellers MUST reject incompatible units, combinations, currency, or overrides before mutation with BIDDING_PLACEMENT_CONFLICT. Media-buy bidding combined with any inheriting package's legacy bid_price or legacy monetary optimization-goal target is ambiguous and MUST be rejected with AMBIGUOUS_BIDDING_POLICY." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field( description="Create the media buy in a paused delivery state. When true, and the buy would otherwise be active because creatives are assigned and the flight has started, the seller returns media_buy_status 'paused'. Setup blockers still take precedence: a buy with no creatives remains 'pending_creatives', and a future-dated buy remains 'pending_start' until its flight can start. Defaults to false." ), + NoExplicitNull, ] = False push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async task status notifications. Publisher will send webhooks when status changes (working, input-required, completed, failed, canceled). Buyers SHOULD supply `push_notification_config_1.operation_id` as the canonical correlation value; publishers echo that field back verbatim in webhook payloads and MUST NOT parse the URL to derive it.' ), + NoExplicitNull, ] = None reporting_webhook: Annotated[ reporting_webhook_1.ReportingWebhook | None, Field(description='Optional webhook configuration for automated reporting delivery'), + NoExplicitNull, ] = None artifact_webhook: Annotated[ ArtifactWebhook | None, Field( description='Optional webhook configuration for content artifact delivery. Used by governance agents to validate content adjacency. Seller pushes artifacts to this endpoint; orchestrator forwards to governance agent for validation.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> CreateMediaBuyRequest: diff --git a/src/adcp/types/domains/media_buy/create_media_buy_response.py b/src/adcp/types/domains/media_buy/create_media_buy_response.py index d3ccf3dd0..d9cd4d726 100644 --- a/src/adcp/types/domains/media_buy/create_media_buy_response.py +++ b/src/adcp/types/domains/media_buy/create_media_buy_response.py @@ -89,7 +89,7 @@ class CreateMediaBuyResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelop class CreateMediaBuyResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/media_buy/decline_proposals_request.py b/src/adcp/types/domains/media_buy/decline_proposals_request.py index f6562aefa..2247b2d22 100644 --- a/src/adcp/types/domains/media_buy/decline_proposals_request.py +++ b/src/adcp/types/domains/media_buy/decline_proposals_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/decline_proposals_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -28,6 +29,7 @@ class DeclineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -37,6 +39,7 @@ class DeclineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, @@ -44,10 +47,15 @@ class DeclineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): description='MCP compatibility field: servers ignore this value; A2A uses transport-native Message/Task contextId.', min_length=1, ), + NoExplicitNull, + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - context: context_1.ContextObject | None = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None idempotency_key: Annotated[ str, Field( @@ -70,4 +78,5 @@ class DeclineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Optional planning-cycle update. Every named proposal MUST belong to this opportunity_id. Sellers apply the update only when every result is declined; if any result is unable, the opportunity remains unchanged. Use status closed when these declines end the broader opportunity.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/decline_proposals_response.py b/src/adcp/types/domains/media_buy/decline_proposals_response.py index aeb50bc62..ae8dead85 100644 --- a/src/adcp/types/domains/media_buy/decline_proposals_response.py +++ b/src/adcp/types/domains/media_buy/decline_proposals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/decline_proposals_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -35,6 +35,7 @@ class Results1(AdCPBaseModel): description='Why the seller could not apply a decline. Present only for outcome unable.', min_length=1, ), + NoExplicitNull, ] = None @@ -79,26 +80,26 @@ class DeclineProposalsResponse1(AdcpResponse, AdCPBaseModel): ) results: Annotated[list[Results], Field(min_length=1)] status: Literal['submitted'] = 'submitted' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class DeclineProposalsResponse2(AdcpResponse, CompactTaskSubmitted): model_config = ConfigDict( extra='forbid', ) - results: Annotated[list[Results3] | None, Field(min_length=1)] = None + results: Annotated[list[Results3] | None, Field(min_length=1), NoExplicitNull] = None status: Literal['submitted'] = 'submitted' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None DeclineProposalsResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/get_media_buy_delivery_request.py b/src/adcp/types/domains/media_buy/get_media_buy_delivery_request.py index 482f387ee..a25708388 100644 --- a/src/adcp/types/domains/media_buy/get_media_buy_delivery_request.py +++ b/src/adcp/types/domains/media_buy/get_media_buy_delivery_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buy_delivery_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StrictBool from ..core import account_ref @@ -38,16 +38,21 @@ class AttributionWindow(AdCPBaseModel): extra='allow', ) post_click: Annotated[ - duration.Duration | None, Field(description='Post-click attribution window to apply.') + duration.Duration | None, + Field(description='Post-click attribution window to apply.'), + NoExplicitNull, ] = None post_view: Annotated[ - duration.Duration | None, Field(description='Post-view attribution window to apply.') + duration.Duration | None, + Field(description='Post-view attribution window to apply.'), + NoExplicitNull, ] = None model: Annotated[ attribution_model.AttributionModel | None, Field( description='Attribution model to use. When omitted, the seller applies their default model.' ), + NoExplicitNull, ] = None @@ -61,6 +66,7 @@ class Spot(AdCPBaseModel): description='Optional maximum number of spot rows to return. When the response is incomplete because of this limit or a seller-imposed maximum, by_spot_truncated is true.', ge=1, ), + NoExplicitNull, ] = None @@ -84,18 +90,21 @@ class CatalogItem(AdCPBaseModel): description='Maximum number of catalog_item entries to return. When omitted, the seller returns its automatic default set.', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -109,18 +118,21 @@ class Creative(AdCPBaseModel): description='Maximum number of creative entries to return. When omitted, the seller returns its automatic default set.', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -134,18 +146,21 @@ class Keyword(AdCPBaseModel): description='Maximum number of keyword entries to return. When omitted, the seller returns its automatic default set.', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -165,6 +180,7 @@ class Geo(AdCPBaseModel): Field( description="Optional classification system for metro or postal_area levels. Metro uses metro-system values (e.g., 'nielsen_dma'); native postal_area uses country-local postal-system values with country (e.g., country 'US', system 'zip'); deprecated legacy postal_area requests may use legacy-postal-system values such as 'us_zip'. Omit to request the level without selecting a specific system." ), + NoExplicitNull, ] = None country: Annotated[ str | None, @@ -172,6 +188,7 @@ class Geo(AdCPBaseModel): description='ISO 3166-1 alpha-2 country code. Required for native postal_area requests; omitted for legacy postal_area and non-postal geo requests.', pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None limit: Annotated[ SchemaInt | None, @@ -179,18 +196,21 @@ class Geo(AdCPBaseModel): description='Maximum number of geo entries to return. Defaults to 25. When truncated, by_geo_truncated is true in the response.', ge=1, ), + NoExplicitNull, ] = 25 sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -204,24 +224,28 @@ class DeviceType(AdCPBaseModel): description='Maximum number of entries to return. When omitted, all entries are returned (the enum is small and bounded).', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc cursor: Annotated[ str | None, Field( description="Opaque cursor from a previous response's by_device_type_pagination to fetch the next page of a truncated by_device_type breakdown. Omit for the first page. limit, sort_by, and sort_direction MUST be repeated unchanged across paged requests for the same logical query." ), + NoExplicitNull, ] = None @@ -235,24 +259,28 @@ class DevicePlatform(AdCPBaseModel): description='Maximum number of entries to return. When omitted, all entries are returned (the enum is small and bounded).', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc cursor: Annotated[ str | None, Field( description="Opaque cursor from a previous response's by_device_platform_pagination to fetch the next page of a truncated by_device_platform breakdown. Omit for the first page. limit, sort_by, and sort_direction MUST be repeated unchanged across paged requests for the same logical query." ), + NoExplicitNull, ] = None @@ -266,18 +294,21 @@ class Format(AdCPBaseModel): description='Maximum number of format rows to return. When omitted, all rows are returned because the canonical format-kind vocabulary is small and bounded.', ge=1, ), + NoExplicitNull, ] = None sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -288,24 +319,28 @@ class Audience(AdCPBaseModel): limit: Annotated[ SchemaInt | None, Field(description='Maximum number of entries to return. Defaults to 25.', ge=1), + NoExplicitNull, ] = 25 sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc cursor: Annotated[ str | None, Field( description="Opaque cursor from a previous response's by_audience_pagination to fetch the next page of a truncated by_audience breakdown. Omit for the first page. limit, sort_by, and sort_direction MUST be repeated unchanged across paged requests for the same logical query." ), + NoExplicitNull, ] = None @@ -319,22 +354,26 @@ class Demographic(AdCPBaseModel): description="Optional canonical age ranges to return as distinct rows. Copying an applied targeting predicate here requests aligned reporting only when the reporting capability supports that exact predicate. Omit to request the product's declared native demographic breakdown.", min_length=1, ), + NoExplicitNull, ] = None limit: Annotated[ SchemaInt | None, Field(description='Maximum number of demographic entries to return. Defaults to 25.', ge=1), + NoExplicitNull, ] = 25 sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc @@ -345,24 +384,28 @@ class Placement(AdCPBaseModel): limit: Annotated[ SchemaInt | None, Field(description='Maximum number of entries to return. Defaults to 25.', ge=1), + NoExplicitNull, ] = 25 sort_by: Annotated[ sort_metric.SortMetric | None, Field( description="Metric to sort breakdown rows by, in `sort_direction` order (descending by default). Falls back to 'spend' when the seller does not report the requested metric at this breakdown's row grain; on fallback the sort direction resets to 'desc'. Rows lacking a value for the applied sort metric order last regardless of direction. The applied sort is echoed in the response." ), + NoExplicitNull, ] = sort_metric.SortMetric.spend sort_direction: Annotated[ sort_direction_1.SortDirection | None, Field( description="Direction for sort_by ordering. Defaults to 'desc' (largest first). 'asc' enables bottom-N queries (e.g., the 25 worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. Sellers MUST apply the requested direction to the applied sort metric — direction has no availability fallback." ), + NoExplicitNull, ] = sort_direction_1.SortDirection.desc cursor: Annotated[ str | None, Field( description="Opaque cursor from a previous response's by_placement_pagination to fetch the next page of a truncated by_placement breakdown. Omit for the first page. limit, sort_by, and sort_direction MUST be repeated unchanged across paged requests for the same logical query." ), + NoExplicitNull, ] = None @@ -375,88 +418,105 @@ class ReportingDimensions(AdCPBaseModel): Field( description='Request a negotiated catalog_item breakdown. Omitting this key preserves the automatic behavior — sellers return catalog_item rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding.' ), + NoExplicitNull, ] = None creative: Annotated[ Creative | None, Field( description='Request a negotiated creative breakdown. Omitting this key preserves the automatic behavior — sellers return creative rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding.' ), + NoExplicitNull, ] = None keyword: Annotated[ Keyword | None, Field( description='Request a negotiated keyword breakdown. Omitting this key preserves the automatic behavior — sellers return keyword rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding.' ), + NoExplicitNull, ] = None geo: Annotated[ Geo | None, Field( description='Request geographic breakdown. Check reporting_capabilities.supports_geo_breakdown for available levels and systems.' ), + NoExplicitNull, ] = None device_type: Annotated[ - DeviceType | None, Field(description='Request device type breakdown.') + DeviceType | None, Field(description='Request device type breakdown.'), NoExplicitNull ] = None device_platform: Annotated[ - DevicePlatform | None, Field(description='Request device platform breakdown.') + DevicePlatform | None, + Field(description='Request device platform breakdown.'), + NoExplicitNull, ] = None format: Annotated[ Format | None, Field( description='Request delivery broken down by canonical creative format kind. This dimension is negotiated on the GET path. Reporting webhook configuration does not negotiate or guarantee dimensional breakdowns, although a webhook payload may carry the same fields as an extension.' ), + NoExplicitNull, ] = None audience: Annotated[ - Audience | None, Field(description='Request audience segment breakdown.') + Audience | None, Field(description='Request audience segment breakdown.'), NoExplicitNull ] = None demographic: Annotated[ Demographic | None, Field( description="Request delivery broken down by demographic. Check the product's reporting_capabilities.supports_demographic_breakdown independently from demographic_targeting. When age_ranges is present, every requested range MUST be exactly supported by exact_predicates or equal one of the declared enumerated_intervals; sellers MUST reject unsupported ranges with UNSUPPORTED_FEATURE rather than silently widen or narrow them." ), + NoExplicitNull, ] = None spot: Annotated[ Spot | None, Field( description='Request a spot-level as-run airing log for broadcast TV, radio, or other scheduled inventory. Rows are ordered by aired_at ascending. When limit is omitted, sellers SHOULD return the complete log for the requested reporting period.' ), + NoExplicitNull, + ] = None + placement: Annotated[ + Placement | None, Field(description='Request placement breakdown.'), NoExplicitNull ] = None - placement: Annotated[Placement | None, Field(description='Request placement breakdown.')] = None property: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery by canonical publisher property. Check reporting_capabilities.supports_property_breakdown.' ), + NoExplicitNull, ] = None collection: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery by canonical content collection. Check reporting_capabilities.supports_collection_breakdown.' ), + NoExplicitNull, ] = None installment: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery by canonical installment within a collection. Check reporting_capabilities.supports_installment_breakdown.' ), + NoExplicitNull, ] = None collection_property: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery at the collection × property intersection. This is the reporting grain that can prove a collection delivered on a particular host service; independent collection and property marginals cannot. Check reporting_capabilities.supports_collection_property_breakdown.' ), + NoExplicitNull, ] = None installment_property: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery at the installment × property intersection. This is the reporting grain that can prove a specific airing, episode, issue, or programming block delivered on a particular host service; independent installment and property marginals cannot. Check reporting_capabilities.supports_installment_property_breakdown.' ), + NoExplicitNull, ] = None placement_property: Annotated[ delivery_breakdown_controls.DeliveryBreakdownControls | None, Field( description='Request delivery at the placement × property intersection. Use when a placement may span multiple web, app, social, or assistant properties. Check reporting_capabilities.supports_placement_property_breakdown.' ), + NoExplicitNull, ] = None @@ -469,10 +529,12 @@ class GetMediaBuyDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Filter delivery data to a specific account. When omitted, returns data across all accessible accounts.' ), + NoExplicitNull, ] = None media_buy_ids: Annotated[ list[str] | None, Field(description='Array of media buy IDs to get delivery data for', min_length=1), + NoExplicitNull, ] = None reporting_revision_id: Annotated[ str | None, @@ -482,16 +544,19 @@ class GetMediaBuyDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description='Cursor pagination. With reporting_revision_id, pages immutable authoritative rows while all revision metadata repeats on every page.' ), + NoExplicitNull, ] = None status_filter: Annotated[ media_buy_status.MediaBuyStatus | StatusFilter | None, Field(description='Filter by status. Can be a single status or array of statuses'), + NoExplicitNull, ] = None start_date: Annotated[ str | None, @@ -499,6 +564,7 @@ class GetMediaBuyDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description="Inclusive start date for the reporting period (YYYY-MM-DD), as a calendar date in the reporting timezone: the reporting_capabilities.timezone of the products behind the in-scope packages. It is a UTC day only when that timezone is UTC. When omitted along with end_date, returns campaign lifetime data. Only accepted when the product's reporting_capabilities.date_range_support is 'date_range'. A date-bounded request whose in-scope packages span more than one reporting timezone MUST be rejected with VALIDATION_ERROR. The buyer narrows media_buy_ids to buys that share one reporting timezone, or omits both dates when a single buy's packages span reporting timezones.", pattern='^\\d{4}-\\d{2}-\\d{2}$', ), + NoExplicitNull, ] = None end_date: Annotated[ str | None, @@ -506,12 +572,14 @@ class GetMediaBuyDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description="Exclusive end date for the reporting period (YYYY-MM-DD), as a calendar date in the same reporting timezone as start_date. Must be later than start_date. When omitted along with start_date, returns campaign lifetime data. Only accepted when the product's reporting_capabilities.date_range_support is 'date_range'.", pattern='^\\d{4}-\\d{2}-\\d{2}$', ), + NoExplicitNull, ] = None include_package_daily_breakdown: Annotated[ StrictBool | None, Field( description='When true, include daily_breakdown arrays within each package in by_package. Useful for per-package pacing analysis and line-item monitoring. Omit or set false to reduce response size — package daily data can be large for multi-package buys over long flights.' ), + NoExplicitNull, ] = False requested_metrics: Annotated[ list[available_metric.AvailableMetric] | None, @@ -519,33 +587,38 @@ class GetMediaBuyDeliveryRequest(AdcpRequest, AdcpVersionEnvelope): description="Optional list of metrics to include in the response. When omitted, all available metrics are included (unchanged behavior). Applies to every metrics-bearing object in the response: totals, by_package, daily and window slices, and breakdown rows. impressions and spend are always included regardless of this list, except that a legacy or externally created mixed-currency buy MUST omit monetary and money-derived values from media-buy and window totals, MUST omit daily_breakdown, and MUST report monetary values only on currency-qualified package rows (including window package rows). Requesting a leaf metric identity returns its canonical nested carrier — e.g. requesting viewable_rate returns the viewability object, requesting quartile_75 returns quartile_data — never a flat duplicate. Metrics requested but not available for this buy are omitted from the response without error; contract accountability is unchanged — missing_metrics still reconciles against committed_metrics, but sellers MUST NOT list a metric in missing_metrics when its absence is solely due to this narrowing. Must be a subset of the product's reporting_capabilities.available_metrics; values outside the declared set are ignored. Subset evaluation follows the container-subsumption rule in enums/available-metric.json. Sort is evaluated before narrowing: excluding a metric from this list never triggers the sort_by fallback, and breakdown rows may be ordered by a metric absent from the narrowed payload — the applied-sort echo still names it. Same narrowing semantics as reporting_webhook.requested_metrics, with one shape difference: this field requires at least one entry when present (omit it entirely for full payloads), while the webhook field permits an empty array with the same meaning as omission.", min_length=1, ), + NoExplicitNull, ] = None time_granularity: Annotated[ reporting_frequency.ReportingFrequency | None, Field( description="Per-window slice granularity for the pull, using the same vocabulary as reporting_webhook.reporting_frequency. When set, the seller returns per-window delivery slices over the date range — useful for reconstructing data a buyer's webhook receiver missed, since the slice payload is shape-aligned with what reporting_webhook would have delivered for the same window. Capability-scoped: the value MUST be one of the seller's declared reporting_capabilities.windowed_pull_granularities; otherwise the seller MUST return UNSUPPORTED_GRANULARITY. When set to daily, weekly, monthly, or quarterly and the in-scope packages span more than one reporting timezone, the seller MUST return VALIDATION_ERROR. When omitted, behavior is unchanged (cumulative aggregates plus optional daily breakdowns per existing fields)." ), + NoExplicitNull, ] = None include_window_breakdown: Annotated[ StrictBool | None, Field( description="When true, the response includes media_buy_deliveries[].windows[] — an array of per-window delivery slices over the date range at the requested time_granularity. Ignored when time_granularity is omitted. Each window's payload mirrors what reporting_webhook would have delivered for the same window, enabling lossless GET-path recovery for buyers who missed webhook fires. Omit or set false to reduce response size when only cumulative aggregates are needed." ), + NoExplicitNull, ] = False attribution_window: Annotated[ AttributionWindow | None, Field( description='Attribution window to apply for conversion metrics. When provided, the seller returns conversion data using the requested lookback windows instead of their platform default. The seller echoes the applied window in the response. Sellers that do not support configurable windows ignore this field and return their default. Check get_adcp_capabilities conversion_tracking.attribution_windows for available options.' ), + NoExplicitNull, ] = None reporting_dimensions: Annotated[ ReportingDimensions | None, Field( description='Request dimensional breakdowns in delivery reporting. Each key enables a specific breakdown dimension within by_package — include as an empty object (e.g., "device_type": {}) to activate with defaults. Omit entirely for no breakdowns (backward compatible). Unsupported dimensions are silently omitted from the response. For every requested dimension that the product declares supported, the seller MUST return the corresponding array (possibly empty) and its truncated flag. Metric-sorted dimensions also return their applied-sort echoes; demographic and property-grain arrays also return their suppressed flag. Spot uses aired_at ordering and has no sort echoes. Note: keyword, catalog_item, and creative breakdowns are returned automatically when the seller supports them; including their keys here is optional and upgrades them to this negotiated contract without changing the automatic default.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _validate_delivery_selector_mode(self) -> GetMediaBuyDeliveryRequest: diff --git a/src/adcp/types/domains/media_buy/get_media_buy_delivery_response.py b/src/adcp/types/domains/media_buy/get_media_buy_delivery_response.py index 624db149a..ecb618592 100644 --- a/src/adcp/types/domains/media_buy/get_media_buy_delivery_response.py +++ b/src/adcp/types/domains/media_buy/get_media_buy_delivery_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buy_delivery_response.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import attribution_window as attribution_window_1 @@ -87,6 +87,7 @@ class ReportingPeriod(AdCPBaseModel): examples=['UTC', 'America/New_York'], min_length=1, ), + NoExplicitNull, ] = None @@ -119,10 +120,14 @@ class DailyBreakdownItem(AdCPBaseModel): ] spend: Annotated[StrictFloat, Field(description='Daily spend for this package', ge=0.0)] conversions: Annotated[ - StrictFloat | None, Field(description='Daily conversions for this package', ge=0.0) + StrictFloat | None, + Field(description='Daily conversions for this package', ge=0.0), + NoExplicitNull, ] = None conversion_value: Annotated[ - StrictFloat | None, Field(description='Daily conversion value for this package', ge=0.0) + StrictFloat | None, + Field(description='Daily conversion value for this package', ge=0.0), + NoExplicitNull, ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -130,10 +135,12 @@ class DailyBreakdownItem(AdCPBaseModel): description='Daily settled conversion value eligible for revenue-share commission for this package', ge=0.0, ), + NoExplicitNull, ] = None roas: Annotated[ StrictFloat | None, Field(description='Daily return on ad spend (conversion_value / spend)', ge=0.0), + NoExplicitNull, ] = None new_to_brand_rate: Annotated[ StrictFloat | None, @@ -142,6 +149,7 @@ class DailyBreakdownItem(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None @@ -158,11 +166,11 @@ class DailyBreakdownItem1(AdCPBaseModel): ] impressions: Annotated[StrictFloat, Field(description='Daily impressions', ge=0.0)] spend: Annotated[StrictFloat, Field(description='Daily spend', ge=0.0)] - conversions: Annotated[StrictFloat | None, Field(description='Daily conversions', ge=0.0)] = ( - None - ) + conversions: Annotated[ + StrictFloat | None, Field(description='Daily conversions', ge=0.0), NoExplicitNull + ] = None conversion_value: Annotated[ - StrictFloat | None, Field(description='Daily conversion value', ge=0.0) + StrictFloat | None, Field(description='Daily conversion value', ge=0.0), NoExplicitNull ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -170,10 +178,12 @@ class DailyBreakdownItem1(AdCPBaseModel): description='Daily settled conversion value eligible for revenue-share commission', ge=0.0, ), + NoExplicitNull, ] = None roas: Annotated[ StrictFloat | None, Field(description='Daily return on ad spend (conversion_value / spend)', ge=0.0), + NoExplicitNull, ] = None new_to_brand_rate: Annotated[ StrictFloat | None, @@ -182,6 +192,7 @@ class DailyBreakdownItem1(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None @@ -214,6 +225,7 @@ class AggregatedTotals(AdCPBaseModel): clicks: Annotated[ StrictFloat | None, Field(description='Total clicks across all media buys (if applicable)', ge=0.0), + NoExplicitNull, ] = None completed_views: Annotated[ StrictFloat | None, @@ -221,18 +233,22 @@ class AggregatedTotals(AdCPBaseModel): description='Total audio/video completions across all media buys (if applicable)', ge=0.0, ), + NoExplicitNull, ] = None views: Annotated[ StrictFloat | None, Field(description='Total views across all media buys (if applicable)', ge=0.0), + NoExplicitNull, ] = None conversions: Annotated[ StrictFloat | None, Field(description='Total conversions across all media buys (if applicable)', ge=0.0), + NoExplicitNull, ] = None conversion_value: Annotated[ StrictFloat | None, Field(description='Total conversion value across all media buys (if applicable)', ge=0.0), + NoExplicitNull, ] = None commissionable_value: Annotated[ StrictFloat | None, @@ -240,6 +256,7 @@ class AggregatedTotals(AdCPBaseModel): description='Total settled conversion value eligible for revenue-share commission across all media buys (if applicable)', ge=0.0, ), + NoExplicitNull, ] = None roas: Annotated[ StrictFloat | None, @@ -247,6 +264,7 @@ class AggregatedTotals(AdCPBaseModel): description='Aggregate return on ad spend across all media buys (total conversion_value / total spend)', ge=0.0, ), + NoExplicitNull, ] = None new_to_brand_rate: Annotated[ StrictFloat | None, @@ -255,6 +273,7 @@ class AggregatedTotals(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None cost_per_acquisition: Annotated[ StrictFloat | None, @@ -262,6 +281,7 @@ class AggregatedTotals(AdCPBaseModel): description='Aggregate cost per conversion across all media buys (total spend / total conversions)', ge=0.0, ), + NoExplicitNull, ] = None completion_rate: Annotated[ StrictFloat | None, @@ -277,18 +297,21 @@ class AggregatedTotals(AdCPBaseModel): description='Reach across all media buys. Only present when all media buys share the same reach_unit. Omitted when reach units are heterogeneous — use per-buy reach values instead. The optional reach_aggregation field declares whether this value is deduplicated across buys or is a sum of constituent reach values.', ge=0.0, ), + NoExplicitNull, ] = None reach_aggregation: Annotated[ reach_aggregation_1.ReachAggregation | None, Field( description='How reach was combined across the media buys in this aggregate. When omitted, legacy reach semantics are unknown and consumers MUST NOT use reach as the denominator for frequency.' ), + NoExplicitNull, ] = None reach_unit: Annotated[ reach_unit_1.ReachUnit | None, Field( description='Unit of measurement for reach. Only present when all aggregated media buys use the same reach_unit.' ), + NoExplicitNull, ] = None frequency: Annotated[ StrictFloat | None, @@ -296,6 +319,7 @@ class AggregatedTotals(AdCPBaseModel): description='Average frequency per reach unit across all media buys (impressions / reach). In new payloads, only present when reach is present and reach_aggregation is deduplicated. MUST be omitted when reach_aggregation is sum_of_constituent_reach. Legacy payloads that omit reach_aggregation remain schema-valid, but consumers MUST NOT treat their reach as a safe frequency denominator.', ge=0.0, ), + NoExplicitNull, ] = None media_buy_count: Annotated[ SchemaInt, Field(description='Number of media buys included in the response', ge=0) @@ -333,6 +357,7 @@ class AggregatedTotals(AdCPBaseModel): ] ], ), + NoExplicitNull, ] = None @@ -343,6 +368,7 @@ class Totals(DeliveryMetrics): description="Effective rate paid per unit based on pricing_model (e.g., actual CPM for 'cpm', actual cost per completed view for 'cpcv', actual cost per point for 'cpp')", ge=0.0, ), + NoExplicitNull, ] = None @@ -388,7 +414,7 @@ class ByAudienceItem(DeliveryMetrics): ), ] audience_name: Annotated[ - str | None, Field(description='Human-readable audience segment name') + str | None, Field(description='Human-readable audience segment name'), NoExplicitNull ] = None impressions: Any spend: Any @@ -412,6 +438,7 @@ class ByDemographicItem(DeliveryMetrics): Field( description="Canonical age interval represented by this row. Required by protocol semantics for age ranges requested through reporting_dimensions.demographic.age_ranges and for intervals declared in the product's demographic reporting capability." ), + NoExplicitNull, ] = None impressions: Any spend: Any @@ -431,6 +458,7 @@ class BySpotItem(DeliveryMetrics): description="Optional identifier for the creative that aired in this spot occurrence, matching the package's creative asset or assignment. For library-backed sellers this is the sync_creatives identifier; inline-only sellers use the packages[].creatives creative_id.", min_length=1, ), + NoExplicitNull, ] = None aired_at: Annotated[ AwareDatetime, @@ -444,6 +472,7 @@ class BySpotItem(DeliveryMetrics): description="Optional network or syndicated service name, for example 'USA Network' or 'Westwood One'.", min_length=1, ), + NoExplicitNull, ] = None station: Annotated[ str | None, @@ -451,6 +480,7 @@ class BySpotItem(DeliveryMetrics): description="Optional station identifier or call sign, for example 'WABC-TV' or 'WNYC-FM'.", min_length=1, ), + NoExplicitNull, ] = None daypart: Annotated[ str | None, @@ -458,6 +488,7 @@ class BySpotItem(DeliveryMetrics): description="Optional seller-defined daypart label, for example 'prime_time' or 'morning_drive'.", min_length=1, ), + NoExplicitNull, ] = None @@ -469,6 +500,7 @@ class ByPackageItem1(DeliveryMetrics): description="ISO 4217 denomination for this package window's monetary values. Required by the runtime currency-coherence rule when neither the parent media-buy row nor deprecated response-wide field supplies a currency and this package window contains monetary values; otherwise it MUST match the row or response currency when present.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None @@ -496,12 +528,14 @@ class Window(AdCPBaseModel): Field( description='Per-package metrics for this window slice, using the same metric envelope and package identity as the parent media_buy_deliveries[].by_package row but scoped to the window. Requested reporting_dimensions do not apply to these webhook-aligned recovery rows; sellers may include dimensional fields only as webhook payload extensions, not as a guaranteed result of the GET request. Sellers MAY omit by_package when per-package window-level data is unavailable; when present, package_id values MUST match the parent by_package entries.' ), + NoExplicitNull, ] = None is_final: Annotated[ StrictBool | None, Field( description='Whether the slice data is final for this window. Same semantics as the per-package is_final flag — when false, a later read may return revised numbers for the same window as measurement matures (broadcast C3 → C7, post-IVT filtering, etc.).' ), + NoExplicitNull, ] = None measurement_window: Annotated[ str | None, @@ -509,6 +543,7 @@ class Window(AdCPBaseModel): description="Which measurement-window stage this slice represents (e.g., 'live', 'c3', 'c7'), referencing a window_id from the product's reporting_capabilities.measurement_windows. Absent when the data is not phased.", max_length=50, ), + NoExplicitNull, ] = None @@ -520,6 +555,7 @@ class ByPackageItem(DeliveryMetrics): description='Package delivery pace relative to its package-level pacing plan (1.0 = on track, <1.0 = behind, >1.0 = ahead). In seller-optimized mode this is a subordinate diagnostic and may be absent when no package pacing preference exists.', ge=0.0, ), + NoExplicitNull, ] = None pricing_model: Annotated[ pricing_model_1.PricingModel, @@ -546,22 +582,26 @@ class ByPackageItem(DeliveryMetrics): Field( description="System-reported operational state of this package. Reflects actual delivery state independent of buyer pause control. 'not_delivering' means zero impressions were recorded for the entire reporting_period while the package was in-flight. Sellers SHOULD only report 'not_delivering' once the package's data is_final for the period — a provisional (is_final: false) zero may still be measurement catching up, not genuine non-delivery." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field(description='Whether this package is currently paused by the buyer'), + NoExplicitNull, ] = None is_final: Annotated[ StrictBool | None, Field( description="Whether this delivery data is final for the reporting period. When false, the data may be updated as measurement matures (e.g., broadcast C7 window accumulating DVR playback) or as processing completes (e.g., IVT filtering, deduplication). When true, the seller considers this data closed — no further updates for this period — and is willing to invoice on it subject to the buy's `measurement_terms.billing_measurement`. Absent means the seller does not distinguish provisional from final data." ), + NoExplicitNull, ] = None finalized_at: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp at which this package's data became final. Present only when `is_final: true`. Anchors reconciliation and (when later defined) dispute-window clocks against the buy's `measurement_terms.billing_measurement.measurement_window`." ), + NoExplicitNull, ] = None measurement_window: Annotated[ str | None, @@ -570,6 +610,7 @@ class ByPackageItem(DeliveryMetrics): examples=['live', 'c3', 'c7'], max_length=50, ), + NoExplicitNull, ] = None supersedes_window: Annotated[ str | None, @@ -578,6 +619,7 @@ class ByPackageItem(DeliveryMetrics): examples=['live', 'c3'], max_length=50, ), + NoExplicitNull, ] = None missing_metrics: Annotated[ list[missing_metric.MissingMetric] | None, @@ -596,466 +638,544 @@ class ByPackageItem(DeliveryMetrics): ], ], ), + NoExplicitNull, ] = None metric_values: Annotated[ list[package_delivery_metric_value.PackageDeliveryMetricValue] | None, Field( description='Qualified standard delivery values for this package. Each entry is the delivered counterpart to a standard-scope package.committed_metrics or by_package[].missing_metrics row, using the same atomic key (scope, metric_id, qualifier). Sellers report one row per full qualifier set at package grain; when a metric appears here, its flat scalar counterpart on the package MUST be omitted to avoid two sources of truth. Vendor-scope values continue to use vendor_metric_values. Buyers reconcile rows directly and perform any compatible cross-package aggregation themselves.' ), + NoExplicitNull, ] = None by_catalog_item: Annotated[ list[catalog_item_delivery_metrics.CatalogItemDeliveryMetrics] | None, Field( description='Delivery by catalog item within this package. Available for catalog-driven packages when the seller supports item-level reporting.' ), + NoExplicitNull, ] = None by_catalog_item_truncated: Annotated[ StrictBool | None, Field( description='Whether by_catalog_item was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_catalog_item is present and the request included reporting_dimensions.catalog_item (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_catalog_item_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_catalog_item rows. Sellers MUST return this field whenever by_catalog_item is present and the request included reporting_dimensions.catalog_item. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract." ), + NoExplicitNull, ] = None by_catalog_item_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_catalog_item ordering. Sellers MUST return this field whenever by_catalog_item is present and the request included reporting_dimensions.catalog_item, alongside by_catalog_item_sorted_by. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_creative: Annotated[ list[creative_delivery_metrics.CreativeDeliveryMetrics] | None, Field( description='Metrics broken down by creative within this package. Available when the seller supports creative-level reporting.' ), + NoExplicitNull, ] = None by_format: Annotated[ list[ByFormatItem] | None, Field( description="Delivery by canonical creative format kind within this package. Negotiated on the GET path when the buyer requests reporting_dimensions.format and the product declares supports_format_breakdown; reporting webhook configuration does not negotiate or guarantee this breakdown. Each row aggregates every served creative of that format kind. Sellers MUST aggregate all delivery using adopter-defined shapes into one format_kind 'custom' row. When by_format_truncated is false, additive metrics such as impressions and spend across the rows SHOULD reconcile to the corresponding package totals, subject to the measurement and attribution semantics of each metric. Buyers MUST NOT expect row-level correspondence between by_format and by_creative because the two breakdowns are independently produced at different grains." ), + NoExplicitNull, ] = None by_format_truncated: Annotated[ StrictBool | None, Field( description='Whether by_format was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_format is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_format_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_format rows. Sellers MUST return this field whenever by_format is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_format_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_format ordering. Sellers MUST return this field whenever by_format is present.' ), + NoExplicitNull, ] = None by_creative_truncated: Annotated[ StrictBool | None, Field( description='Whether by_creative was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_creative is present and the request included reporting_dimensions.creative (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_creative_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_creative rows. Sellers MUST return this field whenever by_creative is present and the request included reporting_dimensions.creative. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract." ), + NoExplicitNull, ] = None by_creative_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_creative ordering. Sellers MUST return this field whenever by_creative is present and the request included reporting_dimensions.creative, alongside by_creative_sorted_by. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_keyword: Annotated[ list[keyword_delivery_metrics.KeywordDeliveryMetrics] | None, Field( description='Metrics broken down by keyword within this package. One row per (keyword, match_type) pair — the same keyword with different match types appears as separate rows. Keyword-grain only: rows reflect aggregate performance of each targeted keyword, not individual search queries. Rows may not sum to package totals when a single impression is attributed to the triggering keyword only. Available for search and retail media packages when the seller supports keyword-level reporting.' ), + NoExplicitNull, ] = None by_keyword_truncated: Annotated[ StrictBool | None, Field( description='Whether by_keyword was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_keyword is present and the request included reporting_dimensions.keyword (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_keyword_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_keyword rows. Sellers MUST return this field whenever by_keyword is present and the request included reporting_dimensions.keyword. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract." ), + NoExplicitNull, ] = None by_keyword_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_keyword ordering. Sellers MUST return this field whenever by_keyword is present and the request included reporting_dimensions.keyword, alongside by_keyword_sorted_by. When the breakdown was returned automatically without a request key, the field is RECOMMENDED but not required — automatic rows carry no completeness contract.' ), + NoExplicitNull, ] = None by_geo: Annotated[ list[geo_delivery_metrics.GeoDeliveryMetrics] | None, Field( description="Delivery by geographic area within this package. Available when the buyer requests geo breakdown via reporting_dimensions and the seller supports it. Each dimension's rows are independent slices that should sum to the package total." ), + NoExplicitNull, ] = None by_geo_truncated: Annotated[ StrictBool | None, Field( description='Whether by_geo was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_geo is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_geo_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_geo rows. Sellers MUST return this field whenever by_geo is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_geo_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_geo ordering. Sellers MUST return this field whenever by_geo is present.' ), + NoExplicitNull, ] = None by_device_type: Annotated[ list[ByDeviceTypeItem] | None, Field( description='Delivery by device form factor within this package. Available when the buyer requests device_type breakdown via reporting_dimensions and the seller supports it.' ), + NoExplicitNull, ] = None by_device_type_truncated: Annotated[ StrictBool | None, Field( description='Whether by_device_type was truncated. Sellers MUST return this flag whenever by_device_type is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_device_type_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_device_type rows. Sellers MUST return this field whenever by_device_type is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_device_type_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_device_type ordering. Sellers MUST return this field whenever by_device_type is present.' ), + NoExplicitNull, ] = None by_device_type_pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description="Cursor to retrieve the remaining by_device_type rows when by_device_type_truncated is true. Sellers MUST return this field whenever by_device_type_truncated is true; omit when false or when by_device_type is absent. Pass the cursor back in the corresponding request's reporting_dimensions.device_type_1.cursor to fetch the next page; other reporting_dimensions.device_type request fields (limit, sort_by, sort_direction) MUST be repeated unchanged across paged requests." ), + NoExplicitNull, ] = None by_device_platform: Annotated[ list[ByDevicePlatformItem] | None, Field( description='Delivery by operating system within this package. Available when the buyer requests device_platform breakdown via reporting_dimensions and the seller supports it. Useful for CTV campaigns where tvOS vs Roku OS vs Fire OS matters.' ), + NoExplicitNull, ] = None by_device_platform_truncated: Annotated[ StrictBool | None, Field( description='Whether by_device_platform was truncated. Sellers MUST return this flag whenever by_device_platform is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_device_platform_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_device_platform rows. Sellers MUST return this field whenever by_device_platform is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_device_platform_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_device_platform ordering. Sellers MUST return this field whenever by_device_platform is present.' ), + NoExplicitNull, ] = None by_device_platform_pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description="Cursor to retrieve the remaining by_device_platform rows when by_device_platform_truncated is true. Sellers MUST return this field whenever by_device_platform_truncated is true; omit when false or when by_device_platform is absent. Pass the cursor back in the corresponding request's reporting_dimensions.device_platform_1.cursor to fetch the next page; other reporting_dimensions.device_platform request fields (limit, sort_by, sort_direction) MUST be repeated unchanged across paged requests." ), + NoExplicitNull, ] = None by_audience: Annotated[ list[ByAudienceItem] | None, Field( description="Delivery by audience segment within this package. Available when the buyer requests audience breakdown via reporting_dimensions and the seller supports it. Only 'synced' audiences are directly targetable via the targeting overlay; other sources are informational." ), + NoExplicitNull, ] = None by_audience_truncated: Annotated[ StrictBool | None, Field( description='Whether by_audience was truncated. Sellers MUST return this flag whenever by_audience is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_audience_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_audience rows. Sellers MUST return this field whenever by_audience is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_audience_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_audience ordering. Sellers MUST return this field whenever by_audience is present.' ), + NoExplicitNull, ] = None by_audience_pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description="Cursor to retrieve the remaining by_audience rows when by_audience_truncated is true. Sellers MUST return this field whenever by_audience_truncated is true; omit when false or when by_audience is absent. Pass the cursor back in the corresponding request's reporting_dimensions.audience.cursor to fetch the next page; other reporting_dimensions.audience request fields (limit, sort_by, sort_direction) MUST be repeated unchanged across paged requests." ), + NoExplicitNull, ] = None by_demographic: Annotated[ list[ByDemographicItem] | None, Field( description='Delivery by demographic within this package. Available when the buyer requests demographic breakdown and the product declares supports_demographic_breakdown. A free-form measurement code does not prove alignment with buyer targeting. When age is present it is the authoritative machine-comparable interval; for requested age_ranges, sellers MUST echo the exact requested interval and MUST NOT substitute a wider or narrower native bucket.' ), + NoExplicitNull, ] = None by_demographic_truncated: Annotated[ StrictBool | None, Field( description='Whether non-suppressed by_demographic rows were truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_demographic is present. False means every non-suppressed row is present; inspect by_demographic_suppressed separately before reconciling rows to package totals.' ), + NoExplicitNull, ] = None by_demographic_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_demographic rows. Sellers MUST return this field whenever by_demographic is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_demographic_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_demographic ordering. Sellers MUST return this field whenever by_demographic is present.' ), + NoExplicitNull, ] = None by_demographic_suppressed: Annotated[ StrictBool | None, Field( description='Whether one or more otherwise reportable demographic rows were omitted due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_demographic is present. False means no rows were threshold-suppressed.' ), + NoExplicitNull, ] = None by_placement: Annotated[ list[placement_delivery_metrics.PlacementDeliveryMetrics] | None, Field( description='Delivery by placement within this package. placement_id remains required for 3.1 compatibility. New 3.2 sellers also emit placement_identity, whose discriminator separates publisher-catalog identity from sales-agent-defined inline identity.' ), + NoExplicitNull, ] = None by_placement_truncated: Annotated[ StrictBool | None, Field( description='Whether by_placement was truncated. Sellers MUST return this flag whenever by_placement is present (false means the list is complete).' ), + NoExplicitNull, ] = None by_placement_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description="The metric actually used to order by_placement rows. Sellers MUST return this field whenever by_placement is present. When the seller cannot sort by the requested sort_by metric it falls back to 'spend'; this echo makes the fallback visible instead of silently returning rows the buyer will misread as ordered by the requested metric." ), + NoExplicitNull, ] = None by_placement_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_placement ordering. Sellers MUST return this field whenever by_placement is present.' ), + NoExplicitNull, ] = None by_placement_pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description="Cursor to retrieve the remaining by_placement rows when by_placement_truncated is true. Sellers MUST return this field whenever by_placement_truncated is true; omit when false or when by_placement is absent. Pass the cursor back in the corresponding request's reporting_dimensions.placement.cursor to fetch the next page; other reporting_dimensions.placement request fields (limit, sort_by, sort_direction) MUST be repeated unchanged across paged requests." ), + NoExplicitNull, ] = None by_property: Annotated[ list[property_delivery_metrics.PropertyDeliveryMetrics] | None, Field( description='Delivery by publisher property within this package. Each row identifies the actual surface with an operational identifier and adds property_ref when it resolves to a canonical publisher catalog entry. Rows are independent of by_collection.' ), + NoExplicitNull, ] = None by_property_truncated: Annotated[ StrictBool | None, Field( description='Whether non-suppressed by_property rows were truncated. Sellers MUST return this flag whenever by_property is present. False means every non-suppressed row is present; inspect by_property_suppressed before reconciling rows to package totals.' ), + NoExplicitNull, ] = None by_property_suppressed: Annotated[ StrictBool | None, Field( description='Whether one or more otherwise reportable by_property rows were omitted from this response due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_property is present. False means no rows were threshold-suppressed. Both suppression and truncation may be true.' ), + NoExplicitNull, ] = None by_property_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_property rows. Sellers MUST return this field whenever by_property is present.' ), + NoExplicitNull, ] = None by_property_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_property rows. Sellers MUST return this field whenever by_property is present.' ), + NoExplicitNull, ] = None by_collection: Annotated[ list[collection_delivery_metrics.CollectionDeliveryMetrics] | None, Field( description='Delivery by publisher-scoped collection within this package. This is a marginal breakdown and does not by itself prove which property carried a collection.' ), + NoExplicitNull, ] = None by_collection_truncated: Annotated[ StrictBool | None, Field( description='Whether by_collection was truncated. Sellers MUST return this flag whenever by_collection is present.' ), + NoExplicitNull, ] = None by_collection_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_collection rows. Sellers MUST return this field whenever by_collection is present.' ), + NoExplicitNull, ] = None by_collection_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_collection rows. Sellers MUST return this field whenever by_collection is present.' ), + NoExplicitNull, ] = None by_installment: Annotated[ list[installment_delivery_metrics.InstallmentDeliveryMetrics] | None, Field(description='Delivery by canonically identified installment within this package.'), + NoExplicitNull, ] = None by_installment_truncated: Annotated[ StrictBool | None, Field( description='Whether by_installment was truncated. Sellers MUST return this flag whenever by_installment is present.' ), + NoExplicitNull, ] = None by_installment_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_installment rows. Sellers MUST return this field whenever by_installment is present.' ), + NoExplicitNull, ] = None by_installment_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_installment rows. Sellers MUST return this field whenever by_installment is present.' ), + NoExplicitNull, ] = None by_collection_property: Annotated[ list[collection_property_delivery_metrics.CollectionPropertyDeliveryMetrics] | None, Field( description='Delivery at the collection × property intersection. A row is affirmative delivery evidence that the referenced collection ran on the referenced property; it is not merely a carriage or catalog assertion.' ), + NoExplicitNull, ] = None by_collection_property_truncated: Annotated[ StrictBool | None, Field( description='Whether non-suppressed by_collection_property rows were truncated. Sellers MUST return this flag whenever by_collection_property is present.' ), + NoExplicitNull, ] = None by_collection_property_suppressed: Annotated[ StrictBool | None, Field( description='Whether one or more otherwise reportable by_collection_property rows were omitted from this response due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_collection_property is present. False means no rows were threshold-suppressed. Both suppression and truncation may be true.' ), + NoExplicitNull, ] = None by_collection_property_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_collection_property rows. Sellers MUST return this field whenever by_collection_property is present.' ), + NoExplicitNull, ] = None by_collection_property_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_collection_property rows. Sellers MUST return this field whenever by_collection_property is present.' ), + NoExplicitNull, ] = None by_installment_property: Annotated[ list[installment_property_delivery_metrics.InstallmentPropertyDeliveryMetrics] | None, Field( description='Delivery at the installment × property intersection. A row is affirmative delivery evidence that the referenced airing, episode, issue, or programming block ran on the referenced property; it is not inferred from collection carriage or independent marginals.' ), + NoExplicitNull, ] = None by_installment_property_truncated: Annotated[ StrictBool | None, Field( description='Whether non-suppressed by_installment_property rows were truncated. Sellers MUST return this flag whenever by_installment_property is present.' ), + NoExplicitNull, ] = None by_installment_property_suppressed: Annotated[ StrictBool | None, Field( description='Whether one or more otherwise reportable by_installment_property rows were omitted from this response due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_installment_property is present. False means no rows were threshold-suppressed. Both suppression and truncation may be true.' ), + NoExplicitNull, ] = None by_installment_property_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_installment_property rows. Sellers MUST return this field whenever by_installment_property is present.' ), + NoExplicitNull, ] = None by_installment_property_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_installment_property rows. Sellers MUST return this field whenever by_installment_property is present.' ), + NoExplicitNull, ] = None by_placement_property: Annotated[ list[placement_property_delivery_metrics.PlacementPropertyDeliveryMetrics] | None, Field( description='Delivery at the placement × property intersection. This proves which actual property carried a placement that may span more than one property.' ), + NoExplicitNull, ] = None by_placement_property_truncated: Annotated[ StrictBool | None, Field( description='Whether non-suppressed by_placement_property rows were truncated. Sellers MUST return this flag whenever by_placement_property is present.' ), + NoExplicitNull, ] = None by_placement_property_suppressed: Annotated[ StrictBool | None, Field( description='Whether one or more otherwise reportable by_placement_property rows were omitted from this response due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_placement_property is present. False means no rows were threshold-suppressed. Both suppression and truncation may be true.' ), + NoExplicitNull, ] = None by_placement_property_sorted_by: Annotated[ sort_metric.SortMetric | None, Field( description='The metric actually used to order by_placement_property rows. Sellers MUST return this field whenever by_placement_property is present.' ), + NoExplicitNull, ] = None by_placement_property_sort_direction: Annotated[ sort_direction.SortDirection | None, Field( description='The direction actually applied to by_placement_property rows. Sellers MUST return this field whenever by_placement_property is present.' ), + NoExplicitNull, ] = None by_spot: Annotated[ list[BySpotItem] | None, Field( description='Spot-level as-run airing records for broadcast TV, radio, or other scheduled inventory. Available when the buyer requests spot breakdown and the product declares supports_spot_breakdown. Sellers MUST order rows by aired_at ascending. The same spot_id is reused when a later package measurement_window adds or revises metrics. Network and station are optional so station-direct radio and network-level TV records use the same channel-neutral shape. Sellers SHOULD populate creative_id whenever they can associate a specific airing with a creative, particularly when the package could serve more than one creative during any part of the reporting period, including a mid-period replacement. Sellers that cannot make that association MUST omit creative_id rather than emit a default or placeholder. Buyers MUST NOT aggregate by_spot rows by creative_id and expect the result to equal by_creative impressions for the same creative: the two dimensions are independently produced at different granularities and are subject to different metric-maturation and attribution semantics within the package measurement_window. by_creative is the authoritative creative-performance aggregate; creative_id on a by_spot row identifies which creative aired, not an independent metric roll-up source.' ), + NoExplicitNull, ] = None by_spot_truncated: Annotated[ StrictBool | None, Field( description='Whether by_spot is incomplete because of the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_spot is present (false means the as-run log is complete for the requested reporting period).' ), + NoExplicitNull, ] = None daily_breakdown: Annotated[ list[DailyBreakdownItem] | None, Field( description='Day-by-day delivery for this package. Only present when include_package_daily_breakdown is true in the request. Enables per-package pacing analysis and line-item monitoring.' ), + NoExplicitNull, ] = None spend: Any @@ -1071,6 +1191,7 @@ class MediaBuyDelivery(AdCPBaseModel): description="ISO 4217 denomination for monetary values in this media-buy delivery row, including totals, package spend, and currency-denominated package rates. Sellers SHOULD populate this field whenever all monetary values in the row share one currency. For AdCP-authored buys it MUST equal the media-buy currency, and every by_package[].currency MUST equal it. For a legacy or externally created mixed-currency buy, omit this field, daily_breakdown, and all monetary or money-derived values from row and window totals; report those values only at package grain with each package's own currency. AdCP does not perform currency conversion.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None status: Annotated[ Status, @@ -1083,28 +1204,33 @@ class MediaBuyDelivery(AdCPBaseModel): Field( description='When delayed data is expected to be available (only present when status is reporting_delayed)' ), + NoExplicitNull, ] = None is_adjusted: Annotated[ StrictBool | None, Field( description='Indicates this delivery contains updated data for a previously reported period. Buyer should replace previous period data with these totals.' ), + NoExplicitNull, ] = None is_final: Annotated[ StrictBool | None, Field( description="Whether this row's delivery data is final for the reporting period. The row does not carry its own `measurement_window` — that lives on each `by_package[*]` entry. Reconciliation joins on per-package `measurement_window`; this row-level flag is a convenience roll-up. Sellers MUST NOT emit `is_final: true` at the row level unless every entry in `by_package` has `is_final: true` for the same `measurement_window` as the buy's `measurement_terms.billing_measurement.measurement_window` (or for the row's natural window when no `billing_measurement.measurement_window` is set). On any disagreement between row-level and package-level finality, package-level is authoritative. When true, the seller considers these numbers closed and is willing to invoice on them subject to `measurement_terms.billing_measurement`. When false, numbers may still move as measurement matures (broadcast C3 → C7) or processing completes (IVT scrubbing, dedup). When absent, the seller does not distinguish provisional from final at the row level — consult per-package `is_final`." ), + NoExplicitNull, ] = None finalized_at: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp at which this row became final. Present only when `is_final: true`. Anchors the buyer's reconciliation and (when later defined) dispute-window clocks against the buy's `measurement_terms.billing_measurement`. Computed as the latest `finalized_at` across the row's packages for the reconciliation window." ), + NoExplicitNull, ] = None pricing_model: Annotated[ pricing_model_1.PricingModel | None, Field(description='Pricing model used for this media buy'), + NoExplicitNull, ] = None pacing_index: Annotated[ StrictFloat | None, @@ -1112,6 +1238,7 @@ class MediaBuyDelivery(AdCPBaseModel): description='Aggregate media-buy delivery pace relative to the media-buy pacing plan (1.0 = on track, <1.0 = behind, >1.0 = ahead). This is the authoritative pacing signal for seller-optimized buys; package pacing indexes are subordinate diagnostics.', ge=0.0, ), + NoExplicitNull, ] = None totals: Totals by_package: Annotated[list[ByPackageItem], Field(description='Metrics broken down by package')] @@ -1120,12 +1247,14 @@ class MediaBuyDelivery(AdCPBaseModel): Field( description="Per-window delivery slices over the reporting period at the requested time_granularity. Only present when the request set time_granularity and include_window_breakdown: true. Each slice mirrors what reporting_webhook would have delivered for the same window — buyers who missed webhook fires can reconstruct identical data by reading this array. Slice rows are ordered by window_start ascending; consecutive rows are contiguous (each row's window_end equals the next row's window_start) and partition the requested date range at the chosen granularity. For a legacy or external mixed-currency media buy, monetary and money-derived values MUST be omitted from each window totals object and reported only in currency-qualified windows[].by_package rows. Sellers MUST exclude this field when time_granularity is omitted; when set, sellers MUST honor pulls at any granularity in reporting_capabilities.windowed_pull_granularities (otherwise return UNSUPPORTED_GRANULARITY). See snapshot-and-log Rule 4 for the two-paths-parity contract this surface anchors." ), + NoExplicitNull, ] = None daily_breakdown: Annotated[ list[DailyBreakdownItem1] | None, Field( description="Day-by-day delivery for a media-buy row with one currency. Sellers MUST omit this aggregate breakdown for a legacy or externally created mixed-currency buy because these rows have no package currency field. Sellers MUST also omit this aggregate breakdown when the media buy's packages span more than one reporting timezone; package-level daily_breakdown remains, each in its own product's reporting timezone." ), + NoExplicitNull, ] = None @@ -1138,12 +1267,14 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Type of webhook notification (only present in webhook deliveries): scheduled = regular periodic update, final = campaign completed, delayed = data not yet available, adjusted = resending period with corrected data (same window), window_update = resending period with a wider measurement window (e.g., C3 superseding live, C7 superseding C3)' ), + NoExplicitNull, ] = None partial_data: Annotated[ StrictBool | None, Field( description='Indicates if any media buys in this webhook have missing/delayed data (only present in webhook deliveries)' ), + NoExplicitNull, ] = None unavailable_count: Annotated[ SchemaInt | None, @@ -1151,6 +1282,7 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv description='Number of media buys with reporting_delayed or failed status (only present in webhook deliveries when partial_data is true)', ge=0, ), + NoExplicitNull, ] = None sequence_number: Annotated[ SchemaInt | None, @@ -1158,12 +1290,14 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv description='Sequential notification number (only present in webhook deliveries, starts at 1)', ge=1, ), + NoExplicitNull, ] = None next_expected_at: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp for next expected notification (only present in webhook deliveries when notification_type is not 'final')" ), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod, @@ -1176,24 +1310,28 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Present only for a reporting_revision_id selector. Binds the complete ordered reporting_rows sequence obtained by concatenating every cursor page to one immutable Reliable Reporting revision; consumers verify content_sha256 over RFC 8785 JCS of {reporting_revision_id,row_count,control_totals,reporting_rows} before using it as revision evidence.' ), + NoExplicitNull, ] = None reporting_revision: Annotated[ reporting_revision_1.ReportingRevision | None, Field( description='Full immutable revision metadata, identical to the get_reporting_status revision record.' ), + NoExplicitNull, ] = None reporting_rows: Annotated[ list[dict[str, Any]] | None, Field( description="One page of authoritative logical rows for an exact reporting_revision_id read. Validate every row against the revision's digest-pinned schema; concatenate pages in cursor order before verifying the binding digest." ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description='Exact revision pages are frozen for the cursor walk. total_count is mandatory for reporting_revision_id and equals reporting_revision_1.row_count and reporting_revision_binding.row_count; it counts the complete ordered row sequence, not this page.' ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -1202,12 +1340,14 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv description='Deprecated in AdCP 3.2 and removed in AdCP 4.0. Optional legacy response-wide ISO 4217 currency code. It may be used only when every monetary value in the response has that denomination. A delivery response can contain media buys with different currencies, so buyers MUST NOT interpret this field as an aggregation currency or evidence of currency conversion. Prefer media_buy_deliveries[].currency when present and package-level currency otherwise.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None attribution_window: Annotated[ attribution_window_1.AttributionWindow | None, Field( description='Attribution methodology and lookback windows used for conversion metrics in this response. All media buys from a single seller share the same attribution methodology. Enables cross-platform comparison (e.g., Amazon 14-day click vs. Criteo 30-day click).' ), + NoExplicitNull, ] = None aggregated_totals: Annotated[ AggregatedTotals | None, @@ -1215,6 +1355,7 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv deprecated=True, description='Deprecated in AdCP 3.2 and removed in AdCP 4.0. Legacy combined metrics across all returned media buys. When this field is present, the deprecated response-wide currency is required and denominates its spend. Cross-buy totals are unsafe when currencies, metric qualifiers, measurement windows, finality, or deduplication semantics differ. Sellers SHOULD omit this field; buyers SHOULD aggregate media_buy_deliveries[] only when the relevant row semantics are compatible.', ), + NoExplicitNull, ] = None media_buy_deliveries: Annotated[ Sequence[MediaBuyDelivery], @@ -1227,10 +1368,12 @@ class GetMediaBuyDeliveryResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Task-specific errors and warnings (e.g., missing delivery data, reporting platform issues)' ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_media_buys_request.py b/src/adcp/types/domains/media_buy/get_media_buys_request.py index 4a1d6c6e9..d4f37b89f 100644 --- a/src/adcp/types/domains/media_buy/get_media_buys_request.py +++ b/src/adcp/types/domains/media_buy/get_media_buys_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buys_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StrictBool from ..core import account_ref @@ -37,6 +38,7 @@ class GetMediaBuysRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account to retrieve media buys for. When omitted, returns data across all accessible accounts.' ), + NoExplicitNull, ] = None media_buy_ids: Annotated[ list[str] | None, @@ -44,12 +46,14 @@ class GetMediaBuysRequest(AdcpRequest, AdcpVersionEnvelope): description='Array of media buy IDs to retrieve. When omitted, returns a paginated set of accessible media buys matching status_filter.', min_length=1, ), + NoExplicitNull, ] = None status_filter: Annotated[ media_buy_status.MediaBuyStatus | StatusFilter | None, Field( description='Filter by status. Can be a single status or array of statuses. Defaults to ["active"] when media_buy_ids is omitted. When media_buy_ids is provided, no implicit status filter is applied.' ), + NoExplicitNull, ] = None indicator_types: Annotated[ list[indicator_type.IndicatorType] | None, @@ -57,12 +61,14 @@ class GetMediaBuysRequest(AdcpRequest, AdcpVersionEnvelope): description='Return media buys with at least one matching current indicator on the media buy, a package, or a package–creative assignment. Values within this field use OR logic; this field composes with status_filter using AND logic. Buyers MUST NOT send this filter unless the seller advertises every requested type in get_adcp_capabilities.media_buy.supported_indicator_types. A seller MAY reject a request that violates this precondition with UNSUPPORTED_FEATURE rather than silently returning an unfiltered superset.', min_length=1, ), + NoExplicitNull, ] = None include_snapshot: Annotated[ StrictBool | None, Field( description='When true, include a near-real-time delivery snapshot for each package. Snapshots reflect the latest available entity-level stats from the platform (e.g., updated every ~15 minutes on GAM, ~1 hour on batch-only platforms). The staleness_seconds field on each snapshot indicates data freshness. If a snapshot cannot be returned, package.snapshot_unavailable_reason explains why. Defaults to false.' ), + NoExplicitNull, ] = False include_history: Annotated[ SchemaInt | None, @@ -71,12 +77,14 @@ class GetMediaBuysRequest(AdcpRequest, AdcpVersionEnvelope): ge=0, le=1000, ), + NoExplicitNull, ] = 0 include_webhook_activity: Annotated[ StrictBool | None, Field( description="When true, each returned media buy includes a `webhook_activity` array describing recent delivery-report webhook fires for the calling principal. Used by buyer agents to verify whether a publisher actually fired against the buyer's registered endpoint and what the endpoint returned — closes the operator-ticket loop for webhook debugging. Scoped to the calling principal: a buyer sees only fires targeting its own endpoint, even when multiple principals share visibility into the same media buy. Defaults to false. See `webhook_activity_limit` for the per-buy cap." ), + NoExplicitNull, ] = False webhook_activity_limit: Annotated[ SchemaInt | None, @@ -85,12 +93,14 @@ class GetMediaBuysRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=200, ), + NoExplicitNull, ] = 50 pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description='Cursor-based pagination controls. Strongly recommended when querying broad scopes (for example, all active media buys in an account).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_media_buys_response.py b/src/adcp/types/domains/media_buy/get_media_buys_response.py index f502e3021..f11e593c3 100644 --- a/src/adcp/types/domains/media_buy/get_media_buys_response.py +++ b/src/adcp/types/domains/media_buy/get_media_buys_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_media_buys_response.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import account as account_1 @@ -74,7 +74,9 @@ class Cancellation(AdCPBaseModel): canceled_by_1.CanceledBy, Field(description='Which party initiated the cancellation.') ] reason: Annotated[ - str | None, Field(description='Reason the media buy was canceled.', max_length=500) + str | None, + Field(description='Reason the media buy was canceled.', max_length=500), + NoExplicitNull, ] = None @@ -91,6 +93,7 @@ class HistoryItem(AdCPBaseModel): Field( description='Identity of who made the change — derived from authentication context, not caller-provided. Format is seller-defined (e.g., agent URL, user email, API key label).' ), + NoExplicitNull, ] = None action: Annotated[ str, @@ -104,12 +107,14 @@ class HistoryItem(AdCPBaseModel): description="Human-readable summary of the change (e.g., 'Budget increased from $5,000 to $7,500 on pkg_abc').", max_length=500, ), + NoExplicitNull, ] = None package_id: Annotated[ str | None, Field(description='Package affected, when the change targeted a specific package.'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class IndicatorTypesEvaluatedEnum1(StrEnum): @@ -139,7 +144,9 @@ class Cancellation1(AdCPBaseModel): Field(description='Which party initiated the package cancellation.'), ] reason: Annotated[ - str | None, Field(description='Reason the package was canceled.', max_length=500) + str | None, + Field(description='Reason the package was canceled.', max_length=500), + NoExplicitNull, ] = None @@ -186,10 +193,12 @@ class Snapshot(AdCPBaseModel): description='ISO 4217 currency code for spend in this snapshot. Optional when unchanged from package.currency or media_buy.currency.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None clicks: Annotated[ StrictFloat | None, Field(description='Total clicks since package start (when available)', ge=0.0), + NoExplicitNull, ] = None pacing_index: Annotated[ StrictFloat | None, @@ -197,16 +206,19 @@ class Snapshot(AdCPBaseModel): description='Current delivery pace relative to expected (1.0 = on track, <1.0 = behind, >1.0 = ahead). Absent when pacing cannot be determined.', ge=0.0, ), + NoExplicitNull, ] = None delivery_status: Annotated[ delivery_status_1.DeliveryStatus | None, Field( description="Operational delivery state of this package. 'not_delivering' means the package is within its scheduled flight but has delivered zero impressions for at least one full staleness cycle — the signal for automated price adjustments or buyer alerts. Implementers must not return 'not_delivering' until at least staleness_seconds have elapsed since package activation." ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field(description='Optional extension object for seller-specific snapshot fields.'), + NoExplicitNull, ] = None @@ -226,6 +238,7 @@ class Indicator(AdCPBaseModel): Field( description='When the seller first detected the current uninterrupted occurrence of this indicator. Keep this value stable while the condition remains present. If the condition clears and is later detected again, use the new detection time. Optional because some upstream platforms expose current assessments without an original detection timestamp.' ), + NoExplicitNull, ] = None scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -233,12 +246,14 @@ class Indicator(AdCPBaseModel): description='Optional narrower publisher or placement scope within the enclosing media buy, package, or package–creative assignment. Omit only when the seller evaluated and asserts the indicator across the whole enclosing resource/relationship. When partial indicators_evaluated_scope is declared, every returned indicator MUST include scope and every entry MUST fall within that coverage. This is scope, not source: the responding seller remains the source.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Seller- or provider-specific detail such as scores, thresholds, evaluation windows, methodology identifiers, or upstream attribution. Core consumers must not need ext to understand the broad meaning of type.' ), + NoExplicitNull, ] = None @@ -258,6 +273,7 @@ class Indicator1(AdCPBaseModel): Field( description='When the seller first detected the current uninterrupted occurrence of this indicator. Keep this value stable while the condition remains present. If the condition clears and is later detected again, use the new detection time. Optional because some upstream platforms expose current assessments without an original detection timestamp.' ), + NoExplicitNull, ] = None scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -265,12 +281,14 @@ class Indicator1(AdCPBaseModel): description='Optional narrower publisher or placement scope within the enclosing media buy, package, or package–creative assignment. Omit only when the seller evaluated and asserts the indicator across the whole enclosing resource/relationship. When partial indicators_evaluated_scope is declared, every returned indicator MUST include scope and every entry MUST fall within that coverage. This is scope, not source: the responding seller remains the source.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Seller- or provider-specific detail such as scores, thresholds, evaluation windows, methodology identifiers, or upstream attribution. Core consumers must not need ext to understand the broad meaning of type.' ), + NoExplicitNull, ] = None @@ -290,6 +308,7 @@ class Indicator2(AdCPBaseModel): Field( description='When the seller first detected the current uninterrupted occurrence of this indicator. Keep this value stable while the condition remains present. If the condition clears and is later detected again, use the new detection time. Optional because some upstream platforms expose current assessments without an original detection timestamp.' ), + NoExplicitNull, ] = None scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -297,12 +316,14 @@ class Indicator2(AdCPBaseModel): description='Optional narrower publisher or placement scope within the enclosing media buy, package, or package–creative assignment. Omit only when the seller evaluated and asserts the indicator across the whole enclosing resource/relationship. When partial indicators_evaluated_scope is declared, every returned indicator MUST include scope and every entry MUST fall within that coverage. This is scope, not source: the responding seller remains the source.', min_length=1, ), + NoExplicitNull, ] = None ext: Annotated[ ext_1.ExtensionObject | None, Field( description='Seller- or provider-specific detail such as scores, thresholds, evaluation windows, methodology identifiers, or upstream attribution. Core consumers must not need ext to understand the broad meaning of type.' ), + NoExplicitNull, ] = None @@ -316,12 +337,14 @@ class CreativeApproval(IndicatorBearingResourceState): description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', min_length=1, ), + NoExplicitNull, ] = None indicators: Annotated[ list[Indicator2] | None, Field( description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' ), + NoExplicitNull, ] = None creative_id: Annotated[str, Field(description='Creative identifier')] approval_status: creative_approval_status.CreativeApprovalStatus @@ -330,6 +353,7 @@ class CreativeApproval(IndicatorBearingResourceState): Field( description="Human-readable explanation of why the creative was rejected. Present only when approval_status is 'rejected'." ), + NoExplicitNull, ] = None approval_scopes: Annotated[ list[creative_approval_scope.ScopedCreativeApproval] | None, @@ -337,12 +361,14 @@ class CreativeApproval(IndicatorBearingResourceState): description='Complete, disjoint publisher/placement approval partition when approval_status is partially_approved. A normalized scope appears once. For one publisher, use either one publisher-wide row or placement-specific rows, never both. Omit when one approval_status applies uniformly to the whole assignment. The same scoped outcomes are mirrored on list_creatives.', min_length=2, ), + NoExplicitNull, ] = None indicators_as_of: Annotated[ AwareDatetime | None, Field( description='When the seller last completed the evaluation represented by indicators for this relationship. Required whenever indicators is present, including an empty array.' ), + NoExplicitNull, ] = None indicators_evaluated_scope: Annotated[ list[indicator_scope.IndicatorScope] | None, @@ -350,6 +376,7 @@ class CreativeApproval(IndicatorBearingResourceState): description='Optional publisher or placement scopes covered by this evaluation. Omit when indicators covers the whole package–creative assignment. When present, scopes not listed remain unknown; every returned indicator.scope entry MUST be contained by this set.', min_length=1, ), + NoExplicitNull, ] = None @@ -377,12 +404,14 @@ class Package(IndicatorBearingResourceState): description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', min_length=1, ), + NoExplicitNull, ] = None indicators: Annotated[ list[Indicator1] | None, Field( description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' ), + NoExplicitNull, ] = None package_id: Annotated[str, Field(description="Seller's package identifier")] product_id: Annotated[ @@ -390,6 +419,7 @@ class Package(IndicatorBearingResourceState): Field( description="Product identifier this package is purchased from. For packages created from an explicit create_media_buy package request, sellers MUST echo the request package's product_id on every response package object that represents that requested package." ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -397,6 +427,7 @@ class Package(IndicatorBearingResourceState): description='Hard lifetime package spend cap denominated in media_buy.currency. In seller-optimized mode this is not a current allocation.', ge=0.0, ), + NoExplicitNull, ] = None min_spend_target: Annotated[ StrictFloat | None, @@ -404,6 +435,7 @@ class Package(IndicatorBearingResourceState): description='Accepted soft lifetime spend target for this package under seller-optimized allocation.', ge=0.0, ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -411,6 +443,7 @@ class Package(IndicatorBearingResourceState): description='Current hard package spend ceiling per shared media-buy cap day, denominated in media_buy.currency. It is a subordinate ceiling, not a reserved allocation; media_buy.budget_cap_timezone defines the day boundary.', ge=0.0, ), + NoExplicitNull, ] = None currency: Annotated[ str | None, @@ -418,6 +451,7 @@ class Package(IndicatorBearingResourceState): description='Legacy/readback package denomination for buys created outside the canonical 3.2 path. For AdCP-authored media buys this MUST equal media_buy.currency; canonical package budget and BiddingPolicy values always use media_buy.currency. Snapshot currency may still identify externally reported spend denomination.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None bid_price: Annotated[ StrictFloat | None, @@ -426,12 +460,14 @@ class Package(IndicatorBearingResourceState): description='DEPRECATED legacy bid representation. 3.2 sellers SHOULD normalize and echo package.bidding instead.', ge=0.0, ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Package-authored bidding override. `{automatic:true}` explicitly overrides media_buy.bidding with provider automatic delivery. Omitted when this package inherits; sellers MUST NOT copy an inherited block here. Monetary fields use media_buy.currency.' ), + NoExplicitNull, ] = None optimization_goals: Annotated[ list[optimization_goal.OptimizationGoal] | None, @@ -439,6 +475,7 @@ class Package(IndicatorBearingResourceState): description='Current package objective functions. Currency-bearing execution controls are returned separately in package.bidding or inherited from media_buy.bidding.', min_length=1, ), + NoExplicitNull, ] = None format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -447,6 +484,7 @@ class Package(IndicatorBearingResourceState): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format IDs supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including dual-emission cases where another selector won precedence.', min_length=1, ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -454,81 +492,97 @@ class Package(IndicatorBearingResourceState): description='Structured 3.1+ format option references supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it.', min_length=1, ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description='Direct canonical selector supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including informational-echo cases where another selector won precedence.' ), + NoExplicitNull, ] = None params: Annotated[ dict[str, Any] | None, Field( description='Parameters for the direct canonical selector in `format_kind`, echoed from the create_media_buy request whenever the request included it. Requires `format_kind`.' ), + NoExplicitNull, ] = None impressions: Annotated[ StrictFloat | None, Field(description='Goal impression count for impression-based packages', ge=0.0), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Package-level pacing preference. Under seller-optimized allocation this is subordinate to the media-buy aggregate pacing.' ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting.TargetingOverlay | None, Field( description='Complete effective targeting applied to this package, including configured-product targeting and the most recent package-specific overlay. Sellers SHOULD echo persisted targeting so buyers can verify stored state without replaying requests. Sellers MUST echo geo_places and geo_places_exclude whenever either was persisted, including the exact applied system_version and normalized values, so buyers can audit catalog-backed targeting. Sellers using placement, property-list, or collection-list targeting MUST include the committed inventory selection here. placement_selection mode default SHOULD resolve to mode selected with committed refs when enumerable; collection_selection follows the same rule, materializing the committed selectors even when the selection was produced through collection_list references.' ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ package_targeting_resolution.PackageTargetingResolution | None, Field( description='Execution details for accepted package targeting. Sellers MUST include targeting_resolution.demographics whenever demographic targeting was requested or applied; its applied predicate and execution fields report the effective booked state.' ), + NoExplicitNull, ] = None start_time: Annotated[ AwareDatetime | None, Field( description='ISO 8601 flight start time for this package. Use to determine whether the package is within its scheduled flight before interpreting delivery status.' ), + NoExplicitNull, ] = None end_time: Annotated[ - AwareDatetime | None, Field(description='ISO 8601 flight end time for this package') + AwareDatetime | None, + Field(description='ISO 8601 flight end time for this package'), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field(description='Whether this package is currently paused by the buyer'), + NoExplicitNull, ] = None canceled: Annotated[ StrictBool | None, Field( description='Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated.' ), + NoExplicitNull, ] = None cancellation: Annotated[ Cancellation1 | None, Field(description='Cancellation metadata. Present only when canceled is true.'), + NoExplicitNull, ] = None creative_deadline: Annotated[ AwareDatetime | None, Field( description="ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies." ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque package-level correlation data echoed unchanged from the create_media_buy package request. Sellers MUST include persisted package context on read surfaces when the package was created through AdCP with context, so buyers can reconcile seller-assigned package_id values with their own line items; this is the legacy-safe fallback when an older seller did not echo product_id on the create response. Sellers MAY omit context for packages created outside AdCP or created without context. Sellers MUST NOT parse this object for business logic.' ), + NoExplicitNull, ] = None creative_approvals: Annotated[ list[CreativeApproval] | None, Field( description='Approval status for each creative assigned to this package. Absent when no creatives have been assigned.' ), + NoExplicitNull, ] = None formats_to_provide: Annotated[ list[package_format_snapshot.PackageFormatSnapshot] | None, @@ -536,12 +590,14 @@ class Package(IndicatorBearingResourceState): description='The immutable PackageFormatSnapshot checklist established for this package at booking time. Contract-bearing snapshots remain present after creative coverage is complete so readback, assignment, and serving never fall back to a mutable live Product declaration.', min_length=1, ), + NoExplicitNull, ] = None formats_pending: Annotated[ list[package_format_snapshot.PackageFormatSnapshot] | None, Field( description='PackageFormatSnapshot entries from formats_to_provide that do not yet have creative coverage. Each entry MUST be canonically equal to the corresponding checklist snapshot and, when product_snapshot_digest is present, carry the identical digest. An empty emitted array means all requirements are covered; absence means readiness was not reported.' ), + NoExplicitNull, ] = None format_ids_to_provide: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -549,6 +605,7 @@ class Package(IndicatorBearingResourceState): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_to_provide retained for older 3.x peers. New sellers emit canonical formats_to_provide declarations.', ), + NoExplicitNull, ] = None format_ids_pending: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -556,20 +613,23 @@ class Package(IndicatorBearingResourceState): deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_pending retained for older 3.x peers. New sellers emit canonical formats_pending declarations. An empty emitted array means every projected requirement is covered. Absence means legacy readiness was not reported and MUST NOT be interpreted as full coverage.', ), + NoExplicitNull, ] = None snapshot_unavailable_reason: Annotated[ snapshot_unavailable_reason_1.SnapshotUnavailableReason | None, Field( description='Machine-readable reason the snapshot is omitted. Present only when include_snapshot was true and snapshot is unavailable for this package.' ), + NoExplicitNull, ] = None snapshot: Annotated[ Snapshot | None, Field( description='Near-real-time delivery snapshot for this package. Only present when include_snapshot was true in the request. Represents the latest available entity-level stats from the platform — not billing-grade data.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class MediaBuy(IndicatorBearingResourceState): @@ -582,12 +642,14 @@ class MediaBuy(IndicatorBearingResourceState): description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', min_length=1, ), + NoExplicitNull, ] = None indicators: Annotated[ list[Indicator] | None, Field( description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' ), + NoExplicitNull, ] = None media_buy_id: Annotated[str, Field(description="Seller's unique identifier for the media buy")] name: Annotated[ @@ -598,6 +660,7 @@ class MediaBuy(IndicatorBearingResourceState): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None accepted_proposal_id: Annotated[ str | None, @@ -606,6 +669,7 @@ class MediaBuy(IndicatorBearingResourceState): max_length=255, min_length=1, ), + NoExplicitNull, ] = None accepted_proposal_terms_digest: Annotated[ str | None, @@ -613,21 +677,26 @@ class MediaBuy(IndicatorBearingResourceState): description='Digest of the current accepted proposal commercial_terms.', pattern='^sha256:[A-Za-z0-9_-]{43}$', ), + NoExplicitNull, ] = None accepted_proposal: Annotated[ AcceptedProposal | None, Field( description='Current accepted compact proposal, including the complete digested commercial envelope. Required whenever accepted_proposal_id is present so restarted SDKs can recover control-versus-refinement routing without reconstructing historical offers.' ), + NoExplicitNull, ] = None account: Annotated[ - account_1.Account | None, Field(description='Account billed for this media buy') + account_1.Account | None, + Field(description='Account billed for this media buy'), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description='Per-buy invoice recipient when provided at creation. Confirms the seller accepted the billing override. Bank details are omitted (write-only).' ), + NoExplicitNull, ] = None status: media_buy_status.MediaBuyStatus status_as_of: Annotated[ @@ -641,18 +710,21 @@ class MediaBuy(IndicatorBearingResourceState): Field( description='Dependency health of the media buy, orthogonal to `status`. `ok` (default) when no upstream resource that this buy depends on is in an offline state. `impaired` when at least one such resource (audience, creative, catalog_item, event_source, property) is offline and affects delivery for one or more packages — `impairments[]` MUST be non-empty in that case. On terminal-status buys, the seller MAY leave this field in whatever state held at the terminal transition. See lifecycle.mdx § Compliance and the impairment.coherence assertion.' ), + NoExplicitNull, ] = media_buy_health.MediaBuyHealth.ok impairments: Annotated[ list[impairment.Impairment] | None, Field( description='Open impairments — upstream dependency state changes that affect delivery for at least one package on this buy. Empty when `health` is `ok`; non-empty iff `health` is `impaired` (health-iff rule on non-terminal buys). Sellers MUST add an entry on the next read after a referenced resource transitions to an offline state, and MUST remove the entry when the resource returns to a serviceable state or stops being a dependency (e.g., via assignment swap via update_media_buy). Staleness budget: the snapshot MUST reflect the impairment within 5 minutes of `impairment.observed_at` regardless of buyer poll cadence — sellers cannot rely on rare buyer polls to defer write propagation. See impairment.coherence assertion for the cross-resource invariant.' ), + NoExplicitNull, ] = None rejection_reason: Annotated[ str | None, Field( description="Reason provided by the seller when status is 'rejected'. Present only when status is 'rejected'." ), + NoExplicitNull, ] = None currency: Annotated[ str, @@ -673,49 +745,59 @@ class MediaBuy(IndicatorBearingResourceState): description='Current hard aggregate spend ceiling per calendar day, denominated in media_buy.currency. It bounds total spend without allocating or reserving package spend.', ge=0.0, ), + NoExplicitNull, ] = None frequency_cap: Annotated[ media_buy_frequency_cap.MediaBuyFrequencyCap | None, Field( description='Current hard MediaBuy-level cap. Sellers MUST echo it whenever set, separately from package targeting-overlay caps.' ), + NoExplicitNull, ] = None budget_cap_timezone: Annotated[ str | None, Field( description='IANA timezone defining the shared calendar-day boundary for every aggregate and package daily cap. Present whenever any daily cap is set on the media buy.' ), + NoExplicitNull, ] = None budget_allocation: Annotated[ budget_allocation_1.BudgetAllocation | None, Field( description='Current cross-package allocation configuration. Omitted means fixed allocation for legacy buys.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field(description='Aggregate pacing strategy for the media-buy budget.'), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Current media-buy-authored bidding policy with scope-specific goal binding and media-buy-currency denomination. Package entries omit bidding when they inherit this block; explicit package automatic overrides remain visible as `{automatic:true}`.' ), + NoExplicitNull, ] = None start_time: Annotated[ AwareDatetime | None, Field( description='ISO 8601 flight start time for this media buy (earliest package start_time). Avoids requiring buyers to compute min(packages[].start_time).' ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description='ISO 8601 flight end time for this media buy (latest package end_time). Avoids requiring buyers to compute max(packages[].end_time).' ), + NoExplicitNull, ] = None creative_deadline: Annotated[ - AwareDatetime | None, Field(description='ISO 8601 timestamp for creative upload deadline') + AwareDatetime | None, + Field(description='ISO 8601 timestamp for creative upload deadline'), + NoExplicitNull, ] = None confirmed_at: Annotated[ AwareDatetime | None, @@ -726,6 +808,7 @@ class MediaBuy(IndicatorBearingResourceState): cancellation: Annotated[ Cancellation | None, Field(description="Cancellation metadata. Present only when status is 'canceled'."), + NoExplicitNull, ] = None revision: Annotated[ SchemaInt, @@ -734,13 +817,18 @@ class MediaBuy(IndicatorBearingResourceState): ge=1, ), ] - created_at: Annotated[AwareDatetime | None, Field(description='Creation timestamp')] = None - updated_at: Annotated[AwareDatetime | None, Field(description='Last update timestamp')] = None + created_at: Annotated[ + AwareDatetime | None, Field(description='Creation timestamp'), NoExplicitNull + ] = None + updated_at: Annotated[ + AwareDatetime | None, Field(description='Last update timestamp'), NoExplicitNull + ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque media-buy-level correlation data echoed unchanged from the create_media_buy request. Sellers MUST include persisted context on read surfaces when the media buy was created through AdCP with context, so buyers can reconcile seller-assigned media_buy_id values with their own tracking state. Sellers MAY omit context for media buys created outside AdCP or created without context. Sellers MUST NOT parse this object for business logic.' ), + NoExplicitNull, ] = None valid_actions: Annotated[ list[media_buy_valid_action.MediaBuyValidAction] | None, @@ -748,6 +836,7 @@ class MediaBuy(IndicatorBearingResourceState): deprecated=True, description='Flat-vocabulary actions the buyer can perform on this media buy in its current state. Eliminates the need for agents to internalize the state machine — the seller declares what is permitted right now. Deprecated in favor of `available_actions[]`, which carries mode, optional SLA, and a 3.2 change_term_id link. Sellers SHOULD populate both during the 3.x deprecation window; consumers MUST prefer `available_actions[]` when both are present. Removed in 4.0.', ), + NoExplicitNull, ] = None available_actions: Annotated[ list[ @@ -758,6 +847,7 @@ class MediaBuy(IndicatorBearingResourceState): Field( description="Structured per-buy resolution of the actions buyer can perform right now. Authoritative — divergence from product `allowed_actions[]` is expected because accepted proposal terms, current state, authorization, and governance delegation are buy-specific. Each entry carries the resolved mode, optional SLA commitment, and in 3.2 an optional change_term_id linking the accepted proposal right. Deprecated 3.1 terms_ref remains readable as an opaque compatibility pointer. Predicate queries via #4425's `requires` grammar address fields by dotted path, e.g. `available_actions.extend_flight.sla.response_max`. Absent SLA means no commitment, not zero commitment — callers composing duration predicates MUST also compose with `present: true` to avoid silently matching sellers who never declared one." ), + NoExplicitNull, ] = None webhook_activity: Annotated[ list[webhook_activity_record.WebhookActivityRecord] | None, @@ -765,12 +855,14 @@ class MediaBuy(IndicatorBearingResourceState): description='Recent webhook fires relevant to this buy for the calling principal, most-recent first. Includes per-buy delivery/health fires and account-anchored indicators.changed or creative.assignment_changed invalidations whose payload names this media_buy_id. Present only when include_webhook_activity was true and the seller surfaces this debug capability. Account-anchored records MUST include subscriber_id. Three-state presence and the 30-day retention floor follow snapshot-and-log.mdx § Webhook activity log pattern.', max_length=200, ), + NoExplicitNull, ] = None history: Annotated[ list[HistoryItem] | None, Field( description='Revision history entries, most recent first. Only present when include_history > 0 in the request. Each entry represents a state change or update to the media buy. Entries are append-only: sellers MUST NOT modify or delete previously emitted history entries. Callers MAY cache entries by revision number. Returns min(N, available entries) when include_history exceeds the total.' ), + NoExplicitNull, ] = None packages: Annotated[ Sequence[Package], @@ -778,7 +870,7 @@ class MediaBuy(IndicatorBearingResourceState): description='Packages within this media buy, augmented with creative approval status and optional delivery snapshots' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class GetMediaBuysResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): @@ -796,14 +888,17 @@ class GetMediaBuysResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): Field( description='Task-specific errors. A read may return media_buys plus nonfatal resource-state errors. If a pinned place target becomes unexecutable, sellers MUST include PLACE_TARGET_UNAVAILABLE with recovery=correctable, field pointing to the exact media_buys[N].packages[M].targeting_overlay.geo_places[_exclude][A].values[V] response path, and details containing media_buy_id, package_id, system, system_version, country, place_type, and value. The persisted target remains echoed until an intentional update replaces it.' ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_response.PaginationResponse | None, Field(description='Pagination metadata for the media_buys array.'), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_async_response_input_required.py b/src/adcp/types/domains/media_buy/get_products_async_response_input_required.py index 30c6eba03..cc700a75f 100644 --- a/src/adcp/types/domains/media_buy/get_products_async_response_input_required.py +++ b/src/adcp/types/domains/media_buy/get_products_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_async_response_input_required.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -25,14 +25,19 @@ class GetProductsInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None partial_results: Annotated[ list[product.Product] | None, Field(description='Partial product results that may help inform the clarification'), + NoExplicitNull, ] = None suggestions: Annotated[ - list[str] | None, Field(description='Suggested values or options for the required input') + list[str] | None, + Field(description='Suggested values or options for the required input'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_async_response_submitted.py b/src/adcp/types/domains/media_buy/get_products_async_response_submitted.py index 32193d55f..1699e6a90 100644 --- a/src/adcp/types/domains/media_buy/get_products_async_response_submitted.py +++ b/src/adcp/types/domains/media_buy/get_products_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import context as context_1 @@ -36,15 +36,19 @@ class GetProductsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Custom curation queued; typical turnaround 10–30 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None estimated_completion: Annotated[ - AwareDatetime | None, Field(description='Estimated completion time for the search') + AwareDatetime | None, + Field(description='Estimated completion time for the search'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_async_response_working.py b/src/adcp/types/domains/media_buy/get_products_async_response_working.py index b34f235a2..1f011b9e8 100644 --- a/src/adcp/types/domains/media_buy/get_products_async_response_working.py +++ b/src/adcp/types/domains/media_buy/get_products_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -20,18 +20,22 @@ class GetProductsWorking(AdCPBaseModel): percentage: Annotated[ StrictFloat | None, Field(description='Progress percentage of the search operation', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step in the search process (e.g., 'searching_inventory', 'validating_availability')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the search process') + SchemaInt | None, + Field(description='Total number of steps in the search process'), + NoExplicitNull, ] = None step_number: Annotated[ - SchemaInt | None, Field(description='Current step number (1-indexed)') + SchemaInt | None, Field(description='Current step number (1-indexed)'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_rejected.py b/src/adcp/types/domains/media_buy/get_products_rejected.py index 4b1326afe..52e1a2d8f 100644 --- a/src/adcp/types/domains/media_buy/get_products_rejected.py +++ b/src/adcp/types/domains/media_buy/get_products_rejected.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_rejected.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations -from adcp.types._scalar import ScalarStr - from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -46,6 +46,7 @@ class GetProductsRejected(AdcpVersionEnvelope, ProtocolEnvelope): max_length=20, min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_request.py b/src/adcp/types/domains/media_buy/get_products_request.py index 292ed4955..874479ba1 100644 --- a/src/adcp/types/domains/media_buy/get_products_request.py +++ b/src/adcp/types/domains/media_buy/get_products_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref, brand_ref @@ -76,6 +76,7 @@ class Refine2(AdCPBaseModel): Field( description="'include' (default): return this product with updated pricing and data. 'omit': exclude this product from the response. 'more_like_this': find additional products similar to this one (the original is also returned). Optional — when omitted, the seller treats the entry as action: 'include'." ), + NoExplicitNull, ] = Action.include ask: Annotated[ str | None, @@ -83,6 +84,7 @@ class Refine2(AdCPBaseModel): description="What the buyer is asking for on this product. For 'include': specific changes to request (e.g., 'add 16:9 format'). For 'more_like_this': what 'similar' means (e.g., 'same audience but video format'). Ignored when action is 'omit'.", min_length=1, ), + NoExplicitNull, ] = None @@ -107,6 +109,7 @@ class Refine3(AdCPBaseModel): Field( description="'include' (default): return this proposal with updated allocations and pricing. 'omit': exclude this proposal from the response. 'finalize': request firm pricing and inventory hold. New callers use refine_proposals with action revise for a draft successor or action finalize for a committed held successor; terminal feedback is available through decline_proposals.\n\nLegacy finalize is exclusive within the parent `refine[]` array: see the array-level description for the finalize-exclusivity rule (mixing finalize with non-finalize entries is rejected) and multi-finalize atomicity contract." ), + NoExplicitNull, ] = Action9.include ask: Annotated[ str | None, @@ -114,6 +117,7 @@ class Refine3(AdCPBaseModel): description="What the buyer is asking for on this proposal (e.g., 'shift more budget toward video', 'reduce total by 10%'). Ignored when action is omit.", min_length=1, ), + NoExplicitNull, ] = None @@ -213,6 +217,7 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None buying_mode: Annotated[ BuyingMode, @@ -225,6 +230,7 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Natural language description of campaign requirements. Required when buying_mode is 'brief'. Must not be provided when buying_mode is 'wholesale' or 'refine'. Buyers SHOULD use structured fields for every requirement that can be expressed structurally, and reserve brief prose for goals, context, preferences, and requirements without a structured representation. Sellers MUST apply explicit hard requirements stated in the brief even when the buyer did not duplicate them in a structured field. When a seller translates hard prose into structured targeting that materially affects product eligibility, pricing, or forecasting, it MUST confirm that interpretation once in GetProductsResponse.targeting_resolution.brief_targeting; otherwise confirmation remains a best practice. If hard prose contradicts a structured field, sellers MUST reject the request with INVALID_REQUEST rather than choose one interpretation or return an unexplained empty result." ), + NoExplicitNull, ] = None refine: Annotated[ list[Refine] | None, @@ -232,30 +238,35 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): description="Array of change requests for iterating on products and proposals from a previous get_products response. Each entry declares a scope (request, product, or proposal) and what the buyer is asking for. Only valid when buying_mode is 'refine'. The seller responds to each entry via refinement_applied in the response, matched by position.\n\nFinalize-exclusivity rule: if any entry has `action: 'finalize'`, ALL entries in the array MUST be proposal-scoped with `action: 'finalize'` — mixing finalize entries with `include`/`omit` entries or with request- / product-scoped entries MUST be rejected by the seller with `INVALID_REQUEST`. Finalize is a commit, not a refinement; the buyer expressing intent to commit means refinements have already converged. Buyers needing to refine AND commit in close succession sequence the calls: first a refine call (no finalize), then a finalize call against the resulting `proposal_id`(s).\n\nMulti-finalize semantics: multiple finalize entries against different `proposal_id` values in a single call are allowed and MUST be **atomic at the observation point** — sellers MUST NOT return a success response unless every named proposal has both completed and been persisted as committed. Pre-commit validation runs before any side-effects (inventory pull, terms lock, governance attestation); if any proposal fails validation, the seller MUST reject the entire call without committing any of the named proposals. There is no rollback operation in the spec — an `unfinalize` would itself be a new mutation surface; the atomicity guarantee runs entirely on the seller's pre-commit validation gate, not on post-commit reversal. Sellers that cannot guarantee atomic pre-commit validation MUST reject multi-finalize arrays with `MULTI_FINALIZE_UNSUPPORTED` (preferred — distinguishes seller-side capability gap from a malformed request) or `INVALID_REQUEST` (acceptable fallback for sellers on a pre-3.1 error catalog). If a mid-commit failure occurs *after* validation passed but before all proposals persist (e.g., a downstream ad server fails between commits one and two), the seller MUST return `INTERNAL_ERROR` with `refinement_applied[]` carrying per-position outcomes — the spec does NOT define a recovery path for this case, and buyers SHOULD treat the resulting state as undefined and re-read via `get_media_buys` / equivalent before retrying. Buyers MUST NOT assume multi-finalize support without a successful first attempt — there is no capability flag for this; the failure response is the discovery surface. Buyers whose intent specifically requires atomic commit (e.g., budget-shared proposals where one finalizing without the other is incoherent) MUST be prepared to abandon the intent if the seller returns `MULTI_FINALIZE_UNSUPPORTED` — there is no recovery for that loss of buyer intent beyond sequencing single-finalize calls and accepting the looser commit guarantee.", min_length=1, ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference for product discovery context. Resolved to full brand identity at execution time.' ), + NoExplicitNull, ] = None acceptance_context: Annotated[ acceptance_context_1.AcceptanceContext | None, Field( description='Structured campaign and advertiser facts for seller acceptance-policy preflight. Sellers may infer omitted facts from brand and brief, but uncertainty never implies acceptance.' ), + NoExplicitNull, ] = None catalog: Annotated[ catalog_1.Catalog | None, Field( description='Catalog of items the buyer wants to promote. The seller matches catalog items against its inventory and returns products where matches exist. Supports all catalog types: a job catalog finds job ad products, a product catalog finds sponsored product slots. Reference a synced catalog by catalog_id, or provide inline items.' ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference | None, Field( description="Account for product lookup. Returns products with pricing specific to this account's rate card. The account must already be provisioned: a reference that does not resolve returns ACCOUNT_NOT_FOUND rather than public products, unless the seller provisions lazily. Omit account (and send brand for discovery context) until sync_accounts succeeds. See /docs/accounts/overview#account-references-before-provisioning." ), + NoExplicitNull, ] = None preferred_delivery_types: Annotated[ list[delivery_type.DeliveryType] | None, @@ -263,36 +274,42 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): description='Delivery types the buyer prefers, in priority order. Unlike filters.delivery_type which excludes non-matching products, this signals preference for curation — the publisher may still include other delivery types when they match the brief well.', min_length=1, ), + NoExplicitNull, ] = None filters: Annotated[ product_filters.ProductFilters | None, Field( description="Offer filters. Valid in brief, wholesale, and refine modes. In every mode, sellers MUST exclude products that do not satisfy them: brief controls curation, wholesale controls feed behavior, and refine controls iteration, but none changes filter semantics. On refine, presence is the complete replacement filter state; when omitted, each referenced product's bound discovery constraints remain in force. Targeting-like legacy fields remain accepted during migration but are deprecated in favor of targeting_overlay and required_overlay_support." ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting.TargetingOverlay | None, Field( description="Concrete delivery constraints the buyer expects to carry into create_media_buy. Buyers SHOULD use this field instead of putting equivalent exact targeting only in brief prose. Sellers evaluate these constraints during discovery and scope returned pricing and forecasts to the effective targeting. If a product cannot honor the request exactly, the seller may omit it or return a request-scoped configured product with Product targeting_resolution modifications. Absence of Product targeting_resolution means exact acceptance of this structured overlay; it does not confirm how targeting in the brief was interpreted. On refine, presence is complete replacement state for returned configurations; when omitted, each referenced product's bound targeting remains in force." ), + NoExplicitNull, ] = None media_buy_frequency_cap: Annotated[ media_buy_frequency_cap_1.MediaBuyFrequencyCap | None, Field( description='Concrete aggregate max-impression cap intended for the MediaBuy root. Every returned product must participate in one shared counter for this exact value. Legacy proposals echo it as proposals[].frequency_cap; direct buyers repeat it at purchase.' ), + NoExplicitNull, ] = None required_overlay_support: Annotated[ targeting_overlay_requirements.TargetingOverlayRequirements | None, Field( description="Minimum product-scoped targeting dimensions the buyer must be able to set independently on packages later. This requests selectable capability, not current targeting values, value-specific availability or forecasts, and not one product per possible value. A requirement true matches support true or any valid support object; an object requirement matches support true or an object containing every requested boolean and requested array subset. The backward-compatible daypart_targets support true is the exception: it satisfies inventory_local requirements only, not iana requirements. Unrequested object fields and numeric limits do not participate. Missing or unknown requirement fields do not match. Seller limit fields are response-only and cannot be requested here. On refine, presence is complete replacement state; when omitted, each referenced product's bound future-support requirements remain in force." ), + NoExplicitNull, ] = None required_media_buy_support: Annotated[ media_buy_support_requirements.ProductMediaBuySupportRequirements | None, Field( description='Minimum product participation in shared MediaBuy-level controls. For a frequency cap, every returned product must be composable in one seller-maintained aggregate counter for every requested per unit. On refine, presence is complete replacement state.' ), + NoExplicitNull, ] = None property_list: Annotated[ property_list_ref.PropertyListReference | None, @@ -300,6 +317,7 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description='DEPRECATED discovery-only property filter. On compact discovery tasks use criteria.offer_filters.property_list to preserve product eligibility. Use targeting_overlay.property_list only when the buyer intends a delivery constraint, or required_overlay_support.property_list for future selectability. A facade MUST NOT convert this discovery filter into delivery targeting.', ), + NoExplicitNull, ] = None fields: Annotated[ list[product_fields.ProductResponseField | Fields] | None, @@ -307,42 +325,49 @@ class GetProductsRequest(AdcpRequest, AdcpVersionEnvelope): description='Specific product fields to include in the response. When omitted, all fields are returned. Use for lightweight discovery calls where only a subset of product data is needed. product_id and name are always included. `format_ids` is a deprecated 3.x compatibility projection; new integrations request canonical `format_options`. Safety-critical request-specific fields override projection: Product.targeting_resolution and expires_at MUST be included whenever the seller returns modifications, overlay_support MUST be included when required_overlay_support was requested, media_buy_support MUST be included when required_media_buy_support or media_buy_frequency_cap was requested, list_applications MUST be included when a property or collection list is in the effective targeting, and audience_evidence_selections MUST be included when filters.audience_evidence_requirements affects eligibility or ranking. fields controls the optional audience_evidence payload, not either decision receipt. Response-level brief targeting confirmation is not a projected product field.', min_length=1, ), + NoExplicitNull, ] = None time_budget: Annotated[ duration.Duration | None, Field( description='Maximum time the buyer will commit to this request. The seller returns the best results achievable within this budget and does not start processes (human approvals, expensive external queries) that cannot complete in time. When omitted, the seller decides timing.' ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async terminal completion/failure notifications on curated discovery. Meaningful only for `buying_mode: "brief"` and `buying_mode: "refine"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief/refine request includes this field and the seller returns a Submitted envelope, the seller MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the seller cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: sellers MUST NOT route `buying_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.' ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description="Cursor-based pagination controls for get_products. Valid in all buying modes. In brief mode, pagination bounds the seller's returned products[] for the curated answer to the brief and is not an exhaustive catalog-enumeration contract. In refine mode, pagination bounds the refined products[] result implied by refine[] and filters; proposals may accompany a page as plan metadata but are not independently counted by this pagination envelope. In wholesale mode, pagination walks the wholesale product feed and may be combined with wholesale feed versioning." ), + NoExplicitNull, ] = None if_wholesale_feed_version: Annotated[ str | None, Field( description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_products response from this agent. Only valid when buying_mode is wholesale. When provided, the seller compares against its current wholesale product feed version for the buyer's cache_scope and MAY return an unchanged: true response (with products omitted) if nothing has changed. The token is scope-keyed: buyers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, buying_mode, filters, targeting_overlay, media_buy_frequency_cap, required_overlay_support, required_media_buy_support, deprecated property_list, catalog) for cache_scope: 'public'; that tuple plus account identity for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. Backward-compatible: pre-v3.1 agents that ignore this field simply return the full payload, same as the unchanged-server path. See specs/wholesale-feed-webhooks.md for the full sync pattern." ), + NoExplicitNull, ] = None if_pricing_version: Annotated[ str | None, Field( description="Opaque pricing_version token from a prior get_products response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → seller returns the full payload (pricing is implicitly stale); (2) if_wholesale_feed_version matches but if_pricing_version mismatches → seller returns the full payload so the buyer sees updated pricing_options; (3) both match → seller MAY return unchanged: true. Agents that don't track pricing separately ignore if_pricing_version and fall back to if_wholesale_feed_version semantics. Useful for storefronts that re-price compositions far more often than they re-render product mirrors." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None required_policies: Annotated[ list[str] | None, Field( description='Registry policy IDs that the buyer requires to be enforced for products in this response. Sellers filter products to only those that comply with or already enforce the requested policies.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_response.py b/src/adcp/types/domains/media_buy/get_products_response.py index 06e34d9e9..81608c267 100644 --- a/src/adcp/types/domains/media_buy/get_products_response.py +++ b/src/adcp/types/domains/media_buy/get_products_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool, StringConstraints @@ -43,8 +43,9 @@ class Extensions(AdCPBaseModel): Field( description='Semantic version of the extension definition. Distinct from the digest — version is human-readable; digest is the integrity check.' ), + NoExplicitNull, ] = None - description: str | None = None + description: Annotated[str | None, NoExplicitNull] = None class Suggestion(ScalarStr): @@ -77,6 +78,7 @@ class RefinementApplied1(AdCPBaseModel): Field( description="Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." ), + NoExplicitNull, ] = None @@ -102,6 +104,7 @@ class RefinementApplied2(AdCPBaseModel): Field( description="Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." ), + NoExplicitNull, ] = None @@ -127,6 +130,7 @@ class RefinementApplied3(AdCPBaseModel): Field( description="Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." ), + NoExplicitNull, ] = None @@ -162,6 +166,7 @@ class IncompleteItem(AdCPBaseModel): Field( description='How much additional time is expected to resolve this scope. Allows the buyer to decide whether to make a new attempt with a larger time_budget; its presence is not an automatic-retry instruction.' ), + NoExplicitNull, ] = None @@ -187,12 +192,14 @@ class ExcludedBy(AdCPBaseModel): Field( description='Optional list of the specific filter values that contributed to exclusions, when meaningful. For `required_metrics`: the metric names that excluded products (strings). For `required_vendor_metrics`: the vendor/metric pin entries (objects). Item shape is filter-specific; the schema admits string OR object items. Buyers without filter-specific knowledge SHOULD treat as opaque.' ), + NoExplicitNull, ] = None notes: Annotated[ str | None, Field( description="Optional human-readable note about why this filter narrowed the set (e.g., 'no products in this brief support DV viewability at the requested threshold')." ), + NoExplicitNull, ] = None @@ -205,6 +212,7 @@ class FilterDiagnostics(AdCPBaseModel): Field( description="How `excluded_by[*].count` values are computed across multiple filters. `only`: counts products that would have been included if not for THIS filter alone (deterministic; the right value for 'which filter killed my result set' triage — recommended when feasible). `any`: counts products excluded by ANY filter (so multiple filters' counts may overlap and sum to more than `total_candidates`). `approximate`: sellers SHOULD use this when their pipeline can't cleanly attribute exclusions to a single filter. Buyers SHOULD inspect `semantics` before doing arithmetic on counts." ), + NoExplicitNull, ] = None total_candidates: Annotated[ SchemaInt | None, @@ -212,12 +220,14 @@ class FilterDiagnostics(AdCPBaseModel): description='Number of products the seller considered before applying `filters`. Baseline for interpreting per-filter exclusion counts. Approximate — sellers MAY return a sampled or capped count when their candidate pool is large. Optional; sellers whose baseline candidate set size is sensitive (revealing market posture or competitive density) MAY omit this while still emitting `excluded_by`.', ge=0, ), + NoExplicitNull, ] = None excluded_by: Annotated[ dict[str, ExcludedBy] | None, Field( description="Per-filter exclusion counts, keyed by the filter property name as it appears in the request's `filters` object (e.g., `pricing_currencies`, `required_metrics`, `required_vendor_metrics`, `required_geo_targeting`, `budget_range`). Values are objects carrying `count` and optional filter-specific detail. Only filters that actually narrowed the set need appear here; absence of a key means that filter did not exclude anything (or was not in the request)." ), + NoExplicitNull, ] = None @@ -231,29 +241,35 @@ class GetProductsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): extra='allow', ) products: Annotated[ - list[product.Product] | None, Field(description='Array of matching products') + list[product.Product] | None, + Field(description='Array of matching products'), + NoExplicitNull, ] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description='Request-level confirmation of structured hard targeting inferred from the brief. Sellers MUST include this when their structured interpretation of hard prose materially affects product eligibility, pricing, or forecasting; otherwise inclusion is a best practice. Omitted when no hard targeting was inferred from the brief.' ), + NoExplicitNull, ] = None extensions: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^https?://[^@]+@sha256:[a-f0-9]{64}$')], Extensions] | None, Field( description='Bundled platform-extension definitions referenced by any product in `products`. Keyed by `@` (e.g., `https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel@sha256:abc...`). When present, lets buyers resolve `platform_extensions` references on product format declarations without a separate fetch. Buyer SDKs cache by URI@digest; subsequent get_products responses MAY omit definitions the buyer already has cached and rely on the digest match. Each value is an extension definition with `extends` (the canonical concept it extends, e.g., `tracking`), `fields` (the schema for additional fields the extension contributes), `version`, and optional `description`.' ), + NoExplicitNull, ] = None proposals: Annotated[ list[proposal.Proposal] | None, Field( description='Optional legacy proposed media plans. When the request carries media_buy_frequency_cap, every returned proposal echoes the bound value as proposal.frequency_cap. Buyers may refine or execute a committed proposal by ID.' ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field(description='Task-specific errors and warnings (e.g., product filtering issues)'), + NoExplicitNull, ] = None reason: Annotated[ str | None, @@ -262,6 +278,7 @@ class GetProductsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): max_length=2000, min_length=1, ), + NoExplicitNull, ] = None suggestions: Annotated[ list[Suggestion] | None, @@ -269,24 +286,28 @@ class GetProductsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): description='Actionable alternatives available only on the GetProductsRejected arm.', max_length=20, ), + NoExplicitNull, ] = None property_list_applied: Annotated[ StrictBool | None, Field( description='[AdCP 3.0] Indicates whether deprecated top-level property_list filtering was applied. True if the agent filtered products based on the provided property_list; every returned product also carries the corresponding property/include list_applications receipt. Absent or false if property_list was not provided or not supported by this agent.' ), + NoExplicitNull, ] = None catalog_applied: Annotated[ StrictBool | None, Field( description='Whether the seller filtered results based on the provided catalog. True if the seller matched catalog items against its inventory. Absent or false if no catalog was provided or the seller does not support catalog matching.' ), + NoExplicitNull, ] = None refinement_applied: Annotated[ list[RefinementApplied] | None, Field( description="Seller's response to each change request in the refine array, matched by position. Each entry acknowledges whether the corresponding ask was applied, partially applied, or unable to be fulfilled. MUST contain the same number of entries in the same order as the request's refine array. Only present when the request used buying_mode: 'refine'. Each entry MUST echo the request entry's scope and — for product and proposal scopes — the matching id field (product_id or proposal_id), so orchestrators can cross-validate alignment." ), + NoExplicitNull, ] = None incomplete: Annotated[ list[IncompleteItem] | None, @@ -294,6 +315,7 @@ class GetProductsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): description="Declares what the seller could not finish within the buyer's time_budget or due to internal limits while still returning a usable response. Each entry identifies a scope that is missing or partial. Absent when the response is fully complete. This field does not classify the condition as retryable; retryability is carried by error.recovery on the error channel.", min_length=1, ), + NoExplicitNull, ] = None filter_diagnostics: Annotated[ FilterDiagnostics | None, @@ -312,40 +334,47 @@ class GetProductsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): } ], ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_response.PaginationResponse | None, Field( description="Cursor metadata for paginated get_products responses. In brief/refine mode, continuation pages bound returned products[] for the seller's curated or refined answer; proposals may accompany a page as plan metadata but are not independently counted by this pagination envelope, and pagination does not convert the response into an exhaustive feed contract. In wholesale mode, continuation pages walk the wholesale product feed." ), + NoExplicitNull, ] = None wholesale_feed_version: Annotated[ str | None, Field( description="Opaque token representing the version of the wholesale product feed state used to compose this response. Sellers that implement conditional-fetch (if_wholesale_feed_version) MUST return this on every wholesale-mode response so buyers can cache and probe later. Buyers MUST treat the value as opaque — no format, no ordering, no inspection. The token is scope-keyed: it describes a version for the cache_scope declared on this response, NOT a global agent version. A buyer caches `(cache_scope, wholesale_feed_version)` pairs and presents the matching token on the next request. Scoping dimensions: (agent, buying_mode, filters, targeting_overlay, media_buy_frequency_cap, required_overlay_support, required_media_buy_support, deprecated property_list, catalog) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full cache layering model." ), + NoExplicitNull, ] = None pricing_version: Annotated[ str | None, Field( description='Opaque token representing the version of the pricing layer, including product pricing_options and nested signal_targeting_options pricing_options. When the seller supports independent pricing versioning, pricing_version changes when prices move but wholesale_feed_version changes only when structure/metadata moves. Same cache_scope keying as wholesale_feed_version. Sellers not separating these MAY omit pricing_version and use wholesale_feed_version for both.' ), + NoExplicitNull, ] = None cache_scope: Annotated[ CacheScope | None, Field( description="Declares whether the wholesale_feed_version and pricing_version on this response describe a universal layer or an account-specific overlay. REQUIRED on every 3.1+ response (the 3.1 schema enforces this — the safety property of the two-layer cache model depends on it). 'public': this response describes the seller's published rate card; the buyer MAY dedupe under (agent, buying_mode, filters, targeting_overlay, media_buy_frequency_cap, required_overlay_support, required_media_buy_support, deprecated property_list, catalog) without scoping by account. 'account': this response includes account-specific overrides; the buyer MUST cache the version under that tuple plus account_id. When the request did NOT include `account`, the seller MUST return `cache_scope: 'public'`. When the request included `account`, the seller MUST return either: 'public' (this account prices off the public rate card — buyer dedupes) or 'account' (account-specific overrides exist — buyer caches under the account key). Sellers MAY return 'public' on an account-scoped request that previously had overrides — buyers SHOULD interpret this as a downgrade and drop their account-overlay. Without schema-required cache_scope, a seller silently omitting the field on an account-scoped response would cause buyers to mis-key the cache and serve account-overlay payloads to other accounts. **Backward-compatibility note for 3.1 validators:** SDKs that validate strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version` (release-precision version negotiation, 3.1). For responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed." ), + NoExplicitNull, ] = CacheScope.public unchanged: Annotated[ Literal[True] | None, Field( description="Present and `true` ONLY on wholesale-mode responses when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the seller's current version for the buyer's cache_scope, in which case products[] MUST be omitted; wholesale_feed_version (echoed), cache_scope (echoed), and pricing_version (echoed when used) MUST still be present. Buyers receiving unchanged: true MUST NOT mutate their local wholesale product mirror. **One shape per state:** sellers MUST NOT emit `unchanged: false` — the absence of the field IS the signal that the response carries products. Two shapes ({ unchanged: false, products: [...] } vs. { products: [...] }) for the same state would let some sellers always emit the field and some never would, creating an inconsistency the wire shouldn't carry. **Cross-scope isolation:** the comparator that decides `unchanged` MUST be keyed on `(cache_scope, wholesale_feed_version)`, not on the token value alone. A seller MUST NOT emit `unchanged: true` when it resolves the request to a different `cache_scope` than the one whose token the buyer echoed in `if_wholesale_feed_version` (and/or `if_pricing_version`): because the token is scope-keyed, a value minted for `cache_scope: 'public'` cannot match the seller's current token for `cache_scope: 'account'` (or vice-versa), so such a request MUST return the full feed for the resolved scope with that scope's own token." ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_products_targeting_resolution.py b/src/adcp/types/domains/media_buy/get_products_targeting_resolution.py index 91dc300cb..21b50e6a1 100644 --- a/src/adcp/types/domains/media_buy/get_products_targeting_resolution.py +++ b/src/adcp/types/domains/media_buy/get_products_targeting_resolution.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/get_products_targeting_resolution.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import ext as ext_1 @@ -23,4 +23,4 @@ class ProductDiscoveryTargetingResolution(AdCPBaseModel): description="Structured delivery constraints the seller resolved from explicit hard targeting in the brief. This is the seller's literal interpretation, not a counteroffer: products that cannot honor it are omitted. Sellers MUST emit this confirmation when the interpretation materially affects product eligibility, pricing, or forecasting; otherwise it is a best practice. Unchanged structured targeting_overlay values are not repeated." ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_reporting_status_request.py b/src/adcp/types/domains/media_buy/get_reporting_status_request.py index 5c1c57281..140437f82 100644 --- a/src/adcp/types/domains/media_buy/get_reporting_status_request.py +++ b/src/adcp/types/domains/media_buy/get_reporting_status_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_reporting_status_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -53,6 +53,7 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): max_length=100, min_length=1, ), + NoExplicitNull, ] = None delivery_config_ids: Annotated[ list[DeliveryConfigId] | None, @@ -61,6 +62,7 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): max_length=16, min_length=1, ), + NoExplicitNull, ] = None feed_purposes: Annotated[ list[reporting_delivery_offering.ReportingFeedPurpose] | None, @@ -68,12 +70,14 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): description='Optional summary/periods feed filter. The response echoes exact resolved configuration generations so this never creates an opaque aggregate.', min_length=1, ), + NoExplicitNull, ] = None period: Annotated[ Period | None, Field( description="Half-open summary/periods horizon. Omit for the seller's documented operational default horizon; the response always echoes the evaluated scope." ), + NoExplicitNull, ] = None health: Annotated[ list[reporting_health.ReportingHealth] | None, @@ -81,10 +85,11 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): description='Periods-view result filter only; it never changes summary health.', min_length=1, ), + NoExplicitNull, + ] = None + finality: Annotated[ + list[reporting_finality.ReportingFinality] | None, Field(min_length=1), NoExplicitNull ] = None - finality: Annotated[list[reporting_finality.ReportingFinality] | None, Field(min_length=1)] = ( - None - ) reporting_revision_id: Annotated[ str | None, Field( @@ -93,6 +98,7 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None changes_after: Annotated[ str | None, @@ -101,12 +107,14 @@ class GetReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): max_length=2048, min_length=1, ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description='Periods or revision-view pagination. Cursors are bound to the authenticated caller, account, filters, and ledger snapshot.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/get_reporting_status_response.py b/src/adcp/types/domains/media_buy/get_reporting_status_response.py index 299fce0b3..6a7922c9a 100644 --- a/src/adcp/types/domains/media_buy/get_reporting_status_response.py +++ b/src/adcp/types/domains/media_buy/get_reporting_status_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/get_reporting_status_response.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, model_validator from ..core import context as context_1 @@ -53,6 +53,7 @@ class ObligationCounts(AdCPBaseModel): description="Obligations in this scope whose elapsed expected period has passed its consumer-status deadline — expected_at plus automated_recovery_window_seconds — without a current consumer status from the authenticated caller. A chain with any unsuperseded leaf counts as current whatever that leaf says; only an empty chain is pending. Because it counts obligations, a period the seller omitted entirely has no obligation and is not counted here — the buyer's independently derived denominator, not this field, remains the authority on omitted periods. It is a visibility count over the caller's own silence, never a health input: it MUST NOT change health, any other count, or seller-advertised reliability_statistics, and it overlaps the health counts rather than partitioning them. Required when the seller advertises consumer_status_task.", ge=0, ), + NoExplicitNull, ] = None @@ -80,7 +81,9 @@ class Scope(AdCPBaseModel): StrictBool, Field(description='True only when no new obligation can enter this evaluated scope.'), ] - media_buy_ids: list[reporting_coverage.ReportingMediaBuyId] | None = None + media_buy_ids: Annotated[ + list[reporting_coverage.ReportingMediaBuyId] | None, NoExplicitNull + ] = None all_accessible_media_buys: Annotated[ StrictBool, Field( @@ -113,7 +116,7 @@ class GetReportingStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve model_config = ConfigDict( extra='allow', ) - view: get_reporting_status_request.ReportingStatusView | None = None + view: Annotated[get_reporting_status_request.ReportingStatusView | None, NoExplicitNull] = None ledger_snapshot_id: Annotated[ str | None, Field( @@ -121,12 +124,14 @@ class GetReportingStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve max_length=255, min_length=1, ), + NoExplicitNull, ] = None ledger_as_of: Annotated[ AwareDatetime | None, Field( description='Exclusive observation boundary for ledger_snapshot_id. Revisions committed later appear only in a later reconciliation.' ), + NoExplicitNull, ] = None changes_checkpoint: Annotated[ str | None, @@ -135,23 +140,27 @@ class GetReportingStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve max_length=2048, min_length=1, ), + NoExplicitNull, ] = None account_id: Annotated[ str | None, Field(description='Resolved seller/storefront account identifier.', min_length=1), + NoExplicitNull, ] = None scope: Annotated[ Scope | None, Field( description='Exact denominator evaluated for summary or periods health. complete is valid only when scope_closed is true.' ), + NoExplicitNull, ] = None - health: reporting_health.ReportingHealth | None = None + health: Annotated[reporting_health.ReportingHealth | None, NoExplicitNull] = None coverage: Annotated[ reporting_coverage.ReportingCoverage | None, Field( description='Aggregated effective coverage for the exact selected scope. This remains independent of reporting health and finality so a fresh covered subset cannot look like complete campaign reporting.' ), + NoExplicitNull, ] = None data_through: Annotated[ AwareDatetime | None, @@ -164,46 +173,56 @@ class GetReportingStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve Field( description='Next obligation due time for an open scope. In a complete summary (view: summary, health: complete), the nearest future period start, strictly after ledger_as_of, across all active committed configuration generations in scope.delivery_config_generations. Sellers MUST populate it when such a scheduled period exists outside the closed evaluated scope, and omit it when none exists. This complete-scope projection applies only to summary responses. It is derived from the configuration schedule and does not represent an open obligation in the evaluated scope; its presence does not indicate that the scope is still open. Projecting it MUST NOT create, expose, lease, count, or alter an obligation whose period has not closed, or change obligation_counts, scope, or coverage. Obligation expected_at remains period.end plus schedule.delivery_sla; get_media_buy_delivery.next_expected_at remains the next webhook notification time.' ), + NoExplicitNull, ] = None - obligation_counts: ObligationCounts | None = None - issues: list[reporting_status_issue.ReportingStatusIssue] | None = None - periods: list[reporting_obligation.ReportingObligation] | None = None + obligation_counts: Annotated[ObligationCounts | None, NoExplicitNull] = None + issues: Annotated[list[reporting_status_issue.ReportingStatusIssue] | None, NoExplicitNull] = ( + None + ) + periods: Annotated[list[reporting_obligation.ReportingObligation] | None, NoExplicitNull] = None revisions: Annotated[ list[reporting_revision.ReportingRevision] | None, Field( description='Revision ledger records on this page. Pagination is over the flat union of obligations, revisions, adjustments, materializations, consumer status statements, revision receipts, and adjustment receipts, avoiding unbounded nested history.' ), + NoExplicitNull, ] = None adjustments: Annotated[ list[reporting_adjustment.ReportingAdjustment] | None, Field( description='Immutable post-official accounting corrections on this page. They preserve the original invoice-to-revision binding and are included in flat ledger pagination.' ), + NoExplicitNull, ] = None consumer_statuses: Annotated[ list[reporting_consumer_status.ReportingConsumerStatus] | None, Field( description="Authenticated caller's append-only reporting status history on this page. Current leaves are identified by obligation current_consumer_status_id or, for a missing seller obligation, by the supersession chain over configuration generation, report definition, and period. No other consumer's status is disclosed." ), + NoExplicitNull, ] = None adjustment_receipts: Annotated[ list[reporting_adjustment_receipt.ReportingAdjustmentReceipt] | None, Field( description='Authenticated Reconciled Billing outcomes for adjustments on this page.' ), + NoExplicitNull, + ] = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + revision: Annotated[reporting_revision.ReportingRevision | None, NoExplicitNull] = None + materializations: Annotated[ + list[reporting_materialization.ReportingMaterialization] | None, NoExplicitNull ] = None - pagination: pagination_response.PaginationResponse | None = None - revision: reporting_revision.ReportingRevision | None = None - materializations: list[reporting_materialization.ReportingMaterialization] | None = None receipts: Annotated[ list[reporting_receipt.ReportingReceipt] | None, Field( description="Authenticated caller's durable reconciliation receipts. Receipts from another consumer principal are never disclosed." ), + NoExplicitNull, ] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None status: Status @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/list_creative_formats_request.py b/src/adcp/types/domains/media_buy/list_creative_formats_request.py index 5b22fa3ab..1a87b3c27 100644 --- a/src/adcp/types/domains/media_buy/list_creative_formats_request.py +++ b/src/adcp/types/domains/media_buy/list_creative_formats_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/list_creative_formats_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -33,6 +34,7 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Return only these specific named-format IDs (for example, from a 3.x get_products response). Use canonical-format discovery in 4.0.', min_length=1, ), + NoExplicitNull, ] = None asset_types: Annotated[ list[asset_content_type.AssetContentType] | None, @@ -40,39 +42,47 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats that include these asset types. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns formats with images and text, ['javascript'] returns formats accepting JavaScript tags.", min_length=1, ), + NoExplicitNull, ] = None max_width: Annotated[ SchemaInt | None, Field( description='Maximum width in pixels (inclusive). Returns formats where ANY render has width <= this value. For multi-render formats, matches if at least one render fits.' ), + NoExplicitNull, ] = None max_height: Annotated[ SchemaInt | None, Field( description='Maximum height in pixels (inclusive). Returns formats where ANY render has height <= this value. For multi-render formats, matches if at least one render fits.' ), + NoExplicitNull, ] = None min_width: Annotated[ SchemaInt | None, Field( description='Minimum width in pixels (inclusive). Returns formats where ANY render has width >= this value.' ), + NoExplicitNull, ] = None min_height: Annotated[ SchemaInt | None, Field( description='Minimum height in pixels (inclusive). Returns formats where ANY render has height >= this value.' ), + NoExplicitNull, ] = None is_responsive: Annotated[ StrictBool | None, Field( description='Filter for responsive formats that adapt to container size. When true, returns formats without fixed dimensions.' ), + NoExplicitNull, ] = None name_search: Annotated[ - str | None, Field(description='Search for formats by name (case-insensitive partial match)') + str | None, + Field(description='Search for formats by name (case-insensitive partial match)'), + NoExplicitNull, ] = None publisher_domain: Annotated[ str | None, @@ -81,18 +91,21 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description="Deprecated compatibility filter for older 3.x callers. A compatibility implementation MAY project publisher-origin or community-catalog declarations obtained through the registry publisher lookup, but MUST NOT synthesize a publisher catalog from seller products. New callers use `GET /api/registry/publisher?domain=...` for publisher acceptance and `get_products` for this seller's deliverability. The pattern below is a syntactic floor, not an SSRF guard.", pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None property_id: Annotated[ property_id_1.PropertyId | None, Field( description="Filter to formats supported on the named property within the publisher's catalog. Resolves to a property in the publisher's `adagents.json` `properties[]`; the agent returns only `formats[]` entries whose `applies_to_property_ids` includes this property (or entries with no scope, which apply to all properties). Typically used in combination with `publisher_domain`." ), + NoExplicitNull, ] = None wcag_level: Annotated[ wcag_level_1.WcagLevel | None, Field( description='Filter to formats that meet at least this WCAG conformance level (A < AA < AAA)' ), + NoExplicitNull, ] = None disclosure_positions: Annotated[ list[disclosure_position.DisclosurePosition] | None, @@ -100,6 +113,7 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats that support all of these disclosure positions. When a format has disclosure_capabilities, match against those positions. Otherwise fall back to supported_disclosure_positions. Use to find formats compatible with a brief's compliance requirements.", min_length=1, ), + NoExplicitNull, ] = None disclosure_persistence: Annotated[ list[disclosure_persistence_1.DisclosurePersistence] | None, @@ -107,6 +121,7 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description='Filter to formats where each requested persistence mode is supported by at least one position in disclosure_capabilities. Different positions may satisfy different modes. Use to find formats compatible with jurisdiction-specific persistence requirements (e.g., continuous for EU AI Act).', min_length=1, ), + NoExplicitNull, ] = None output_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -114,6 +129,7 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats whose output_format_ids includes any of these format IDs. Returns formats that can produce these outputs — inspect each result's input_format_ids to see what inputs they accept.", min_length=1, ), + NoExplicitNull, ] = None input_format_ids: Annotated[ list[format_id.FormatReferenceStructuredObject] | None, @@ -121,6 +137,7 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): description="Filter to formats whose input_format_ids includes any of these format IDs. Returns formats that accept these creatives as input — inspect each result's output_format_ids to see what they can produce.", min_length=1, ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference | None, @@ -128,7 +145,8 @@ class ListCreativeFormatsRequest(AdcpRequest, AdcpVersionEnvelope): deprecated=True, description="**DEPRECATED with `list_creative_formats` in 3.2. Removed at 4.0.** Use `get_products` with `account`; each `Product.format_options[]` lists the formats this seller can deliver for that account. *Legacy 3.x behavior:* scopes the returned formats to this account. Sellers that keep account-specific format catalogs (including sandbox accounts) return that account's formats; sellers with a single catalog MAY ignore this field.", ), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/list_creative_formats_response.py b/src/adcp/types/domains/media_buy/list_creative_formats_response.py index e38f6bfd3..945755279 100644 --- a/src/adcp/types/domains/media_buy/list_creative_formats_response.py +++ b/src/adcp/types/domains/media_buy/list_creative_formats_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/list_creative_formats_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -35,11 +35,12 @@ class CreativeAgent(AdCPBaseModel): ), ] agent_name: Annotated[ - str | None, Field(description='Human-readable name for the creative agent') + str | None, Field(description='Human-readable name for the creative agent'), NoExplicitNull ] = None capabilities: Annotated[ list[creative_agent_capability.CreativeAgentCapability] | None, Field(description='Capabilities this creative agent provides'), + NoExplicitNull, ] = None @@ -60,6 +61,7 @@ class ListCreativeFormatsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv deprecated=True, description='Deprecated compatibility provenance. `publisher` means publisher-origin catalog; `aao_mirror` means community catalog; `agent_derived` is retained only to parse historical 3.x responses and MUST NOT be produced by a new 3.2 implementation because seller products are not publisher authority.', ), + NoExplicitNull, ] = None creative_agents: Annotated[ list[CreativeAgent] | None, @@ -67,15 +69,18 @@ class ListCreativeFormatsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv deprecated=True, description="Deprecated recursive discovery projection retained for historical 3.x responses. New buyers query the registry's canonical creative capability index and confirm candidates with get_adcp_capabilities; they do not recursively walk agent-provided lists.", ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field(description='Task-specific errors and warnings (e.g., format availability issues)'), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/list_products_request.py b/src/adcp/types/domains/media_buy/list_products_request.py index 74a08a216..683830153 100644 --- a/src/adcp/types/domains/media_buy/list_products_request.py +++ b/src/adcp/types/domains/media_buy/list_products_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/list_products_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import brand_key, canonical_account_ref @@ -28,6 +29,7 @@ class ListProductsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -37,6 +39,7 @@ class ListProductsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str | None, @@ -46,6 +49,7 @@ class ListProductsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$', ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, @@ -53,35 +57,44 @@ class ListProductsRequest(AdcpRequest, AdcpVersionEnvelope): description='MCP compatibility field: servers ignore this value; A2A uses transport-native Message/Task contextId.', min_length=1, ), + NoExplicitNull, + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Uniform per-call envelope field accepted for SDK compatibility. This does not register a wholesale feed subscription; durable product.* and wholesale_feed.bulk_change subscribers are registered through sync_accounts notification_configs.' ), + NoExplicitNull, ] = None account: Annotated[ canonical_account_ref.CanonicalAccountReference | None, Field( description='Account scope for pricing and availability. A natural-key account is the single brand source and MUST NOT be combined with top-level brand.' ), + NoExplicitNull, + ] = None + brand: Annotated[brand_key.BrandKey | None, NoExplicitNull] = None + criteria: Annotated[ + product_discovery_criteria.ProductDiscoveryCriteria | None, NoExplicitNull ] = None - brand: brand_key.BrandKey | None = None - criteria: product_discovery_criteria.ProductDiscoveryCriteria | None = None - fields: product_fields.ProductResponseFields | None = None - cursor: Annotated[str | None, Field(min_length=1)] = None - max_results: Annotated[SchemaInt | None, Field(ge=1, le=100)] = 25 + fields: Annotated[product_fields.ProductResponseFields | None, NoExplicitNull] = None + cursor: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + max_results: Annotated[SchemaInt | None, Field(ge=1, le=100), NoExplicitNull] = 25 if_feed_version: Annotated[ str | None, Field( description='Opaque feed version returned by a prior list_products response or wholesale product-feed webhook for the same cache scope and canonicalized selection. Used for repair and conditional reconciliation, not routine polling when webhooks are active.' ), + NoExplicitNull, ] = None if_pricing_version: Annotated[ str | None, Field( description='Opaque pricing version returned by a prior list_products response. Valid only with if_feed_version.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/list_products_response.py b/src/adcp/types/domains/media_buy/list_products_response.py index 329429744..0fe3e2579 100644 --- a/src/adcp/types/domains/media_buy/list_products_response.py +++ b/src/adcp/types/domains/media_buy/list_products_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/list_products_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import canonical_product @@ -41,7 +41,7 @@ class IncompleteItem(AdCPBaseModel): ) scope: Scope description: Annotated[str, Field(min_length=1)] - estimated_wait: duration.Duration | None = None + estimated_wait: Annotated[duration.Duration | None, NoExplicitNull] = None class IncompleteItem3(IncompleteItem): @@ -65,6 +65,7 @@ class ListProductsResponse1(AdcpResponse, AdCPBaseModel): description='Cursor for the next page. Omitted when this is the last page.', min_length=1, ), + NoExplicitNull, ] = None feed_version: Annotated[ str, @@ -73,7 +74,9 @@ class ListProductsResponse1(AdcpResponse, AdCPBaseModel): ), ] pricing_version: Annotated[ - str | None, Field(description='Opaque version of the selected pricing layer.') + str | None, + Field(description='Opaque version of the selected pricing layer.'), + NoExplicitNull, ] = None cache_scope: CacheScope incomplete: Annotated[ @@ -82,10 +85,11 @@ class ListProductsResponse1(AdcpResponse, AdCPBaseModel): description='Usable partial response with explicitly missing product-list scopes.', min_length=1, ), + NoExplicitNull, ] = None - replayed: Literal[True] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class ListProductsResponse2(AdcpResponse, AdCPBaseModel): @@ -98,13 +102,14 @@ class ListProductsResponse2(AdcpResponse, AdCPBaseModel): description='Whether this response carries a product page or confirms that the selected feed has not changed.' ), ] = 'unchanged' - products: list[canonical_product.CanonicalProduct] | None = None + products: Annotated[list[canonical_product.CanonicalProduct] | None, NoExplicitNull] = None next_cursor: Annotated[ str | None, Field( description='Cursor for the next page. Omitted when this is the last page.', min_length=1, ), + NoExplicitNull, ] = None feed_version: Annotated[ str, @@ -113,7 +118,9 @@ class ListProductsResponse2(AdcpResponse, AdCPBaseModel): ), ] pricing_version: Annotated[ - str | None, Field(description='Opaque version of the selected pricing layer.') + str | None, + Field(description='Opaque version of the selected pricing layer.'), + NoExplicitNull, ] = None cache_scope: CacheScope incomplete: Annotated[ @@ -122,10 +129,11 @@ class ListProductsResponse2(AdcpResponse, AdCPBaseModel): description='Usable partial response with explicitly missing product-list scopes.', min_length=1, ), + NoExplicitNull, ] = None - replayed: Literal[True] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None ListProductsResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/log_event_request.py b/src/adcp/types/domains/media_buy/log_event_request.py index 0daa8bc3a..cf10fbdcd 100644 --- a/src/adcp/types/domains/media_buy/log_event_request.py +++ b/src/adcp/types/domains/media_buy/log_event_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/log_event_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -28,6 +29,7 @@ class LogEventRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Test event code for validation without affecting production data. Events with this code appear in the platform's test events UI." ), + NoExplicitNull, ] = None events: Annotated[ list[event.Event], Field(description='Events to log', max_length=10000, min_length=1) @@ -41,5 +43,5 @@ class LogEventRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/media_buy_commitment_response.py b/src/adcp/types/domains/media_buy/media_buy_commitment_response.py index fe7103178..869389c64 100644 --- a/src/adcp/types/domains/media_buy/media_buy_commitment_response.py +++ b/src/adcp/types/domains/media_buy/media_buy_commitment_response.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/media_buy_commitment_response.json -# timestamp: 2026-10-03T14:21:15+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from ..core import canonical_media_buy_action @@ -48,9 +48,9 @@ class MediaBuyCommitmentResponse2(AdCPBaseModel): ) status: Literal['failed'] = 'failed' errors: Annotated[list[error.Error], Field(min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class MediaBuyCommitmentResponse3(AdCPBaseModel): @@ -59,11 +59,11 @@ class MediaBuyCommitmentResponse3(AdCPBaseModel): ) status: Literal['submitted'] = 'submitted' task_id: Annotated[str, Field(min_length=1)] - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class Warning(Warning_1): @@ -73,6 +73,7 @@ class Warning(Warning_1): description='Closed warning vocabulary for the negotiated AdCP release. A non-blocking condition observed while an operation still succeeded. New values are added through normal protocol releases; buyers validate against the negotiated release.', title='Warning Code', ), + NoExplicitNull, ] = None @@ -104,9 +105,10 @@ class MediaBuyCommitmentResponse1(AdCPBaseModel): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None revision: Annotated[SchemaInt, Field(ge=1)] - media_buy_status: media_buy_status_1.MediaBuyStatus | None = None + media_buy_status: Annotated[media_buy_status_1.MediaBuyStatus | None, NoExplicitNull] = None confirmed_at: AwareDatetime | None = None accepted_proposal: AcceptedProposal purchase_bindings: Annotated[ @@ -123,10 +125,11 @@ class MediaBuyCommitmentResponse1(AdCPBaseModel): description='Non-blocking observations about this completed commitment. The MediaBuy was still created or amended exactly as represented. Continuing conditions also appear as indicators on get_media_buys.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None MediaBuyCommitmentResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/media_buy_delivery_webhook_result.py b/src/adcp/types/domains/media_buy/media_buy_delivery_webhook_result.py index 5ac17133c..aad82084f 100644 --- a/src/adcp/types/domains/media_buy/media_buy_delivery_webhook_result.py +++ b/src/adcp/types/domains/media_buy/media_buy_delivery_webhook_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/media_buy_delivery_webhook_result.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import attribution_window as attribution_window_1 @@ -41,6 +41,7 @@ class ReportingPeriod(AdCPBaseModel): examples=['UTC', 'America/New_York'], min_length=1, ), + NoExplicitNull, ] = None @@ -58,43 +59,47 @@ class Status(StrEnum): class Totals(DeliveryMetrics): - effective_rate: Annotated[StrictFloat | None, Field(ge=0.0)] = None + effective_rate: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None class ByPackageItem(DeliveryMetrics): - package_id: str | None = None + package_id: Annotated[str | None, NoExplicitNull] = None pacing_index: Annotated[ StrictFloat | None, Field( description='Subordinate package pacing diagnostic. May be absent for seller-optimized packages without package-level pacing.', ge=0.0, ), + NoExplicitNull, ] = None - pricing_model: pricing_model_1.PricingModel | None = None - rate: Annotated[StrictFloat | None, Field(ge=0.0)] = None + pricing_model: Annotated[pricing_model_1.PricingModel | None, NoExplicitNull] = None + rate: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None currency: Annotated[ str | None, Field( description="ISO 4217 currency code for this package's spend and currency-denominated pricing rate. The rate for revenue_share is a dimensionless commission fraction, but attributed monetary values still use this currency. When the enclosing media_buy_deliveries[].currency is present, this value MUST equal it. For AdCP-authored buys both values MUST equal the media-buy currency. A different package currency is permitted only for a legacy or externally created mixed-currency buy whose row currency and row-level monetary totals are omitted. When both row and deprecated response-wide currency are absent, a package with monetary values MUST include this field. AdCP does not perform currency conversion.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None delivery_status: Annotated[ delivery_status_1.DeliveryStatus | None, Field( description="System-reported operational state of this package. Reflects actual delivery state independent of buyer pause control. 'not_delivering' means zero impressions were recorded for the entire reporting_period while the package was in-flight. Sellers SHOULD only report 'not_delivering' once the package's data is_final for the period — a provisional (is_final: false) zero may still be measurement catching up, not genuine non-delivery." ), + NoExplicitNull, ] = None - paused: StrictBool | None = None - is_final: StrictBool | None = None - finalized_at: AwareDatetime | None = None - measurement_window: Annotated[str | None, Field(max_length=50)] = None - supersedes_window: Annotated[str | None, Field(max_length=50)] = None + paused: Annotated[StrictBool | None, NoExplicitNull] = None + is_final: Annotated[StrictBool | None, NoExplicitNull] = None + finalized_at: Annotated[AwareDatetime | None, NoExplicitNull] = None + measurement_window: Annotated[str | None, Field(max_length=50), NoExplicitNull] = None + supersedes_window: Annotated[str | None, Field(max_length=50), NoExplicitNull] = None metric_values: Annotated[ list[package_delivery_metric_value.PackageDeliveryMetricValue] | None, Field( description='Qualified standard delivery values for this package. Each entry is the delivered counterpart to a standard-scope package.committed_metrics or by_package[].missing_metrics row, using the same atomic key (scope, metric_id, qualifier). Sellers report one row per full qualifier set at package grain; when a metric appears here, its flat scalar counterpart on the package MUST be omitted to avoid two sources of truth. Vendor-scope values continue to use vendor_metric_values. Buyers reconcile rows directly and perform any compatible cross-package aggregation themselves.' ), + NoExplicitNull, ] = None spend: Any @@ -110,6 +115,7 @@ class MediaBuyDelivery(AdCPBaseModel): description="ISO 4217 denomination for monetary values in this media-buy delivery row, including totals, package spend, and currency-denominated package rates. Sellers SHOULD populate this field whenever all monetary values in the row share one currency. For AdCP-authored buys it MUST equal the media-buy currency, and every by_package[].currency MUST equal it. For a legacy or externally created mixed-currency buy, omit this field and all monetary or money-derived values from row totals; report those values only at package level with each package's own currency. AdCP does not perform currency conversion.", pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None status: Annotated[ Status, @@ -122,30 +128,35 @@ class MediaBuyDelivery(AdCPBaseModel): Field( description='When delayed data is expected to be available. Present when status is reporting_delayed.' ), + NoExplicitNull, ] = None is_adjusted: Annotated[ StrictBool | None, Field( description='Indicates this row contains updated data for a previously reported period.' ), + NoExplicitNull, ] = None is_final: Annotated[ StrictBool | None, Field(description="Whether this row's delivery data is final for the reporting period."), + NoExplicitNull, ] = None finalized_at: Annotated[ AwareDatetime | None, Field( description='Timestamp when this row became final. Present only when is_final is true.' ), + NoExplicitNull, ] = None - pricing_model: pricing_model_1.PricingModel | None = None + pricing_model: Annotated[pricing_model_1.PricingModel | None, NoExplicitNull] = None pacing_index: Annotated[ StrictFloat | None, Field( description='Aggregate media-buy delivery pace relative to the media-buy pacing plan. Authoritative for seller-optimized buys.', ge=0.0, ), + NoExplicitNull, ] = None totals: Totals by_package: Annotated[list[ByPackageItem], Field(description='Metrics broken down by package.')] @@ -166,6 +177,7 @@ class MediaBuyDeliveryWebhookResult(AdCPBaseModel): Field( description='Indicates if any media buys in this webhook have missing or delayed data.' ), + NoExplicitNull, ] = None unavailable_count: Annotated[ SchemaInt | None, @@ -173,18 +185,21 @@ class MediaBuyDeliveryWebhookResult(AdCPBaseModel): description='Number of media buys with reporting_delayed or failed status when partial_data is true.', ge=0, ), + NoExplicitNull, ] = None sequence_number: Annotated[ SchemaInt | None, Field( description='Sequential notification number for this reporting webhook stream.', ge=1 ), + NoExplicitNull, ] = None next_expected_at: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp for the next expected notification. Omitted on final notifications.' ), + NoExplicitNull, ] = None reporting_period: Annotated[ ReportingPeriod, @@ -199,12 +214,14 @@ class MediaBuyDeliveryWebhookResult(AdCPBaseModel): description='Deprecated in AdCP 3.2 and removed in AdCP 4.0. Optional legacy report-wide ISO 4217 currency code. It may be used only when every monetary value in the report has that denomination. A report can contain media buys with different currencies, so buyers MUST NOT interpret this field as an aggregation currency or evidence of currency conversion. Prefer media_buy_deliveries[].currency when present and package-level currency otherwise.', pattern='^[A-Z]{3}$', ), + NoExplicitNull, ] = None attribution_window: Annotated[ attribution_window_1.AttributionWindow | None, Field( description='Attribution methodology and lookback windows used for conversion metrics in this report.' ), + NoExplicitNull, ] = None media_buy_deliveries: Annotated[ list[MediaBuyDelivery], @@ -213,8 +230,10 @@ class MediaBuyDeliveryWebhookResult(AdCPBaseModel): ), ] errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific delivery errors or warnings.') + list[error.Error] | None, + Field(description='Task-specific delivery errors or warnings.'), + NoExplicitNull, ] = None - sandbox: StrictBool | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + sandbox: Annotated[StrictBool | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/outcome_target.py b/src/adcp/types/domains/media_buy/outcome_target.py index e91193301..d1e657b3b 100644 --- a/src/adcp/types/domains/media_buy/outcome_target.py +++ b/src/adcp/types/domains/media_buy/outcome_target.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/outcome_target.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat, model_validator from ..core import outcome_target_cost_per @@ -47,6 +47,7 @@ class Goal1(AdCPBaseModel): description="Required when event_type is 'custom'. Platform-specific name for the custom event.", min_length=1, ), + NoExplicitNull, ] = None @@ -67,8 +68,9 @@ class OutcomeTarget(AdCPBaseModel): description="Desired total volume of the goal's metric or event across the planned flight. Alone, the seller solves for budget and answers with total_budget_guidance and a forecast. With cost_per, the seller plans toward the volume at the cost: under a cap it SHOULD keep the buyer's amount and forecast the lower volume it can deliver, unless no volume can be planned at that amount; under a target the ask is plannable when the seller can forecast the volume around it.", gt=0.0, ), + NoExplicitNull, ] = None - cost_per: outcome_target_cost_per.OutcomeTargetCostPer | None = None + cost_per: Annotated[outcome_target_cost_per.OutcomeTargetCostPer | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> OutcomeTarget: diff --git a/src/adcp/types/domains/media_buy/package_control.py b/src/adcp/types/domains/media_buy/package_control.py index d70a2f583..00d48411b 100644 --- a/src/adcp/types/domains/media_buy/package_control.py +++ b/src/adcp/types/domains/media_buy/package_control.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/package_control.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool, StrictFloat, model_validator @@ -48,27 +48,32 @@ class PackageControl(AdCPBaseModel): ge=0.0, ), ] = None - impressions: Annotated[StrictFloat | None, Field(ge=0.0)] = None + impressions: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Replace package pacing, subordinate to media-buy pacing. In a resulting seller-optimized buy, pacing that differs from the media-buy pacing requires advertised media_buy.features.seller_optimized_package_pacing; otherwise rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None bidding: bidding_policy.BiddingPolicy | None = None - paused: StrictBool | None = None + paused: Annotated[StrictBool | None, NoExplicitNull] = None canceled: Annotated[ Literal[True] | None, Field( description='Exercise an already-accepted unilateral package cancellation right. If seller agreement is required, refine the accepted proposal with change_kind amendment; cancellation proposals terminate the whole MediaBuy.' ), + NoExplicitNull, + ] = None + cancellation_reason: Annotated[ + str | None, Field(max_length=500, min_length=1), NoExplicitNull ] = None - cancellation_reason: Annotated[str | None, Field(max_length=500, min_length=1)] = None targeting_overlay: Annotated[ targeting_input.TargetingOverlayInput | None, Field( description='Per-dimension targeting patch. Omission preserves the current dimension, a non-null value replaces it, and null clears it. The resulting effective targeting must remain within the accepted commercial envelope and is read back without null dimensions.' ), + NoExplicitNull, ] = None catalog_ids: Annotated[ list[CatalogId] | None, @@ -76,21 +81,24 @@ class PackageControl(AdCPBaseModel): description="Replace the package's promoted catalogs with references previously managed through sync_catalogs.", min_length=1, ), + NoExplicitNull, ] = None keyword_targets_add: Annotated[ - list[keyword_target.KeywordTarget] | None, Field(min_length=1) + list[keyword_target.KeywordTarget] | None, Field(min_length=1), NoExplicitNull ] = None keyword_targets_remove: Annotated[ - list[keyword_target.KeywordTarget] | None, Field(min_length=1) + list[keyword_target.KeywordTarget] | None, Field(min_length=1), NoExplicitNull ] = None negative_keywords_add: Annotated[ - list[keyword_target.KeywordTarget] | None, Field(min_length=1) + list[keyword_target.KeywordTarget] | None, Field(min_length=1), NoExplicitNull ] = None negative_keywords_remove: Annotated[ - list[keyword_target.KeywordTarget] | None, Field(min_length=1) + list[keyword_target.KeywordTarget] | None, Field(min_length=1), NoExplicitNull ] = None optimization_goals: Annotated[ - list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, Field(min_length=1) + list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, + Field(min_length=1), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/package_request.py b/src/adcp/types/domains/media_buy/package_request.py index 2882032e5..366f00c3d 100644 --- a/src/adcp/types/domains/media_buy/package_request.py +++ b/src/adcp/types/domains/media_buy/package_request.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/package_request.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from collections.abc import Sequence from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat, model_validator from ..core import audience_evidence_pin @@ -32,11 +32,15 @@ class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - viewability_standard: viewability_standard_1.ViewabilityStandard | None = None - completion_source: completion_source_1.CompletionSource | None = None - attribution_methodology: attribution_methodology_1.AttributionMethodology | None = None - attribution_window: duration.Duration | None = None - lift_dimension: lift_dimension_1.LiftDimension | None = None + viewability_standard: Annotated[ + viewability_standard_1.ViewabilityStandard | None, NoExplicitNull + ] = None + completion_source: Annotated[completion_source_1.CompletionSource | None, NoExplicitNull] = None + attribution_methodology: Annotated[ + attribution_methodology_1.AttributionMethodology | None, NoExplicitNull + ] = None + attribution_window: Annotated[duration.Duration | None, NoExplicitNull] = None + lift_dimension: Annotated[lift_dimension_1.LiftDimension | None, NoExplicitNull] = None class CommittedMetrics1(AdCPBaseModel): @@ -58,6 +62,7 @@ class CommittedMetrics1(AdCPBaseModel): Field( description='Disambiguator — same shape as on the response-side `committed_metrics`. Required when the buyer wants to pin a specific measurement path: `viewability_standard` for MRC vs GroupM viewability; `completion_source` for seller- vs vendor-attested `completion_rate`; `attribution_methodology` for how attribution was computed (deterministic_purchase, probabilistic, panel_based, modeled); `attribution_window` for the time window over which outcomes are attributed. See response-side description for full semantics.' ), + NoExplicitNull, ] = None @@ -86,12 +91,14 @@ class CommittedMetrics2(AdCPBaseModel): Field( description="Optional buyer-proposed pin of the vendor's `measurement.metrics[].methodology_version`. The seller accepts by echoing it on the confirmed package's `committed_metrics` entry, normalizes to the version it can actually deliver, or rejects with `TERMS_REJECTED`. Opaque string — equality comparison only." ), + NoExplicitNull, ] = None qualifier: Annotated[ Qualifier | None, Field( description='Optional disambiguator for vendor metrics committed under more than one methodology or window — same closed key set as standard-scope entries.' ), + NoExplicitNull, ] = None @@ -118,6 +125,7 @@ class PackageRequest(AdcpVersionEnvelope): description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format selector retained for older 3.x peers. New buyers MUST NOT emit this field. Sellers MUST normalize every entry through the canonical mapping path before product satisfaction checks; an entry that cannot be normalized is rejected with `UNSUPPORTED_FEATURE` before any equivalence check. When this field coexists with `format_option_refs` or `format_kind` plus `params`, sellers MUST compare the product option sets selected by each resolved route. Legacy parameter compatibility follows the asymmetric v2-narrows-v1 relation defined by canonical formats, not raw object equality. Different format shapes, selected option sets, or incompatible dimensions are rejected with `CONFLICTING_SELECTORS`; sellers MUST NOT silently ignore the legacy projection. Equivalent dual emission remains valid during the 3.x compatibility window. If omitted and no canonical selector is present, all formats supported by the product are active.', min_length=1, ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -125,18 +133,21 @@ class PackageRequest(AdcpVersionEnvelope): description='Canonical 3.2 format-option selector. Each reference matches one target product `format_options[]` entry. Publisher-backed options match `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match `{ scope: "product", format_option_id }`. Sellers reject unresolved options with `UNSUPPORTED_FEATURE` and a field path to the failing entry before comparing co-present routes. New buyers MUST use this route by itself and MUST NOT dual-emit either a direct canonical selector or deprecated `format_ids`. Receivers handling older 3.x multi-route requests MUST resolve every present route, require each route to select the same product option set, and reject disagreement with `CONFLICTING_SELECTORS` before treating `format_option_refs` as authoritative.', min_length=1, ), + NoExplicitNull, ] = None format_kind: Annotated[ str | None, Field( description='Canonical 3.2 direct selector. Names the canonical format shape this package targets when the buyer is not selecting a published `format_option_ref`. Pair with `params` for dimensions, duration, codecs, or other constraints. New buyers MUST NOT combine this route with `format_option_refs` or deprecated `format_ids`. Receivers handling older 3.x multi-route requests MUST equivalence-check every present route before applying precedence and reject disagreement with `CONFLICTING_SELECTORS`. Product satisfaction is directional: broad `{ format_kind: "image" }` does not satisfy a fixed-size product declaration.' ), + NoExplicitNull, ] = None params: Annotated[ dict[str, Any] | None, Field( description="Parameters for the direct canonical selector in `format_kind`. Shape follows the selected canonical's parameter vocabulary: dimensions (`width`, `height`, `sizes`), duration (`duration_ms_exact`, `duration_ms_range`), codecs, asset-source and slot narrowing, or other canonical-specific constraints. Requires `format_kind`. For fixed-size image selectors, `width` and `height` MUST co-occur; a selector containing only one dimension is schema-invalid. New buyers omit `params` when selecting by `format_option_refs` or `format_ids`; older multi-route requests are accepted only when every route selects the same product option set." ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -144,6 +155,7 @@ class PackageRequest(AdcpVersionEnvelope): description="Hard lifetime spend cap for this package in the media buy's currency. Required in fixed allocation mode. Optional in seller-optimized mode; when omitted, the package is bounded by the shared total_budget and any other package constraints. In seller-optimized mode this is a ceiling, not a reserved or current allocation, and requires advertised media_buy.features.seller_optimized_package_budgets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation.", ge=0.0, ), + NoExplicitNull, ] = None min_spend_target: Annotated[ StrictFloat | None, @@ -151,12 +163,14 @@ class PackageRequest(AdcpVersionEnvelope): description="Soft lifetime spend target for this package in the media buy's currency. Only valid with seller-optimized budget allocation. Requires advertised media_buy.features.seller_optimized_min_spend_targets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation. The seller SHOULD attempt to deliver at least this amount before allocating incremental spend elsewhere, but inventory, policy, optimization targets, or other delivery constraints may prevent it. This is not a billing guarantee. Must not exceed the package budget when both are present; a seller advertising both seller_optimized_min_spend_targets and seller_optimized_package_budgets MUST reject a violation with `INVALID_REQUEST`, while a seller missing either capability rejects the undeclared control with UNSUPPORTED_FEATURE before any over-subscription validation.", ge=0.0, ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Package pacing, subordinate to media-buy pacing. In seller-optimized mode, pacing that differs from the media-buy pacing requires advertised media_buy.features.seller_optimized_package_pacing; otherwise rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None pricing_option_id: Annotated[ str, @@ -171,15 +185,19 @@ class PackageRequest(AdcpVersionEnvelope): description='DEPRECATED in 3.2 and removed in the next major. Use bidding.bid_amount or bidding.max_bid. Legacy normalization: selected pricing_option.max_bid=true maps to bidding.max_bid; otherwise maps to bidding.bid_amount. A package MUST NOT supply both representations.', ge=0.0, ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, Field( description='Package-authored bidding policy. This complete block replaces, rather than field-merges with, any media-buy bidding policy for this package. `{automatic:true}` explicitly overrides a media-buy policy with provider automatic bidding; omission inherits the complete media-buy block. Monetary fields use the media-buy currency, while the selected pricing option supplies only the auction unit and MUST declare that same currency. Sellers MUST reject a new bidding block combined with legacy bid_price or legacy monetary optimization-goal targets on the same effective package with AMBIGUOUS_BIDDING_POLICY.' ), + NoExplicitNull, ] = None impressions: Annotated[ - StrictFloat | None, Field(description='Impression goal for this package', ge=0.0) + StrictFloat | None, + Field(description='Impression goal for this package', ge=0.0), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -187,30 +205,35 @@ class PackageRequest(AdcpVersionEnvelope): description="Optional hard package daily spend ceiling in the media-buy currency. It is subordinate, not a reserved allocation; package caps need not sum to the aggregate cap. Uses the media buy's cap timezone. Requires advertised package budget-capping scope; otherwise rejected with UNSUPPORTED_FEATURE.", ge=0.0, ), + NoExplicitNull, ] = None start_time: Annotated[ AwareDatetime | None, Field( description="Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Must fall within the media buy's date range." ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description="Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Must fall within the media buy's date range." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field( description='Whether this package should be created in a paused state. Paused packages do not deliver impressions. Defaults to false.' ), + NoExplicitNull, ] = False catalogs: Annotated[ list[catalog.Catalog] | None, Field( description='Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level — sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Makes the package catalog-driven: one budget envelope, platform optimizes across items.' ), + NoExplicitNull, ] = None optimization_goals: Annotated[ list[optimization_goal.OptimizationGoal] | None, @@ -218,18 +241,21 @@ class PackageRequest(AdcpVersionEnvelope): description='Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.', min_length=1, ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting_input.TargetingOverlayInput | None, Field( description="Optional package-specific targeting input with three states per dimension. Omission inherits targeting already bound to the configured product, a non-null value replaces that dimension, and null explicitly suppresses the configured/product default for that dimension. Null cannot remove inherent product scope: sellers reject a clear the product cannot execute rather than silently retaining the default. Non-null fields MUST be declared in the product's overlay_support unless they were already accepted during discovery. The one fixed-inventory restatement exception is placement_selection equal to the product's complete, explicitly enumerated mode: included placement set: that set is an inherent exact match across discovery, create, and update and does not require overlay_support.placement_selection; partial selection still requires a selectable product. Opaque property_list and collection_list references have no equivalent exception because their membership can change independently and cannot be proven equal from the product wire representation. Package readback echoes the complete effective targeting without null dimensions. A supported value with no current inventory returns PRODUCT_UNAVAILABLE rather than a silent substitute or reprice." ), + NoExplicitNull, ] = None audience_evidence_requirements: Annotated[ audience_evidence_requirements_1.AudienceEvidenceRequirements | None, Field( description='Buyer policy that the selected product and constructed package MUST satisfy using product audience evidence. This remains planning and suitability evidence, not a targeting instruction. Sellers MUST reject an unsatisfied required policy rather than silently drop it, and a confirmed package MUST include every evidence snapshot used to satisfy this policy in audience_evidence_selections with decision_use package_construction.' ), + NoExplicitNull, ] = None audience_evidence_pins: Annotated[ list[audience_evidence_pin.AudienceEvidencePin] | None, @@ -237,12 +263,14 @@ class PackageRequest(AdcpVersionEnvelope): description="Exact immutable evidence snapshots selected by the buyer during discovery. The seller MUST match evidence_id, snapshot_id, version, and content_digest against one published snapshot and MUST reject catalog mutation, snapshot reuse, missing snapshots, or substitutions. Every accepted pin MUST be echoed in the confirmed package's audience_evidence_selections with decision_use package_construction.", min_length=1, ), + NoExplicitNull, ] = None measurement_terms: Annotated[ measurement_terms_1.MeasurementTerms | None, Field( description="Buyer's proposed billing measurement and makegood terms. Overrides product defaults. Seller accepts (echoed on confirmed package), rejects with TERMS_REJECTED, or adjusts. When absent, product's measurement_terms apply." ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[performance_standard.PerformanceStandard] | None, @@ -250,6 +278,7 @@ class PackageRequest(AdcpVersionEnvelope): description="Buyer's proposed performance standards for this package. Overrides product defaults. Seller accepts, rejects with TERMS_REJECTED, or adjusts. When absent, product's performance_standards apply.", min_length=1, ), + NoExplicitNull, ] = None committed_metrics: Annotated[ list[CommittedMetrics] | None, @@ -257,6 +286,7 @@ class PackageRequest(AdcpVersionEnvelope): description="Buyer's proposed reporting contract for this package — the metrics the buyer wants the seller to commit to populating in delivery reports. Same negotiation pattern as `measurement_terms` and `performance_standards`: seller accepts (echoes on confirmed package with `committed_at` stamped), rejects with `TERMS_REJECTED` (with explanation of which entries were unworkable), or normalizes (echoes a different but compatible list — buyer can accept by retrying with the normalized terms). When absent, the seller decides what to commit based on the product's `available_metrics` and the buyer's `required_metrics` filter on `get_products`. Each entry uses an explicit `scope` discriminator (`standard` or `vendor`) and identifies the metric — request-side entries do NOT carry `committed_at`; that timestamp is stamped by the seller on accept. Constraints on what the buyer MAY propose: each `scope: standard` entry's `metric_id` MUST be in the product's `available_metrics`, and each `scope: vendor` entry's `(vendor, metric_id)` MUST appear in the product's `vendor_metrics` — sellers SHOULD reject with `TERMS_REJECTED` and reference the offending entry when the proposal exceeds product capability.", min_length=1, ), + NoExplicitNull, ] = None creative_assignments: Annotated[ list[creative_assignment.CreativeAssignment] | None, @@ -264,6 +294,7 @@ class PackageRequest(AdcpVersionEnvelope): description='Assign existing library creatives to this package with optional rotation, grouping, weights, and placement targeting. rotation_mode is package-scoped: omission resolves to weighted, and every assignment MUST resolve to the same effective mode. In sequential mode, sequence_position MUST be unique within each package-local group. Sellers reject conflicts with VALIDATION_ERROR before creating the package.', min_length=1, ), + NoExplicitNull, ] = None creatives: Annotated[ Sequence[creative_asset.CreativeAsset] | None, @@ -272,6 +303,7 @@ class PackageRequest(AdcpVersionEnvelope): max_length=100, min_length=1, ), + NoExplicitNull, ] = None agency_estimate_number: Annotated[ str | None, @@ -279,14 +311,16 @@ class PackageRequest(AdcpVersionEnvelope): description='Agency estimate or authorization number for this package. Overrides the media buy-level estimate number when different packages correspond to different agency estimates (e.g., different stations or flights within the same buy).', max_length=100, ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque package-level correlation data echoed unchanged in the package response, webhooks, and read surfaces. Buyers targeting mixed seller populations SHOULD include a per-package correlation value here, commonly context_1.buyer_ref, so responses from legacy sellers that do not echo product_id can still be mapped back to the requested product or line item. Do not use deprecated top-level buyer_ref for v3 correlation.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _validate_format_params(self) -> PackageRequest: diff --git a/src/adcp/types/domains/media_buy/package_update.py b/src/adcp/types/domains/media_buy/package_update.py index 4146ac47c..f3ec451d4 100644 --- a/src/adcp/types/domains/media_buy/package_update.py +++ b/src/adcp/types/domains/media_buy/package_update.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/package_update.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import bidding_policy, catalog @@ -30,6 +30,7 @@ class KeywordTargetsAddItem(AdCPBaseModel): description="Per-keyword bid price. Inherits currency and max_bid interpretation from the package's pricing option.", ge=0.0, ), + NoExplicitNull, ] = None @@ -81,6 +82,7 @@ class PackageUpdate(AdCPBaseModel): Field( description='Updated package pacing, subordinate to media-buy pacing. In a resulting seller-optimized buy, pacing that differs from the media-buy pacing requires advertised media_buy.features.seller_optimized_package_pacing; otherwise rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None bid_price: Annotated[ StrictFloat | None, @@ -89,6 +91,7 @@ class PackageUpdate(AdCPBaseModel): description='DEPRECATED in 3.2 and removed in the next major. Use bidding. A package update MUST NOT supply both a non-null bidding object and bid_price. During migration, bidding:null MAY accompany bid_price to clear the canonical block and set the legacy representation atomically.', ge=0.0, ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, @@ -97,7 +100,9 @@ class PackageUpdate(AdCPBaseModel): ), ] = None impressions: Annotated[ - StrictFloat | None, Field(description='Updated impression goal for this package', ge=0.0) + StrictFloat | None, + Field(description='Updated impression goal for this package', ge=0.0), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -111,25 +116,31 @@ class PackageUpdate(AdCPBaseModel): Field( description="Updated flight start date/time for this package in ISO 8601 format. Must fall within the media buy's date range." ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description="Updated flight end date/time for this package in ISO 8601 format. Must fall within the media buy's date range." ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field(description='Pause/resume specific package (true = paused, false = active)'), + NoExplicitNull, ] = None canceled: Annotated[ Literal[True] | None, Field( description='Cancel this specific package. Cancellation is irreversible — canceled packages stop delivery and cannot be reactivated. When true, package cancellation takes precedence over sibling fields on this package: the seller applies only canceled and cancellation_reason for this package and SHOULD return a structured warning naming ignored sibling fields. Root fields and other package updates still participate in the same atomic update when root canceled is absent. Sellers MAY reject with NOT_CANCELLABLE.' ), + NoExplicitNull, ] = None cancellation_reason: Annotated[ - str | None, Field(description='Reason for canceling this package.', max_length=500) + str | None, + Field(description='Reason for canceling this package.', max_length=500), + NoExplicitNull, ] = None catalogs: Annotated[ list[catalog.Catalog] | None, @@ -137,6 +148,7 @@ class PackageUpdate(AdCPBaseModel): description='Replace the catalogs this package promotes. Uses replacement semantics — the provided array replaces the current list. Omit to leave catalogs unchanged.', min_length=1, ), + NoExplicitNull, ] = None optimization_goals: Annotated[ list[optimization_goal.OptimizationGoal] | None, @@ -144,12 +156,14 @@ class PackageUpdate(AdCPBaseModel): description='Replace all optimization goals for this package. Uses replacement semantics — omit to leave goals unchanged.', min_length=1, ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting_input.TargetingOverlayInput | None, Field( description='Per-dimension targeting patch for this package. Omit targeting_overlay, or omit an individual dimension inside it, to leave the corresponding effective targeting unchanged. A non-null dimension replaces its current value; null clears it, including a value inherited from configured-product selection or a product default. Every resulting effective overlay must remain executable by the product. Sellers reject unsupported or partially applicable changes and use REQUOTE_REQUIRED when a change, including a broader inventory set, falls outside the priced envelope. placement_selection is purchased-inventory targeting; mode default restores the product default, null clears the dimension when the product permits it, and successful readback echoes the committed selected set when enumerable. If a patch removes a placement referenced by an existing creative assignment, the seller MUST reject the update unless the same atomic package mutation supplies a compatible complete creative_assignments replacement. Sellers MUST NOT silently delete assignments or retain orphan placement refs.' ), + NoExplicitNull, ] = None keyword_targets_add: Annotated[ list[KeywordTargetsAddItem] | None, @@ -157,6 +171,7 @@ class PackageUpdate(AdCPBaseModel): description='Keyword targets to add or update on this package. Upserts by (keyword, match_type) identity: if the pair already exists, its bid_price is updated; if not, a new keyword target is added. Use targeting_overlay.keyword_targets in create_media_buy to set the initial list.', min_length=1, ), + NoExplicitNull, ] = None keyword_targets_remove: Annotated[ list[KeywordTargetsRemoveItem] | None, @@ -164,6 +179,7 @@ class PackageUpdate(AdCPBaseModel): description='Keyword targets to remove from this package. Removes matching (keyword, match_type) pairs. If a specified pair is not present, sellers SHOULD treat it as a no-op for that entry.', min_length=1, ), + NoExplicitNull, ] = None negative_keywords_add: Annotated[ list[NegativeKeywordsAddItem] | None, @@ -171,6 +187,7 @@ class PackageUpdate(AdCPBaseModel): description='Negative keywords to add to this package. Appends to the existing negative keyword list — does not replace it. If a keyword+match_type pair already exists, sellers SHOULD treat it as a no-op for that entry. Use targeting_overlay.negative_keywords in create_media_buy to set the initial list.', min_length=1, ), + NoExplicitNull, ] = None negative_keywords_remove: Annotated[ list[NegativeKeywordsRemoveItem] | None, @@ -178,12 +195,14 @@ class PackageUpdate(AdCPBaseModel): description='Negative keywords to remove from this package. Removes matching keyword+match_type pairs from the existing list. If a specified pair is not present, sellers SHOULD treat it as a no-op for that entry.', min_length=1, ), + NoExplicitNull, ] = None creative_assignments: Annotated[ list[creative_assignment.CreativeAssignment] | None, Field( description='Replace creative assignments for this package with optional rotation, grouping, weights, and placement routing. Uses replacement semantics - omit to leave assignments unchanged. rotation_mode is package-scoped: omission resolves to weighted, and every assignment MUST resolve to the same effective mode. In sequential mode, sequence_position MUST be unique within each package-local group. Sellers reject conflicts with VALIDATION_ERROR before mutation. When the same mutation narrows or clears targeting_overlay.placement_selection, this complete replacement MUST remove or reroute every assignment reference that would otherwise be orphaned; the seller validates both changes atomically.' ), + NoExplicitNull, ] = None creatives: Annotated[ list[creative_asset.CreativeAsset] | None, @@ -192,6 +211,7 @@ class PackageUpdate(AdCPBaseModel): max_length=100, min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/product_discovery_criteria.py b/src/adcp/types/domains/media_buy/product_discovery_criteria.py index 565df258f..c4019add4 100644 --- a/src/adcp/types/domains/media_buy/product_discovery_criteria.py +++ b/src/adcp/types/domains/media_buy/product_discovery_criteria.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/product_discovery_criteria.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -41,49 +41,57 @@ class ProductDiscoveryCriteria(AdCPBaseModel): description='Exact seller-issued products to retrieve or use as proposal candidates.', min_length=1, ), + NoExplicitNull, ] = None offer_filters: Annotated[ product_offer_filters.ProductOfferFilters | None, Field( description='Hard commercial and product-characteristic filters. Every returned product MUST satisfy them. These fields do not become package delivery targeting.' ), + NoExplicitNull, ] = None targeting_overlay: Annotated[ targeting.TargetingOverlay | None, Field( description='Concrete delivery constraints the buyer expects to carry into create_media_buy. Returned product pricing and forecasts MUST reflect the effective targeting. A product that cannot apply the request exactly is omitted or returned as a distinct configured product with sparse Product.targeting_resolution modifications; absence of that resolution means exact acceptance.' ), + NoExplicitNull, ] = None media_buy_frequency_cap: Annotated[ media_buy_frequency_cap_1.MediaBuyFrequencyCap | None, Field( description='Concrete aggregate max-impression cap expected at the MediaBuy root. Every returned product must participate in one shared counter for this exact value. Proposals carry it in commercial_terms.frequency_cap.' ), + NoExplicitNull, ] = None required_overlay_support: Annotated[ targeting_overlay_requirements.TargetingOverlayRequirements | None, Field( description='Minimum product-scoped targeting dimensions the buyer must be able to select independently on packages later. This requests capability, not current values, value-specific availability, a forecast for every later value, or one product per value.' ), + NoExplicitNull, ] = None required_media_buy_support: Annotated[ media_buy_support_requirements.ProductMediaBuySupportRequirements | None, Field( description='Minimum product participation in shared MediaBuy-level controls. For frequency caps, every returned product is promising composability in one aggregate seller counter for each requested per unit.' ), + NoExplicitNull, ] = None outcome_target: Annotated[ outcome_target_1.OutcomeTarget | None, Field( description='Reverse-forecast planning input for sellers declaring media_buy.outcome_target: a goal plus a volume, a cost target, or both, answered on proposals. Inert on list_products for every seller. See outcome-target.json.' ), + NoExplicitNull, ] = None acceptance_context: Annotated[ acceptance_context_1.AcceptanceContext | None, Field( description='Structured campaign and advertiser facts for coarse acceptance-policy matching. A matching product is not a guarantee of final acceptance.' ), + NoExplicitNull, ] = None - catalog: catalog_selection.CatalogSelection | None = None - policy_ids: Annotated[list[PolicyId] | None, Field(min_length=1)] = None - ext: dict[str, Any] | None = None + catalog: Annotated[catalog_selection.CatalogSelection | None, NoExplicitNull] = None + policy_ids: Annotated[list[PolicyId] | None, Field(min_length=1), NoExplicitNull] = None + ext: Annotated[dict[str, Any] | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/product_purchase.py b/src/adcp/types/domains/media_buy/product_purchase.py index 571908505..49f84e9fe 100644 --- a/src/adcp/types/domains/media_buy/product_purchase.py +++ b/src/adcp/types/domains/media_buy/product_purchase.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/product_purchase.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat @@ -40,6 +40,7 @@ class ProductPurchase(AdCPBaseModel): Field( description='Resolved selected pricing terms. Optional on buy_products input, where pricing_option_id plus the versioned feed identifies the offer; required inside accepted commercial_terms. Its pricing_option_id MUST match the sibling field.' ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -47,6 +48,7 @@ class ProductPurchase(AdCPBaseModel): description='Canonical format options selected from the published product offer. Legacy named-format identifiers are not accepted.', min_length=1, ), + NoExplicitNull, ] = None catalog_ids: Annotated[ list[CatalogId] | None, @@ -54,6 +56,7 @@ class ProductPurchase(AdCPBaseModel): description='Previously synchronized account catalog IDs promoted by this selection. Callers manage catalog bodies through sync_catalogs rather than inlining them here.', min_length=1, ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -61,6 +64,7 @@ class ProductPurchase(AdCPBaseModel): description='Hard spend cap for this selection in the media-buy currency. In seller-optimized allocation it is an optional ceiling, not a reserved allocation, and requires advertised media_buy.features.seller_optimized_package_budgets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation.', ge=0.0, ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -68,6 +72,7 @@ class ProductPurchase(AdCPBaseModel): description="Optional hard daily spend ceiling for this purchase. It is subordinate to the media-buy aggregate daily cap and is not a reserved daily allocation. Its day boundary is the media buy's budget_cap_timezone.", ge=0.0, ), + NoExplicitNull, ] = None min_spend_target: Annotated[ StrictFloat | None, @@ -75,37 +80,44 @@ class ProductPurchase(AdCPBaseModel): description='Soft lifetime spend target for seller-optimized allocation. Requires advertised media_buy.features.seller_optimized_min_spend_targets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation.', ge=0.0, ), + NoExplicitNull, ] = None impressions: Annotated[ - StrictFloat | None, Field(ge=0.0, title='Product Purchase Impressions') + StrictFloat | None, Field(ge=0.0, title='Product Purchase Impressions'), NoExplicitNull ] = None start_time: Annotated[ AwareDatetime | None, Field( description='Resolved package flight start. On direct-purchase input, omission inherits the MediaBuy start; accepted proposal snapshots carry the resolved timestamp.' ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description='Resolved package flight end. On direct-purchase input, omission inherits the MediaBuy end; accepted proposal snapshots carry the resolved timestamp.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Purchase pacing, subordinate to media-buy pacing. In seller-optimized allocation, pacing that differs from the media-buy pacing requires advertised media_buy.features.seller_optimized_package_pacing; otherwise rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None - bidding: bidding_policy.BiddingPolicy | None = None + bidding: Annotated[bidding_policy.BiddingPolicy | None, NoExplicitNull] = None targeting_overlay: Annotated[ targeting.TargetingOverlay | None, Field( description="Resolved effective buyer-selected targeting, including compatible wholesale signal selections, applied within the product's published targeting contract. Cleared dimensions are omitted; null is invalid in an accepted snapshot." ), + NoExplicitNull, ] = None optimization_goals: Annotated[ - list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, Field(min_length=1) + list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, + Field(min_length=1), + NoExplicitNull, ] = None audience_evidence_requirements: Annotated[ product_audience_evidence_requirements.ProductAudienceEvidenceRequirements | None, @@ -113,6 +125,7 @@ class ProductPurchase(AdCPBaseModel): description='Buyer evidence-admissibility policy carried into the accepted purchase snapshot.', title='Product Purchase Audience Evidence Requirements', ), + NoExplicitNull, ] = None audience_evidence_pins: Annotated[ list[audience_evidence_pin.AudienceEvidencePin] | None, @@ -120,26 +133,30 @@ class ProductPurchase(AdCPBaseModel): description='Exact immutable audience-evidence snapshots selected for package construction.', min_length=1, ), + NoExplicitNull, ] = None agency_estimate_number: Annotated[ str | None, Field( description='Package-level agency estimate or authorization reference.', max_length=100 ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque buyer package correlation preserved in the accepted snapshot and readback.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None measurement_terms: Annotated[ canonical_measurement_terms.CanonicalMeasurementTerms | None, Field( description='Published or negotiated billing-measurement and makegood terms for this purchase. Direct buyers may omit this to inherit the product default; accepted proposal snapshots preserve the resolved terms.', title='Product Purchase Measurement Terms', ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[canonical_performance_standard.CanonicalPerformanceStandard] | None, @@ -147,4 +164,5 @@ class ProductPurchase(AdCPBaseModel): description='Published or negotiated metric thresholds and measurement vendors. Direct buyers may omit this to inherit the product defaults; accepted proposal snapshots preserve every applicable standard.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/product_purchase_input.py b/src/adcp/types/domains/media_buy/product_purchase_input.py index 4c213947d..76c19ffbc 100644 --- a/src/adcp/types/domains/media_buy/product_purchase_input.py +++ b/src/adcp/types/domains/media_buy/product_purchase_input.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/product_purchase_input.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat @@ -40,6 +40,7 @@ class ProductPurchaseInput(AdCPBaseModel): Field( description='Resolved selected pricing terms. Optional on buy_products input, where pricing_option_id plus the versioned feed identifies the offer; required inside accepted commercial_terms. Its pricing_option_id MUST match the sibling field.' ), + NoExplicitNull, ] = None format_option_refs: Annotated[ list[format_option_ref.FormatOptionReference] | None, @@ -47,6 +48,7 @@ class ProductPurchaseInput(AdCPBaseModel): description='Canonical format options selected from the published product offer. Legacy named-format identifiers are not accepted.', min_length=1, ), + NoExplicitNull, ] = None catalog_ids: Annotated[ list[CatalogId] | None, @@ -54,6 +56,7 @@ class ProductPurchaseInput(AdCPBaseModel): description='Previously synchronized account catalog IDs promoted by this selection. Callers manage catalog bodies through sync_catalogs rather than inlining them here.', min_length=1, ), + NoExplicitNull, ] = None budget: Annotated[ StrictFloat | None, @@ -61,6 +64,7 @@ class ProductPurchaseInput(AdCPBaseModel): description='Hard spend cap for this selection in the media-buy currency. In seller-optimized allocation it is an optional ceiling, not a reserved allocation, and requires advertised media_buy.features.seller_optimized_package_budgets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation.', ge=0.0, ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -68,6 +72,7 @@ class ProductPurchaseInput(AdCPBaseModel): description="Optional hard daily spend ceiling for this purchase. It is subordinate to the media-buy aggregate daily cap and is not a reserved daily allocation. Its day boundary is the media buy's budget_cap_timezone.", ge=0.0, ), + NoExplicitNull, ] = None min_spend_target: Annotated[ StrictFloat | None, @@ -75,37 +80,44 @@ class ProductPurchaseInput(AdCPBaseModel): description='Soft lifetime spend target for seller-optimized allocation. Requires advertised media_buy.features.seller_optimized_min_spend_targets; otherwise rejected with UNSUPPORTED_FEATURE before any over-subscription validation.', ge=0.0, ), + NoExplicitNull, ] = None impressions: Annotated[ - StrictFloat | None, Field(ge=0.0, title='Product Purchase Impressions') + StrictFloat | None, Field(ge=0.0, title='Product Purchase Impressions'), NoExplicitNull ] = None start_time: Annotated[ AwareDatetime | None, Field( description='Resolved package flight start. On direct-purchase input, omission inherits the MediaBuy start; accepted proposal snapshots carry the resolved timestamp.' ), + NoExplicitNull, ] = None end_time: Annotated[ AwareDatetime | None, Field( description='Resolved package flight end. On direct-purchase input, omission inherits the MediaBuy end; accepted proposal snapshots carry the resolved timestamp.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Purchase pacing, subordinate to media-buy pacing. In seller-optimized allocation, pacing that differs from the media-buy pacing requires advertised media_buy.features.seller_optimized_package_pacing; otherwise rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None - bidding: bidding_policy.BiddingPolicy | None = None + bidding: Annotated[bidding_policy.BiddingPolicy | None, NoExplicitNull] = None targeting_overlay: Annotated[ targeting_input.TargetingOverlayInput | None, Field( description="Per-dimension targeting input. Omission inherits the selected product's configured/default dimension, a non-null value replaces it, and null explicitly suppresses it. A clear cannot remove inherent product scope." ), + NoExplicitNull, ] = None optimization_goals: Annotated[ - list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, Field(min_length=1) + list[canonical_optimization_goal.CanonicalOptimizationGoal] | None, + Field(min_length=1), + NoExplicitNull, ] = None audience_evidence_requirements: Annotated[ product_audience_evidence_requirements.ProductAudienceEvidenceRequirements | None, @@ -113,6 +125,7 @@ class ProductPurchaseInput(AdCPBaseModel): description='Buyer evidence-admissibility policy carried into the accepted purchase snapshot.', title='Product Purchase Audience Evidence Requirements', ), + NoExplicitNull, ] = None audience_evidence_pins: Annotated[ list[audience_evidence_pin.AudienceEvidencePin] | None, @@ -120,26 +133,30 @@ class ProductPurchaseInput(AdCPBaseModel): description='Exact immutable audience-evidence snapshots selected for package construction.', min_length=1, ), + NoExplicitNull, ] = None agency_estimate_number: Annotated[ str | None, Field( description='Package-level agency estimate or authorization reference.', max_length=100 ), + NoExplicitNull, ] = None context: Annotated[ context_1.ContextObject | None, Field( description='Opaque buyer package correlation preserved in the accepted snapshot and readback.' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None measurement_terms: Annotated[ canonical_measurement_terms.CanonicalMeasurementTerms | None, Field( description='Published or negotiated billing-measurement and makegood terms for this purchase. Direct buyers may omit this to inherit the product default; accepted proposal snapshots preserve the resolved terms.', title='Product Purchase Measurement Terms', ), + NoExplicitNull, ] = None performance_standards: Annotated[ list[canonical_performance_standard.CanonicalPerformanceStandard] | None, @@ -147,4 +164,5 @@ class ProductPurchaseInput(AdCPBaseModel): description='Published or negotiated metric thresholds and measurement vendors. Direct buyers may omit this to inherit the product defaults; accepted proposal snapshots preserve every applicable standard.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/product_refinement.py b/src/adcp/types/domains/media_buy/product_refinement.py index d04a34d79..8e4614063 100644 --- a/src/adcp/types/domains/media_buy/product_refinement.py +++ b/src/adcp/types/domains/media_buy/product_refinement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/product_refinement.json -# timestamp: 2026-10-03T00:36:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel @@ -36,11 +36,13 @@ class ProductRefinementRequests3(AdCPBaseModel): str, Field(description='Product ID from a previous product-discovery response.', min_length=1), ] - action: Annotated[Action | None, Field(description='Requested product-level action.')] = ( - Action.include - ) + action: Annotated[ + Action | None, Field(description='Requested product-level action.'), NoExplicitNull + ] = Action.include ask: Annotated[ - str | None, Field(description='What the buyer is asking for on this product.', min_length=1) + str | None, + Field(description='What the buyer is asking for on this product.', min_length=1), + NoExplicitNull, ] = None @@ -61,12 +63,13 @@ class ProductRefinementRequests4(AdCPBaseModel): str, Field(description='Proposal ID from a previous product-discovery response.', min_length=1), ] - action: Annotated[Action11 | None, Field(description='Requested proposal-level action.')] = ( - Action11.include - ) + action: Annotated[ + Action11 | None, Field(description='Requested proposal-level action.'), NoExplicitNull + ] = Action11.include ask: Annotated[ str | None, Field(description='What the buyer is asking for on this proposal.', min_length=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/proposal_budget_constraint.py b/src/adcp/types/domains/media_buy/proposal_budget_constraint.py index fdbc10b80..977d4ee66 100644 --- a/src/adcp/types/domains/media_buy/proposal_budget_constraint.py +++ b/src/adcp/types/domains/media_buy/proposal_budget_constraint.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/proposal_budget_constraint.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat, model_validator @@ -17,8 +17,8 @@ class ProposalBudgetConstraint(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[StrictFloat | None, Field(ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(ge=0.0)] = None + min: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + max: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/proposal_decline.py b/src/adcp/types/domains/media_buy/proposal_decline.py index 971962596..66425271a 100644 --- a/src/adcp/types/domains/media_buy/proposal_decline.py +++ b/src/adcp/types/domains/media_buy/proposal_decline.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/proposal_decline.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..enums import proposal_decline_reason @@ -32,4 +32,5 @@ class ProposalDecline(AdCPBaseModel): max_length=500, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/proposal_refinement.py b/src/adcp/types/domains/media_buy/proposal_refinement.py index 7370dfeca..daa42b41c 100644 --- a/src/adcp/types/domains/media_buy/proposal_refinement.py +++ b/src/adcp/types/domains/media_buy/proposal_refinement.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/proposal_refinement.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T12:09:21+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat from ..core import product_change_map @@ -43,8 +43,8 @@ class Flight(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - start_no_later_than: AwareDatetime | None = None - end_no_earlier_than: AwareDatetime | None = None + start_no_later_than: Annotated[AwareDatetime | None, NoExplicitNull] = None + end_no_earlier_than: Annotated[AwareDatetime | None, NoExplicitNull] = None class Constraints(AdCPBaseModel): @@ -56,24 +56,28 @@ class Constraints(AdCPBaseModel): Field( description='Inclusive bounds checked against commercial_terms.total_budget. Satisfied only when commercial_terms.total_budget is present, its currency matches, and its amount falls within every supplied bound.' ), + NoExplicitNull, ] = None cpm: Annotated[ Cpm | None, Field( description='Hard ceiling on fixed CPM rates. Satisfied only when every entry of commercial_terms.purchases carries pricing with pricing_model cpm or vcpm, a matching currency, and a fixed_price no greater than max. Auction-priced, missing, or non-CPM pricing on any purchase leaves the constraint unsatisfied.' ), + NoExplicitNull, ] = None impressions: Annotated[ Impressions | None, Field( description='Hard minimum contracted impression volume. Satisfied only when every entry of commercial_terms.purchases carries impressions and their sum is at least min.' ), + NoExplicitNull, ] = None flight: Annotated[ Flight | None, Field( description='Hard flight-window bounds checked against commercial_terms.start_time and end_time. start_no_later_than is satisfied only by a concrete start_time at or before the bound; an asap start is unverifiable and therefore unsatisfied. end_no_earlier_than is satisfied only by an end_time at or after the bound.' ), + NoExplicitNull, ] = None @@ -135,24 +139,28 @@ class ProposalRefinement2(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -160,18 +168,21 @@ class ProposalRefinement2(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -191,6 +202,7 @@ class ProposalRefinement3(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints1, @@ -203,12 +215,14 @@ class ProposalRefinement3(AdCPBaseModel): Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -216,18 +230,21 @@ class ProposalRefinement3(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -247,12 +264,14 @@ class ProposalRefinement4(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints2 | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap, @@ -265,6 +284,7 @@ class ProposalRefinement4(AdCPBaseModel): Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -272,18 +292,21 @@ class ProposalRefinement4(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -303,18 +326,21 @@ class ProposalRefinement5(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints3 | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives, @@ -328,18 +354,21 @@ class ProposalRefinement5(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -359,24 +388,28 @@ class ProposalRefinement6(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints4 | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str, @@ -390,12 +423,14 @@ class ProposalRefinement6(AdCPBaseModel): Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -415,24 +450,28 @@ class ProposalRefinement7(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints5 | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -440,6 +479,7 @@ class ProposalRefinement7(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria, @@ -452,6 +492,7 @@ class ProposalRefinement7(AdCPBaseModel): Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None @@ -471,24 +512,28 @@ class ProposalRefinement8(AdCPBaseModel): Field( description='Desired successor proposal. cancellation is valid only when the source proposal is accepted and attached to a non-terminal MediaBuy.' ), + NoExplicitNull, ] = ChangeKind.amendment constraints: Annotated[ Constraints6 | None, Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -496,12 +541,14 @@ class ProposalRefinement8(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True], @@ -533,18 +580,21 @@ class ProposalRefinement9(AdCPBaseModel): Field( description='Hard requirements for every revised proposal. Keys are echoed in unsatisfied_constraints on partial or unable results. A draft that fails any present constraint MUST NOT be returned as revised: the result is partial or unable with reason_code constraint_unsatisfiable — which takes precedence over every other reason code — and the failing keys in unsatisfied_constraints.' ), + NoExplicitNull, ] = None product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Product IDs mapped to membership actions checked against commercial_terms.purchases. include requires presence; omit requires absence. The verbs match legacy get_products refinement.' ), + NoExplicitNull, ] = None alternatives: Annotated[ Alternatives | None, Field( description='Request drafts with distinct commercial terms by count. Every returned alternative MUST carry distinct commercial_terms and therefore a unique terms_digest; diversity preferences remain in ask.' ), + NoExplicitNull, ] = None ask: Annotated[ str | None, @@ -552,18 +602,21 @@ class ProposalRefinement9(AdCPBaseModel): description='What the buyer is asking for on this proposal, or the cancellation reason for action revise. The seller returns a new draft proposal snapshot; this text never directly mutates the source proposal or MediaBuy.', min_length=1, ), + NoExplicitNull, ] = None criteria: Annotated[ product_discovery_criteria.ProductDiscoveryCriteria | None, Field( description='Structured discovery changes for action revise. Each present field replaces that criterion from the source proposal; omitted fields inherit. Concrete targeting, future targeting support, and offer filters belong here rather than being repeated in ask.' ), + NoExplicitNull, ] = None remove_media_buy_frequency_cap: Annotated[ Literal[True] | None, Field( description='Request a revised draft whose commercial terms omit the existing MediaBuy frequency cap. Omission inherits it. To replace it, provide criteria.media_buy_frequency_cap instead.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/media_buy/provide_performance_feedback_request.py b/src/adcp/types/domains/media_buy/provide_performance_feedback_request.py index f4583aacd..78d7cd726 100644 --- a/src/adcp/types/domains/media_buy/provide_performance_feedback_request.py +++ b/src/adcp/types/domains/media_buy/provide_performance_feedback_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/provide_performance_feedback_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -29,5 +30,5 @@ class ProvidePerformanceFeedbackRequest(AdcpRequest, AdcpVersionEnvelope, Perfor pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/refine_proposals_request.py b/src/adcp/types/domains/media_buy/refine_proposals_request.py index 4c726c0b5..8aae650d2 100644 --- a/src/adcp/types/domains/media_buy/refine_proposals_request.py +++ b/src/adcp/types/domains/media_buy/refine_proposals_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/refine_proposals_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -32,6 +32,7 @@ class RefineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -41,6 +42,7 @@ class RefineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, @@ -48,10 +50,15 @@ class RefineProposalsRequest(AdcpRequest, AdcpVersionEnvelope): description='MCP compatibility field: servers ignore this value; A2A uses transport-native Message/Task contextId.', min_length=1, ), + NoExplicitNull, + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - context: context_1.ContextObject | None = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/media_buy/refine_proposals_response.py b/src/adcp/types/domains/media_buy/refine_proposals_response.py index 3a5ea7f32..bb2621ddc 100644 --- a/src/adcp/types/domains/media_buy/refine_proposals_response.py +++ b/src/adcp/types/domains/media_buy/refine_proposals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/refine_proposals_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field, StrictFloat @@ -48,9 +48,9 @@ class TotalBudgetGuidance(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[StrictFloat | None, Field(ge=0.0)] = None - recommended: Annotated[StrictFloat | None, Field(ge=0.0)] = None - max: Annotated[StrictFloat | None, Field(ge=0.0)] = None + min: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + recommended: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None + max: Annotated[StrictFloat | None, Field(ge=0.0), NoExplicitNull] = None currency: Annotated[str, Field(pattern='^[A-Z]{3}$')] @@ -86,7 +86,7 @@ class Proposal(AdCPBaseModel): min_length=1, ), ] - media_buy_id: Annotated[str | None, Field(min_length=1)] = None + media_buy_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None opportunity_id: Annotated[ str | None, Field( @@ -95,8 +95,9 @@ class Proposal(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None - base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1)] = None + base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None proposal_status: Annotated[ Literal['draft'], Field( @@ -104,16 +105,17 @@ class Proposal(AdCPBaseModel): title='Proposal Status', ), ] = 'draft' - accepted_at: AwareDatetime | None = None + accepted_at: Annotated[AwareDatetime | None, NoExplicitNull] = None expires_at: Annotated[ AwareDatetime | None, Field( description='For a draft, the indicative-terms freshness deadline. For a committed proposal, the inventory-hold deadline.' ), + NoExplicitNull, ] = None name: Annotated[str, Field(max_length=500, min_length=1)] - description: Annotated[str | None, Field(max_length=2000)] = None - brief_alignment: Annotated[str | None, Field(max_length=2000)] = None + description: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + brief_alignment: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None commercial_terms: commercial_terms_1.CommercialTerms terms_digest: Annotated[ str, @@ -122,18 +124,20 @@ class Proposal(AdCPBaseModel): pattern='^sha256:[A-Za-z0-9_-]{43}$', ), ] - insertion_order: insertion_order_1.InsertionOrder | None = None + insertion_order: Annotated[insertion_order_1.InsertionOrder | None, NoExplicitNull] = None total_budget_guidance: Annotated[ TotalBudgetGuidance | None, Field( description="Optional budget guidance for this proposal — the planning answer to criteria.outcome_target and to open-budget briefs. commercial_terms.total_budget remains the concrete figure the plan is priced at; this band expresses the seller's recommended range around it. When criteria.outcome_target carries cost_per, the cost answer is commercial_terms.bidding.cost_per and this band's currency equals cost_per.currency." ), + NoExplicitNull, ] = None forecast: Annotated[ canonical_delivery_forecast.CanonicalDeliveryForecast | None, Field( description="Aggregate forecasted delivery for the proposal. For outcome_target requests, points carry the goal's metric or event key in metrics; with cost_per, that is the goal volume planned under the commercial_terms.bidding policy, and currency equals cost_per.currency." ), + NoExplicitNull, ] = None @@ -155,7 +159,7 @@ class Proposal3(AdCPBaseModel): min_length=1, ), ] - media_buy_id: Annotated[str | None, Field(min_length=1)] = None + media_buy_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None opportunity_id: Annotated[ str | None, Field( @@ -164,8 +168,9 @@ class Proposal3(AdCPBaseModel): min_length=1, pattern='^[A-Za-z0-9_.:-]{1,255}$', ), + NoExplicitNull, ] = None - base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1)] = None + base_media_buy_revision: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None proposal_status: Annotated[ Literal['committed'], Field( @@ -173,7 +178,7 @@ class Proposal3(AdCPBaseModel): title='Proposal Status', ), ] = 'committed' - accepted_at: AwareDatetime | None = None + accepted_at: Annotated[AwareDatetime | None, NoExplicitNull] = None expires_at: Annotated[ AwareDatetime, Field( @@ -181,8 +186,8 @@ class Proposal3(AdCPBaseModel): ), ] name: Annotated[str, Field(max_length=500, min_length=1)] - description: Annotated[str | None, Field(max_length=2000)] = None - brief_alignment: Annotated[str | None, Field(max_length=2000)] = None + description: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + brief_alignment: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None commercial_terms: commercial_terms_1.CommercialTerms terms_digest: Annotated[ str, @@ -191,18 +196,20 @@ class Proposal3(AdCPBaseModel): pattern='^sha256:[A-Za-z0-9_-]{43}$', ), ] - insertion_order: insertion_order_1.InsertionOrder | None = None + insertion_order: Annotated[insertion_order_1.InsertionOrder | None, NoExplicitNull] = None total_budget_guidance: Annotated[ TotalBudgetGuidance | None, Field( description="Optional budget guidance for this proposal — the planning answer to criteria.outcome_target and to open-budget briefs. commercial_terms.total_budget remains the concrete figure the plan is priced at; this band expresses the seller's recommended range around it. When criteria.outcome_target carries cost_per, the cost answer is commercial_terms.bidding.cost_per and this band's currency equals cost_per.currency." ), + NoExplicitNull, ] = None forecast: Annotated[ canonical_delivery_forecast.CanonicalDeliveryForecast | None, Field( description="Aggregate forecasted delivery for the proposal. For outcome_target requests, points carry the goal's metric or event key in metrics; with cost_per, that is the goal volume planned under the commercial_terms.bidding policy, and currency equals cost_per.currency." ), + NoExplicitNull, ] = None @@ -224,7 +231,7 @@ class Results9(AdCPBaseModel): ) source_proposal_id: Annotated[str, Field(min_length=1)] outcome: Literal['revised'] = 'revised' - proposal: canonical_proposal.CanonicalProposal | None = None + proposal: Annotated[canonical_proposal.CanonicalProposal | None, NoExplicitNull] = None proposals: Annotated[ list[Proposal], Field( @@ -237,27 +244,31 @@ class Results9(AdCPBaseModel): Field( description='Single most-significant code for this result. constraint_unsatisfiable takes precedence over every other code; an alternatives shortfall alongside an unsatisfied constraint remains visible through proposals.length.' ), + NoExplicitNull, ] = None - reason: Annotated[str | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None unsatisfied_constraints: Annotated[ list[UnsatisfiedConstraint] | None, Field( description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -267,7 +278,7 @@ class Results10(AdCPBaseModel): ) source_proposal_id: Annotated[str, Field(min_length=1)] outcome: Literal['partial'] = 'partial' - proposal: canonical_proposal.CanonicalProposal | None = None + proposal: Annotated[canonical_proposal.CanonicalProposal | None, NoExplicitNull] = None proposals: Annotated[ list[Proposal2], Field( @@ -288,19 +299,22 @@ class Results10(AdCPBaseModel): description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -323,33 +337,38 @@ class Results11(AdCPBaseModel): description='Draft successors produced for a revision. Without alternatives this contains one proposal. With alternatives.count, revised contains exactly that many proposals with unique terms_digest values; fewer or commercially duplicate proposals require partial.', min_length=1, ), + NoExplicitNull, ] = None reason_code: Annotated[ proposal_refinement_reason.ProposalRefinementReason | None, Field( description='Single most-significant code for this result. constraint_unsatisfiable takes precedence over every other code; an alternatives shortfall alongside an unsatisfied constraint remains visible through proposals.length.' ), + NoExplicitNull, ] = None - reason: Annotated[str | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None unsatisfied_constraints: Annotated[ list[UnsatisfiedConstraint] | None, Field( description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -359,13 +378,14 @@ class Results12(AdCPBaseModel): ) source_proposal_id: Annotated[str, Field(min_length=1)] outcome: Literal['unable'] = 'unable' - proposal: canonical_proposal.CanonicalProposal | None = None + proposal: Annotated[canonical_proposal.CanonicalProposal | None, NoExplicitNull] = None proposals: Annotated[ list[canonical_proposal.CanonicalProposal] | None, Field( description='Draft successors produced for a revision. Without alternatives this contains one proposal. With alternatives.count, revised contains exactly that many proposals with unique terms_digest values; fewer or commercially duplicate proposals require partial.', min_length=1, ), + NoExplicitNull, ] = None reason_code: Annotated[ proposal_refinement_reason.ProposalRefinementReason, @@ -380,19 +400,22 @@ class Results12(AdCPBaseModel): description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -408,7 +431,7 @@ class Results15(AdCPBaseModel): ) source_proposal_id: Annotated[str, Field(min_length=1)] outcome: Literal['revised'] = 'revised' - proposal: canonical_proposal.CanonicalProposal | None = None + proposal: Annotated[canonical_proposal.CanonicalProposal | None, NoExplicitNull] = None proposals: Annotated[ list[Proposal4], Field( @@ -421,27 +444,31 @@ class Results15(AdCPBaseModel): Field( description='Single most-significant code for this result. constraint_unsatisfiable takes precedence over every other code; an alternatives shortfall alongside an unsatisfied constraint remains visible through proposals.length.' ), + NoExplicitNull, ] = None - reason: Annotated[str | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None unsatisfied_constraints: Annotated[ list[UnsatisfiedConstraint] | None, Field( description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -451,7 +478,7 @@ class Results16(AdCPBaseModel): ) source_proposal_id: Annotated[str, Field(min_length=1)] outcome: Literal['partial'] = 'partial' - proposal: canonical_proposal.CanonicalProposal | None = None + proposal: Annotated[canonical_proposal.CanonicalProposal | None, NoExplicitNull] = None proposals: Annotated[ list[Proposal5], Field( @@ -472,19 +499,22 @@ class Results16(AdCPBaseModel): description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -507,33 +537,38 @@ class Results17(AdCPBaseModel): description='Draft successors produced for a revision. Without alternatives this contains one proposal. With alternatives.count, revised contains exactly that many proposals with unique terms_digest values; fewer or commercially duplicate proposals require partial.', min_length=1, ), + NoExplicitNull, ] = None reason_code: Annotated[ proposal_refinement_reason.ProposalRefinementReason | None, Field( description='Single most-significant code for this result. constraint_unsatisfiable takes precedence over every other code; an alternatives shortfall alongside an unsatisfied constraint remains visible through proposals.length.' ), + NoExplicitNull, ] = None - reason: Annotated[str | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None unsatisfied_constraints: Annotated[ list[UnsatisfiedConstraint] | None, Field( description='Stable keys from the request constraints object that were not satisfied by every returned draft. A result carrying any key here MUST use outcome partial or unable, never revised.', min_length=1, ), + NoExplicitNull, ] = None unsatisfied_product_changes: Annotated[ product_change_map.ProductChangeMap | None, Field( description='Requested product actions not satisfied by every returned draft. This is a subset of the request product_changes map and is valid only on partial or unable results.' ), + NoExplicitNull, ] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description="Confirmation of structured targeting interpreted from this refinement's explicit hard prose instructions. Required when that interpretation materially affects the revised proposal's eligibility, pricing, or forecast; otherwise a best practice." ), + NoExplicitNull, ] = None @@ -558,6 +593,7 @@ class RefineProposalsResponse1(AdcpResponse, AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None results: Annotated[ list[Results8] | Results, @@ -573,12 +609,12 @@ class RefineProposalsResponse1(AdcpResponse, AdCPBaseModel): ), ] status: Literal['completed'] = 'completed' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class RefineProposalsResponse2(AdcpResponse, CompactTaskSubmitted): @@ -592,6 +628,7 @@ class RefineProposalsResponse2(AdcpResponse, CompactTaskSubmitted): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None results: Annotated[ list[Results14] | Results | None, @@ -599,20 +636,22 @@ class RefineProposalsResponse2(AdcpResponse, CompactTaskSubmitted): description="Ordered results. If any result is finalized, every result MUST be finalized; a finalize batch either creates every requested hold or none. Every returned proposal carries parent_proposal_id equal to the result's source_proposal_id, making negotiation lineage reconstructible from the proposals alone.", min_length=1, ), + NoExplicitNull, ] = None products: Annotated[ list[canonical_product.CanonicalProduct] | None, Field( description="Canonical products needed to evaluate the resulting terms. For revised or partial results whose effective criteria contain property or collection lists, each affected product MUST carry fresh list_applications receipts from the revision's product reevaluation. Finalization changes no terms and MAY repeat the receipts already bound to the source proposal rather than reevaluating them." ), + NoExplicitNull, ] = None - status: Status | None = None - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + status: Annotated[Status | None, NoExplicitNull] = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None RefineProposalsResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/request_proposals_request.py b/src/adcp/types/domains/media_buy/request_proposals_request.py index d16ee042f..7a1b7187b 100644 --- a/src/adcp/types/domains/media_buy/request_proposals_request.py +++ b/src/adcp/types/domains/media_buy/request_proposals_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/request_proposals_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..core import brand_key, canonical_account_ref @@ -47,6 +48,7 @@ class RequestProposalsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -56,6 +58,7 @@ class RequestProposalsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None context_id: Annotated[ str | None, @@ -63,10 +66,15 @@ class RequestProposalsRequest(AdcpRequest, AdcpVersionEnvelope): description='MCP compatibility field: servers ignore this value; A2A uses transport-native Message/Task contextId.', min_length=1, ), + NoExplicitNull, + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + governance_context: Annotated[ + str | None, Field(max_length=4096, min_length=1), NoExplicitNull + ] = None + push_notification_config: Annotated[ + push_notification_config_1.PushNotificationConfig | None, NoExplicitNull ] = None - context: context_1.ContextObject | None = None - governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None - push_notification_config: push_notification_config_1.PushNotificationConfig | None = None idempotency_key: Annotated[ str, Field( @@ -81,12 +89,14 @@ class RequestProposalsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Alternative brand source for proposal terms. Provide either this natural-key account containing brand and operator or top-level brand, not both.' ), + NoExplicitNull, ] = None brand: Annotated[ brand_key.BrandKey | None, Field( description='Alternative brand source for proposal terms. Provide either top-level brand or a natural-key account containing brand and operator, not both.' ), + NoExplicitNull, ] = None brief: Annotated[ str, @@ -95,12 +105,15 @@ class RequestProposalsRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, ), ] - criteria: product_discovery_criteria.ProductDiscoveryCriteria | None = None + criteria: Annotated[ + product_discovery_criteria.ProductDiscoveryCriteria | None, NoExplicitNull + ] = None opportunity: Annotated[ Opportunity | None, Field( description='Optional planning-cycle context that the seller associates with every proposal created by this request.' ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/media_buy/request_proposals_response.py b/src/adcp/types/domains/media_buy/request_proposals_response.py index a83da6b01..5f7b864b6 100644 --- a/src/adcp/types/domains/media_buy/request_proposals_response.py +++ b/src/adcp/types/domains/media_buy/request_proposals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/request_proposals_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AwareDatetime, ConfigDict, Field @@ -53,7 +53,7 @@ class IncompleteItem(AdCPBaseModel): ) scope: Scope description: Annotated[str, Field(min_length=1)] - estimated_wait: duration.Duration | None = None + estimated_wait: Annotated[duration.Duration | None, NoExplicitNull] = None class ProductId(Suggestion): @@ -86,6 +86,7 @@ class PurchaseContinuation(AdCPBaseModel): description='Real seller-issued pricing fence from the same account-scoped list_products response, when the seller versions pricing independently.', min_length=1, ), + NoExplicitNull, ] = None @@ -215,10 +216,11 @@ class RequestProposalsResponse1(AdcpResponse, AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None outcome: Literal['proposed'] = 'proposed' - reason: Annotated[str | None, Field(min_length=1)] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None proposals: Annotated[list[Proposal], Field(min_length=1)] products: Annotated[list[canonical_product.CanonicalProduct], Field(min_length=1)] incomplete: Annotated[ @@ -227,6 +229,7 @@ class RequestProposalsResponse1(AdcpResponse, AdCPBaseModel): description='Usable partial discovery result retained from the established get_products response. Absence means the source response did not declare an incomplete scope; it does not authorize an adapter to infer missing proposal terms.', min_length=1, ), + NoExplicitNull, ] = None purchase_continuation: Annotated[ PurchaseContinuation | PurchaseContinuation1 | None, @@ -234,20 +237,22 @@ class RequestProposalsResponse1(AdcpResponse, AdCPBaseModel): deprecated=True, description='Deprecated AdCP 3.x projection instruction for purchasing products returned without a proposal. This is coordinator state, not a claim that the established seller implements a compact task. A native 3.2 seller MUST NOT emit it.', ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description='Response-level confirmation of structured targeting interpreted from explicit hard requirements in the brief. Sellers MUST emit this when that interpretation materially affects eligibility, pricing, or forecasting; otherwise it remains a best practice. Product-specific changes to explicit criteria.targeting_overlay values remain on Product.targeting_resolution.' ), + NoExplicitNull, ] = None status: Literal['completed'] = 'completed' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class RequestProposalsResponse2(AdcpResponse, AdCPBaseModel): @@ -261,11 +266,12 @@ class RequestProposalsResponse2(AdcpResponse, AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None outcome: Literal['products_available'] = 'products_available' - reason: Annotated[str | None, Field(min_length=1)] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None - proposals: Annotated[list[Proposal] | None, Field(min_length=1)] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None + proposals: Annotated[list[Proposal] | None, Field(min_length=1), NoExplicitNull] = None products: Annotated[list[canonical_product.CanonicalProduct], Field(min_length=1)] incomplete: Annotated[ list[IncompleteItem5] | None, @@ -273,6 +279,7 @@ class RequestProposalsResponse2(AdcpResponse, AdCPBaseModel): description='Usable partial discovery result retained from the established get_products response. Absence means the source response did not declare an incomplete scope; it does not authorize an adapter to infer missing proposal terms.', min_length=1, ), + NoExplicitNull, ] = None purchase_continuation: Annotated[ PurchaseContinuation2 | PurchaseContinuation3, @@ -286,14 +293,15 @@ class RequestProposalsResponse2(AdcpResponse, AdCPBaseModel): Field( description='Response-level confirmation of structured targeting interpreted from explicit hard requirements in the brief. Sellers MUST emit this when that interpretation materially affects eligibility, pricing, or forecasting; otherwise it remains a best practice. Product-specific changes to explicit criteria.targeting_overlay values remain on Product.targeting_resolution.' ), + NoExplicitNull, ] = None status: Literal['completed'] = 'completed' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class RequestProposalsResponse3(AdcpResponse, AdCPBaseModel): @@ -307,18 +315,22 @@ class RequestProposalsResponse3(AdcpResponse, AdCPBaseModel): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None outcome: Literal['rejected'] = 'rejected' reason: Annotated[str, Field(min_length=1)] - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None - proposals: Annotated[list[Proposal] | None, Field(min_length=1)] = None - products: Annotated[list[canonical_product.CanonicalProduct] | None, Field(min_length=1)] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None + proposals: Annotated[list[Proposal] | None, Field(min_length=1), NoExplicitNull] = None + products: Annotated[ + list[canonical_product.CanonicalProduct] | None, Field(min_length=1), NoExplicitNull + ] = None incomplete: Annotated[ list[IncompleteItem6] | None, Field( description='Usable partial discovery result retained from the established get_products response. Absence means the source response did not declare an incomplete scope; it does not authorize an adapter to infer missing proposal terms.', min_length=1, ), + NoExplicitNull, ] = None purchase_continuation: Annotated[ PurchaseContinuation4 | PurchaseContinuation5 | None, @@ -326,20 +338,22 @@ class RequestProposalsResponse3(AdcpResponse, AdCPBaseModel): deprecated=True, description='Deprecated AdCP 3.x projection instruction for purchasing products returned without a proposal. This is coordinator state, not a claim that the established seller implements a compact task. A native 3.2 seller MUST NOT emit it.', ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description='Response-level confirmation of structured targeting interpreted from explicit hard requirements in the brief. Sellers MUST emit this when that interpretation materially affects eligibility, pricing, or forecasting; otherwise it remains a best practice. Product-specific changes to explicit criteria.targeting_overlay values remain on Product.targeting_resolution.' ), + NoExplicitNull, ] = None status: Literal['completed'] = 'completed' - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None class RequestProposalsResponse4(AdcpResponse, CompactTaskSubmitted): @@ -353,18 +367,22 @@ class RequestProposalsResponse4(AdcpResponse, CompactTaskSubmitted): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, + ] = None + outcome: Annotated[Outcome | None, NoExplicitNull] = None + reason: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + suggestions: Annotated[list[Suggestion] | None, Field(min_length=1), NoExplicitNull] = None + proposals: Annotated[list[Proposal] | None, Field(min_length=1), NoExplicitNull] = None + products: Annotated[ + list[canonical_product.CanonicalProduct] | None, Field(min_length=1), NoExplicitNull ] = None - outcome: Outcome | None = None - reason: Annotated[str | None, Field(min_length=1)] = None - suggestions: Annotated[list[Suggestion] | None, Field(min_length=1)] = None - proposals: Annotated[list[Proposal] | None, Field(min_length=1)] = None - products: Annotated[list[canonical_product.CanonicalProduct] | None, Field(min_length=1)] = None incomplete: Annotated[ list[IncompleteItem7] | None, Field( description='Usable partial discovery result retained from the established get_products response. Absence means the source response did not declare an incomplete scope; it does not authorize an adapter to infer missing proposal terms.', min_length=1, ), + NoExplicitNull, ] = None purchase_continuation: Annotated[ PurchaseContinuation6 | PurchaseContinuation7 | None, @@ -372,20 +390,22 @@ class RequestProposalsResponse4(AdcpResponse, CompactTaskSubmitted): deprecated=True, description='Deprecated AdCP 3.x projection instruction for purchasing products returned without a proposal. This is coordinator state, not a claim that the established seller implements a compact task. A native 3.2 seller MUST NOT emit it.', ), + NoExplicitNull, ] = None targeting_resolution: Annotated[ get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None, Field( description='Response-level confirmation of structured targeting interpreted from explicit hard requirements in the brief. Sellers MUST emit this when that interpretation materially affects eligibility, pricing, or forecasting; otherwise it remains a best practice. Product-specific changes to explicit criteria.targeting_overlay values remain on Product.targeting_resolution.' ), + NoExplicitNull, ] = None - status: Status | None = None - task_id: Annotated[str | None, Field(min_length=1)] = None - message: Annotated[str | None, Field(max_length=2000)] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None - replayed: Literal[True] | None = None + status: Annotated[Status | None, NoExplicitNull] = None + task_id: Annotated[str | None, Field(min_length=1), NoExplicitNull] = None + message: Annotated[str | None, Field(max_length=2000), NoExplicitNull] = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None + replayed: Annotated[Literal[True] | None, NoExplicitNull] = None RequestProposalsResponse = Annotated[ diff --git a/src/adcp/types/domains/media_buy/sync_audiences_request.py b/src/adcp/types/domains/media_buy/sync_audiences_request.py index dacc27e58..3db210466 100644 --- a/src/adcp/types/domains/media_buy/sync_audiences_request.py +++ b/src/adcp/types/domains/media_buy/sync_audiences_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_audiences_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool @@ -41,24 +41,29 @@ class Audience(AdCPBaseModel): description="Buyer's identifier for this audience. Used to reference the audience in targeting overlays." ), ] - name: Annotated[str | None, Field(description='Human-readable name for this audience')] = None + name: Annotated[ + str | None, Field(description='Human-readable name for this audience'), NoExplicitNull + ] = None description: Annotated[ str | None, Field( description="Human-readable description of this audience's composition or purpose (e.g., 'High-value customers who purchased in the last 90 days')." ), + NoExplicitNull, ] = None audience_type: Annotated[ AudienceType | None, Field( description="Intended use for this audience. 'crm': target these users. 'suppression': exclude these users from delivery. 'lookalike_seed': use as a seed for the seller's lookalike modeling. Sellers may handle audiences differently based on type (e.g., suppression lists bypass minimum size requirements on some platforms)." ), + NoExplicitNull, ] = None tags: Annotated[ list[Tag] | None, Field( description="Buyer-defined tags for organizing and filtering audiences (e.g., 'holiday_2026', 'high_ltv'). Tags are stored by the seller and returned in discovery-only calls." ), + NoExplicitNull, ] = None add: Annotated[ list[audience_member.AudienceMember] | None, @@ -66,6 +71,7 @@ class Audience(AdCPBaseModel): description='Members to add to this audience. Hashed before sending — normalize emails to lowercase+trim, phones to E.164.', min_length=1, ), + NoExplicitNull, ] = None remove: Annotated[ list[audience_member.AudienceMember] | None, @@ -73,24 +79,28 @@ class Audience(AdCPBaseModel): description='Members to remove from this audience. If the same identifier appears in both add and remove in a single request, remove takes precedence.', min_length=1, ), + NoExplicitNull, ] = None source: Annotated[ audience_source.AudienceSource | None, Field( description='External source reference: the seller ingests membership from a shared dataset or vendor-distributed segment instead of inline member deltas. Mutually exclusive with add/remove — an audience is either buyer-pushed or externally sourced, and transport is fixed at creation: a cross-transport upsert (member deltas against a sourced audience, or source against a pushed one) is rejected with CONFLICT (error.field: audience_id); convert by delete-and-recreate. Only send source kinds whose activation pattern the seller declared via audience_activation (undeclared kinds are rejected with UNSUPPORTED_FEATURE). Experimental — see the media_buy.audience_activation feature.' ), + NoExplicitNull, ] = None delete: Annotated[ StrictBool | None, Field( description='When true, delete this audience from the account entirely. All other fields on this audience object are ignored. Use this to delete a specific audience without affecting others.' ), + NoExplicitNull, ] = None consent_basis: Annotated[ consent_basis_1.ConsentBasis | None, Field( description='GDPR lawful basis for processing this audience list. Informational — not validated by the protocol, but required by some sellers operating in regulated markets (e.g. EU). When omitted, the buyer asserts they have a lawful basis appropriate to their jurisdiction.' ), + NoExplicitNull, ] = None @@ -116,12 +126,14 @@ class SyncAudiencesRequest(AdcpRequest, AdcpVersionEnvelope): description='Audiences to sync (create or update). When omitted, the call is discovery-only and returns all existing audiences on the account without modification.', min_length=1, ), + NoExplicitNull, ] = None delete_missing: Annotated[ StrictBool | None, Field( description='When true, buyer-managed audiences on the account not included in this sync will be removed. Does not affect seller-managed audiences. Do not combine with an omitted audiences array or all buyer-managed audiences will be deleted.' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_audiences_response.py b/src/adcp/types/domains/media_buy/sync_audiences_response.py index 6dd1e2a27..02bc2dd2c 100644 --- a/src/adcp/types/domains/media_buy/sync_audiences_response.py +++ b/src/adcp/types/domains/media_buy/sync_audiences_response.py @@ -73,7 +73,7 @@ class SyncAudiencesResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope class SyncAudiencesResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_input_required.py b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_input_required.py index c6801acb0..0e2a5fab3 100644 --- a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_input_required.py +++ b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/sync_catalogs_async_response_input_required.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -30,6 +30,7 @@ class SyncCatalogsInputRequired(AdCPBaseModel): Field( description='Reason code indicating why buyer input is needed. APPROVAL_REQUIRED: platform requires explicit approval before activating the catalog. FEED_VALIDATION: feed URL returned unexpected format or schema errors. ITEM_REVIEW: platform flagged items for manual review. FEED_ACCESS: platform cannot access the feed URL (authentication, CORS, etc.).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_submitted.py b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_submitted.py index a2852c7fe..95e78e511 100644 --- a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_submitted.py +++ b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/sync_catalogs_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,12 +36,14 @@ class SyncCatalogsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Catalog ingestion queued; typical turnaround 5–15 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_working.py b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_working.py index d9691add8..f42da8036 100644 --- a/src/adcp/types/domains/media_buy/sync_catalogs_async_response_working.py +++ b/src/adcp/types/domains/media_buy/sync_catalogs_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/sync_catalogs_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -18,31 +18,44 @@ class SyncCatalogsWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description="Current step or phase of the operation (e.g., 'Fetching product feed', 'Validating items', 'Platform review')" ), + NoExplicitNull, ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None catalogs_processed: Annotated[ - SchemaInt | None, Field(description='Number of catalogs processed so far', ge=0) + SchemaInt | None, + Field(description='Number of catalogs processed so far', ge=0), + NoExplicitNull, ] = None catalogs_total: Annotated[ - SchemaInt | None, Field(description='Total number of catalogs to process', ge=0) + SchemaInt | None, + Field(description='Total number of catalogs to process', ge=0), + NoExplicitNull, ] = None items_processed: Annotated[ SchemaInt | None, Field(description='Total number of catalog items processed across all catalogs', ge=0), + NoExplicitNull, ] = None items_total: Annotated[ SchemaInt | None, Field(description='Total number of catalog items to process across all catalogs', ge=0), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_catalogs_request.py b/src/adcp/types/domains/media_buy/sync_catalogs_request.py index df99e8e9f..6b5663e3f 100644 --- a/src/adcp/types/domains/media_buy/sync_catalogs_request.py +++ b/src/adcp/types/domains/media_buy/sync_catalogs_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_catalogs_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import ( @@ -47,6 +48,7 @@ class SyncCatalogsRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None item_availability_updates: Annotated[ list[catalog_item_availability_update.CatalogItemAvailabilityUpdate] | None, @@ -55,6 +57,7 @@ class SyncCatalogsRequest(AdcpRequest, AdcpVersionEnvelope): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None item_availability_queries: Annotated[ list[catalog_item_availability_ref.CatalogItemAvailabilityReference] | None, @@ -63,6 +66,7 @@ class SyncCatalogsRequest(AdcpRequest, AdcpVersionEnvelope): max_length=1000, min_length=1, ), + NoExplicitNull, ] = None catalog_ids: Annotated[ list[str] | None, @@ -71,30 +75,35 @@ class SyncCatalogsRequest(AdcpRequest, AdcpVersionEnvelope): max_length=50, min_length=1, ), + NoExplicitNull, ] = None delete_missing: Annotated[ StrictBool | None, Field( description='When true, buyer-managed catalogs on the account not included in this sync will be removed. Does not affect seller-managed catalogs. Requires catalogs; item_availability_updates alone cannot define deletion scope.' ), + NoExplicitNull, ] = False dry_run: Annotated[ StrictBool | None, Field( description='When true, preview catalog create, update, and delete changes without applying them. MUST NOT be combined with item_availability_updates.' ), + NoExplicitNull, ] = False validation_mode: Annotated[ validation_mode_1.ValidationMode | None, Field( description="Validation strictness for semantically valid-looking catalog and item entries. In strict mode (default), an unknown, inaccessible, unauthorized, or stale-generation catalog/item reference, a known seller-managed catalog, a stale expected_overlay_revision, or another per-entry error fails the entire operation before any catalog or availability mutation. In lenient mode, the seller returns a positionally matched failed result for each such item entry and processes the remaining valid entries. Unknown, inaccessible, unauthorized, and stale-generation references MUST be observationally equivalent: code REFERENCE_NOT_FOUND, message exactly 'Catalog item not found', recovery 'correctable', and no field, suggestion, retry_after, issues, details, or resource metadata. Authorization and lookup MUST use the same externally observable failure path and SHOULD avoid materially distinguishable timing. A known seller-managed catalog may use INVALID_REQUEST only after catalog access is authorized. Request-schema failures, duplicate identity tuples, unsupported capability, batch-limit excess, dry_run conflicts, and mixed requests that cannot commit synchronously and atomically are operation-level failures before lookup or mutation in both modes. A stale revision uses CONFLICT without mutation." ), + NoExplicitNull, ] = validation_mode_1.ValidationMode.strict push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async sync notifications. Publisher will send webhook when sync completes if operation takes longer than immediate response time (common for large feeds requiring platform review).' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_catalogs_response.py b/src/adcp/types/domains/media_buy/sync_catalogs_response.py index 9337b697d..f6767bf71 100644 --- a/src/adcp/types/domains/media_buy/sync_catalogs_response.py +++ b/src/adcp/types/domains/media_buy/sync_catalogs_response.py @@ -67,7 +67,7 @@ class SyncCatalogsResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) class SyncCatalogsResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/media_buy/sync_event_sources_request.py b/src/adcp/types/domains/media_buy/sync_event_sources_request.py index 4a2abca4d..ef836dd84 100644 --- a/src/adcp/types/domains/media_buy/sync_event_sources_request.py +++ b/src/adcp/types/domains/media_buy/sync_event_sources_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_event_sources_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field, StrictBool @@ -31,15 +31,16 @@ class EventSource(AdCPBaseModel): extra='allow', ) event_source_id: Annotated[str, Field(description='Unique identifier for this event source')] - name: Annotated[str | None, Field(description='Human-readable name for this event source')] = ( - None - ) + name: Annotated[ + str | None, Field(description='Human-readable name for this event source'), NoExplicitNull + ] = None event_types: Annotated[ list[event_type.EventType] | None, Field( description='Event types this source handles (e.g. purchase, lead). If omitted, accepts all event types.', min_length=1, ), + NoExplicitNull, ] = None value_currencies: Annotated[ list[ValueCurrency] | None, @@ -47,22 +48,26 @@ class EventSource(AdCPBaseModel): description='ISO 4217 currencies this source accepts for monetary custom_data values. Required for a source used by canonical ROAS. Once declared, log_event monetary records for this source MUST include a currency from this list. Sources omitting the field retain legacy event compatibility but are ineligible for canonical ROAS. One source may list multiple currencies; each media buy consumes only records matching its own currency and no conversion occurs.', min_length=1, ), + NoExplicitNull, ] = None action_source: Annotated[ action_source_1.ActionSource | None, Field( description='Flat action-source category for this event source, such as website, app, in_store, system_generated, or other. Producers SHOULD also populate `surface` when the flat value is too coarse, and SHOULD keep this field aligned with `surface.category` when the category is also an ActionSource value.' ), + NoExplicitNull, ] = None allowed_domains: Annotated[ list[str] | None, Field(description='Domains authorized to send events for this event source', min_length=1), + NoExplicitNull, ] = None surface: Annotated[ event_surface.EventSurface | None, Field( description='Optional structured surface this source represents, such as an owned channel, profile, feed, podcast, newsletter list, website, app, or store. Complements the flat `action_source` returned by sellers and avoids putting optimization-relevant surface meaning only in `ext`.' ), + NoExplicitNull, ] = None @@ -88,10 +93,12 @@ class SyncEventSourcesRequest(AdcpRequest, AdcpVersionEnvelope): description='Event sources to sync (create or update). When omitted, the call is discovery-only and returns all existing event sources on the account without modification.', min_length=1, ), + NoExplicitNull, ] = None delete_missing: Annotated[ StrictBool | None, Field(description='When true, event sources not included in this sync will be removed'), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_reporting_receipts_request.py b/src/adcp/types/domains/media_buy/sync_reporting_receipts_request.py index a04986daf..69d758f5f 100644 --- a/src/adcp/types/domains/media_buy/sync_reporting_receipts_request.py +++ b/src/adcp/types/domains/media_buy/sync_reporting_receipts_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_reporting_receipts_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..core import canonical_account_ref @@ -31,6 +32,7 @@ class SyncReportingReceiptsRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -40,6 +42,7 @@ class SyncReportingReceiptsRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None account: canonical_account_ref.CanonicalAccountReference idempotency_key: Annotated[ @@ -52,7 +55,9 @@ class SyncReportingReceiptsRequest(AdcpRequest, AdcpVersionEnvelope): ), ] receipts: Annotated[ - list[reporting_receipt.ReportingReceipt] | None, Field(max_length=100, min_length=1) + list[reporting_receipt.ReportingReceipt] | None, + Field(max_length=100, min_length=1), + NoExplicitNull, ] = None adjustment_receipts: Annotated[ list[reporting_adjustment_receipt.ReportingAdjustmentReceipt] | None, @@ -61,9 +66,10 @@ class SyncReportingReceiptsRequest(AdcpRequest, AdcpVersionEnvelope): max_length=100, min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> SyncReportingReceiptsRequest: diff --git a/src/adcp/types/domains/media_buy/sync_reporting_receipts_response.py b/src/adcp/types/domains/media_buy/sync_reporting_receipts_response.py index a5f174bdc..b3b9d7aab 100644 --- a/src/adcp/types/domains/media_buy/sync_reporting_receipts_response.py +++ b/src/adcp/types/domains/media_buy/sync_reporting_receipts_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_reporting_receipts_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -76,5 +76,5 @@ class SyncReportingReceiptsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolE list[Results20 | Results21 | Results22 | Results23 | Results], Field(max_length=100, min_length=1), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_reporting_status_request.py b/src/adcp/types/domains/media_buy/sync_reporting_status_request.py index 340281ea4..5d1d3f36d 100644 --- a/src/adcp/types/domains/media_buy/sync_reporting_status_request.py +++ b/src/adcp/types/domains/media_buy/sync_reporting_status_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_reporting_status_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import canonical_account_ref @@ -28,6 +29,7 @@ class SyncReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): examples=['3.0', '3.1', '3.1-beta', '3.1-rc.1'], pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$', ), + NoExplicitNull, ] = None adcp_major_version: Annotated[ SchemaInt | None, @@ -37,6 +39,7 @@ class SyncReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): ge=1, le=99, ), + NoExplicitNull, ] = None account: canonical_account_ref.CanonicalAccountReference idempotency_key: Annotated[ @@ -56,5 +59,5 @@ class SyncReportingStatusRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/sync_reporting_status_response.py b/src/adcp/types/domains/media_buy/sync_reporting_status_response.py index c0f57cbc1..79a03ddf0 100644 --- a/src/adcp/types/domains/media_buy/sync_reporting_status_response.py +++ b/src/adcp/types/domains/media_buy/sync_reporting_status_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_reporting_status_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -57,5 +57,5 @@ class SyncReportingStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv ), ] = 'completed' results: Annotated[list[Results | Results26 | Results27], Field(max_length=100, min_length=1)] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/update_media_buy_async_response_input_required.py b/src/adcp/types/domains/media_buy/update_media_buy_async_response_input_required.py index c5d5034a9..0c048040f 100644 --- a/src/adcp/types/domains/media_buy/update_media_buy_async_response_input_required.py +++ b/src/adcp/types/domains/media_buy/update_media_buy_async_response_input_required.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_async_response_input_required.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -24,7 +24,9 @@ class UpdateMediaBuyInputRequired(AdCPBaseModel): extra='allow', ) reason: Annotated[ - Reason | None, Field(description='Reason code indicating why input is needed') + Reason | None, + Field(description='Reason code indicating why input is needed'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/update_media_buy_async_response_submitted.py b/src/adcp/types/domains/media_buy/update_media_buy_async_response_submitted.py index 5b7d9c15e..922029bdb 100644 --- a/src/adcp/types/domains/media_buy/update_media_buy_async_response_submitted.py +++ b/src/adcp/types/domains/media_buy/update_media_buy_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -36,12 +36,14 @@ class UpdateMediaBuySubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Awaiting operator re-approval; typical turnaround 2–4 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.", max_length=2000, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/update_media_buy_async_response_working.py b/src/adcp/types/domains/media_buy/update_media_buy_async_response_working.py index 1542e9dd8..6ef171dbf 100644 --- a/src/adcp/types/domains/media_buy/update_media_buy_async_response_working.py +++ b/src/adcp/types/domains/media_buy/update_media_buy_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -18,14 +18,20 @@ class UpdateMediaBuyWorking(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull + ] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/update_media_buy_request.py b/src/adcp/types/domains/media_buy/update_media_buy_request.py index 0cf88c012..cfb718333 100644 --- a/src/adcp/types/domains/media_buy/update_media_buy_request.py +++ b/src/adcp/types/domains/media_buy/update_media_buy_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/update_media_buy_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import account_ref, bidding_policy @@ -46,6 +46,7 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference, @@ -62,6 +63,7 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='\\S', ), + NoExplicitNull, ] = None revision: Annotated[ SchemaInt | None, @@ -69,16 +71,19 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description="Expected current revision for optimistic concurrency. Optional for backward compatibility. When provided, sellers MUST reject the update with CONFLICT if the media buy's current revision does not match, and MUST enforce that comparison atomically with the write. Obtain from get_media_buys or the most recent create/update response.", ge=1, ), + NoExplicitNull, ] = None paused: Annotated[ StrictBool | None, Field(description='Pause/resume the entire media buy (true = paused, false = active)'), + NoExplicitNull, ] = None canceled: Annotated[ Literal[True] | None, Field( description='Cancel the entire media buy. Cancellation is irreversible — canceled media buys cannot be reactivated. Sellers MAY reject with NOT_CANCELLABLE if the media buy cannot be canceled in its current state.' ), + NoExplicitNull, ] = None cancellation_reason: Annotated[ str | None, @@ -86,16 +91,20 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='Reason for cancellation. Sellers SHOULD store this and return it in subsequent get_media_buys responses.', max_length=500, ), + NoExplicitNull, ] = None - start_time: start_timing.StartTiming | None = None + start_time: Annotated[start_timing.StartTiming | None, NoExplicitNull] = None end_time: Annotated[ - AwareDatetime | None, Field(description='New end date/time in ISO 8601 format') + AwareDatetime | None, + Field(description='New end date/time in ISO 8601 format'), + NoExplicitNull, ] = None total_budget: Annotated[ TotalBudget | None, Field( description='Updated hard aggregate lifetime budget. currency MUST equal the existing media-buy currency; an update does not redenominate a buy. When supplied alone (without packages or new_packages), in fixed mode the seller MUST atomically scale every active package budget in proportion to its current committed budget, rejecting the entire request if any derived budget cannot be accepted. When supplied with packages or new_packages, the amount MUST equal the resulting fixed-mode package sum; the seller applies the explicit package mutations and rejects with VALIDATION_ERROR if the total is inconsistent. In seller-optimized mode this changes the shared pool without converting package caps into allocations. Already-spent amounts still count against the new total.' ), + NoExplicitNull, ] = None daily_budget_cap: Annotated[ StrictFloat | None, @@ -122,12 +131,14 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Updated allocation configuration. Switching between fixed and seller-optimized modes is allowed only when update_budget_allocation is advertised in available_actions and the resulting package constraints are valid. A resulting seller_optimized allocation requires advertised media_buy.features.seller_optimized_budget, and any package budget caps, min_spend_target values, or package pacing it retains require their own advertised sub-capability; otherwise the update is rejected with UNSUPPORTED_FEATURE before any over-subscription validation.' ), + NoExplicitNull, ] = None pacing: Annotated[ pacing_1.Pacing | None, Field( description='Updated aggregate media-buy pacing. Package pacing remains subordinate to this aggregate strategy. When the resulting buy is seller-optimized, a seller declaring media_buy.features.seller_optimized_budget MUST accept `even`; it MAY reject `asap` or `front_loaded` with UNSUPPORTED_FEATURE (error.field `pacing`) before any provider mutation, including a switch to seller-optimized allocation that would retain such pacing, and MUST NOT silently coerce them to `even`. Fixed-allocation semantics are unchanged.' ), + NoExplicitNull, ] = None bidding: Annotated[ bidding_policy.BiddingPolicy | None, @@ -138,12 +149,14 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): packages: Annotated[ Sequence[package_update.PackageUpdate] | None, Field(description='Package-specific updates for existing packages', min_length=1), + NoExplicitNull, ] = None invoice_recipient: Annotated[ business_entity.BusinessEntity | None, Field( description="Update who receives the invoice for this buy. When provided, the seller invoices this entity instead of the account's default billing_entity. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request." ), + NoExplicitNull, ] = None new_packages: Annotated[ list[package_request.PackageRequest] | None, @@ -151,18 +164,21 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): description='New packages to add to this media buy. Uses the same schema as create_media_buy packages. When budget_allocation is omitted or fixed, every new package MUST carry budget and MUST NOT carry min_spend_target. To add a package without a hard cap to an existing seller-optimized buy, include its resulting seller_optimized budget_allocation block in the update so the allocation context is schema-visible. Repeating an unchanged allocation block does not itself switch modes. Sellers that support mid-flight package additions advertise `add_packages` in both `valid_actions[]` (deprecated) and as an entry in `available_actions[]` (authoritative). Sellers that do not support this MUST reject with ACTION_NOT_ALLOWED (preferred) or UNSUPPORTED_FEATURE (legacy). If the buy has a root frequency_cap, every added product must support that exact aggregate cap; otherwise the seller rejects atomically with UNSUPPORTED_FEATURE, leaving the whole update unapplied and preserving the buy and its counter history.', min_length=1, ), + NoExplicitNull, ] = None reporting_webhook: Annotated[ reporting_webhook_1.ReportingWebhook | None, Field( description='Optional webhook configuration for automated reporting delivery. Updates the reporting configuration for this media buy.' ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time. This is separate from reporting_webhook which configures ongoing campaign reporting.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -173,5 +189,5 @@ class UpdateMediaBuyRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/media_buy/update_media_buy_response.py b/src/adcp/types/domains/media_buy/update_media_buy_response.py index 61b6eb3b8..abc2f3484 100644 --- a/src/adcp/types/domains/media_buy/update_media_buy_response.py +++ b/src/adcp/types/domains/media_buy/update_media_buy_response.py @@ -85,7 +85,7 @@ class UpdateMediaBuyResponse2(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelop class UpdateMediaBuyResponse3(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): - model_config = ConfigDict(extra='allow', validate_default=True) + model_config = ConfigDict(extra='allow') status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted task_id: str message: Annotated[str, StringConstraints(max_length=2000)] | None = None diff --git a/src/adcp/types/domains/pricing_options/cpa_option.py b/src/adcp/types/domains/pricing_options/cpa_option.py index 5b62dca51..d98c8dbfd 100644 --- a/src/adcp/types/domains/pricing_options/cpa_option.py +++ b/src/adcp/types/domains/pricing_options/cpa_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpa_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -35,12 +35,14 @@ class CpaPricingOption(AdCPBaseModel): Field( description="Name of the custom event when event_type is 'custom'. Required when event_type is 'custom', ignored otherwise." ), + NoExplicitNull, ] = None event_source_id: Annotated[ str | None, Field( description='When present, only events from this specific event source count toward billing. Allows different CPA rates for different sources (e.g., online vs in-store purchases). Must match an event source configured via sync_event_sources.' ), + NoExplicitNull, ] = None currency: Annotated[ str, @@ -60,16 +62,19 @@ class CpaPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/cpc_option.py b/src/adcp/types/domains/pricing_options/cpc_option.py index 6ca810aa5..31ca2ef3b 100644 --- a/src/adcp/types/domains/pricing_options/cpc_option.py +++ b/src/adcp/types/domains/pricing_options/cpc_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpc_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..enums import adjustment_kind @@ -36,6 +36,7 @@ class CpcPricingOption(AdCPBaseModel): description='Fixed price per click. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -43,6 +44,7 @@ class CpcPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictBool | None, @@ -50,10 +52,12 @@ class CpcPricingOption(AdCPBaseModel): deprecated=True, description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.', ), + NoExplicitNull, ] = False price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None min_spend_per_package: Annotated[ StrictFloat | None, @@ -61,16 +65,19 @@ class CpcPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/cpcv_option.py b/src/adcp/types/domains/pricing_options/cpcv_option.py index 744c699cb..dc4418cec 100644 --- a/src/adcp/types/domains/pricing_options/cpcv_option.py +++ b/src/adcp/types/domains/pricing_options/cpcv_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpcv_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..enums import adjustment_kind @@ -38,6 +38,7 @@ class CpcvPricingOption(AdCPBaseModel): description='Fixed price per completed view. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -45,6 +46,7 @@ class CpcvPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictBool | None, @@ -52,10 +54,12 @@ class CpcvPricingOption(AdCPBaseModel): deprecated=True, description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.', ), + NoExplicitNull, ] = False price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None min_spend_per_package: Annotated[ StrictFloat | None, @@ -63,16 +67,19 @@ class CpcvPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/cpm_option.py b/src/adcp/types/domains/pricing_options/cpm_option.py index 63c207f99..cf7296f88 100644 --- a/src/adcp/types/domains/pricing_options/cpm_option.py +++ b/src/adcp/types/domains/pricing_options/cpm_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpm_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..enums import adjustment_kind @@ -36,6 +36,7 @@ class CpmPricingOption(AdCPBaseModel): description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -43,6 +44,7 @@ class CpmPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictBool | None, @@ -50,10 +52,12 @@ class CpmPricingOption(AdCPBaseModel): deprecated=True, description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.', ), + NoExplicitNull, ] = False price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None min_spend_per_package: Annotated[ StrictFloat | None, @@ -61,16 +65,19 @@ class CpmPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/cpp_option.py b/src/adcp/types/domains/pricing_options/cpp_option.py index 53e5c30de..44e0619d0 100644 --- a/src/adcp/types/domains/pricing_options/cpp_option.py +++ b/src/adcp/types/domains/pricing_options/cpp_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpp_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -24,6 +24,7 @@ class Parameters(AdCPBaseModel): Field( description='Measurement system for the demographic field. Defaults to nielsen when omitted.' ), + NoExplicitNull, ] = None demographic: Annotated[ str, @@ -32,7 +33,7 @@ class Parameters(AdCPBaseModel): ), ] min_points: Annotated[ - StrictFloat | None, Field(description='Minimum GRPs/TRPs required', ge=0.0) + StrictFloat | None, Field(description='Minimum GRPs/TRPs required', ge=0.0), NoExplicitNull ] = None @@ -58,6 +59,7 @@ class CppPricingOption(AdCPBaseModel): description='Fixed price per rating point. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -65,10 +67,12 @@ class CppPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None parameters: Annotated[ Parameters, Field(description='CPP-specific parameters for demographic targeting') @@ -79,16 +83,19 @@ class CppPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/cpv_option.py b/src/adcp/types/domains/pricing_options/cpv_option.py index 138e39a4c..5ff4604c6 100644 --- a/src/adcp/types/domains/pricing_options/cpv_option.py +++ b/src/adcp/types/domains/pricing_options/cpv_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/cpv_option.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarFloat from pydantic import ConfigDict, Field, StrictBool, StrictFloat @@ -59,6 +59,7 @@ class CpvPricingOption(AdCPBaseModel): description='Fixed price per view. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -66,6 +67,7 @@ class CpvPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictBool | None, @@ -73,10 +75,12 @@ class CpvPricingOption(AdCPBaseModel): deprecated=True, description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.', ), + NoExplicitNull, ] = False price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None parameters: Annotated[ Parameters, Field(description='CPV-specific parameters defining the view threshold') @@ -87,16 +91,19 @@ class CpvPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/flat_rate_option.py b/src/adcp/types/domains/pricing_options/flat_rate_option.py index bf19ed369..1d36d7d1c 100644 --- a/src/adcp/types/domains/pricing_options/flat_rate_option.py +++ b/src/adcp/types/domains/pricing_options/flat_rate_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/flat_rate_option.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -28,6 +28,7 @@ class Parameters(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None slot_span: Annotated[ SchemaInt | None, @@ -35,6 +36,7 @@ class Parameters(AdCPBaseModel): description='Number of consecutive loop slots reserved by this seller-declared pre-packaged offer. Contiguity is guaranteed; position is not guaranteed unless loop_position is also present. This field does not carry a buyer-requested custom span for an offer the seller has not pre-packaged.', ge=1, ), + NoExplicitNull, ] = None loop_position: Annotated[ str | None, @@ -42,6 +44,7 @@ class Parameters(AdCPBaseModel): description='Seller-defined position label for the reserved span within the loop, such as any, first, last, or adjacent_to_content_break. This is deliberately an open string because network vocabularies differ. Consumers MUST accept unrecognized values as opaque and treat them as conveying no position guarantee the consumer understands; an unrecognized value is not a protocol error.', min_length=1, ), + NoExplicitNull, ] = None loop_duration_seconds: Annotated[ SchemaInt | None, @@ -50,15 +53,19 @@ class Parameters(AdCPBaseModel): description='Deprecated compatibility copy of the placement-level dooh_placement_attributes.loop_duration_seconds, which is the canonical source. Retained for backward compatibility; new integrations read the placement-level field. When both are present they MUST agree. A product that offers different loop durations under different prices MUST expose distinct placements or products rather than vary this compatibility copy by pricing option.', ge=1, ), + NoExplicitNull, ] = None min_plays_per_hour: Annotated[ - SchemaInt | None, Field(description='Minimum number of plays per hour guaranteed', ge=1) + SchemaInt | None, + Field(description='Minimum number of plays per hour guaranteed', ge=1), + NoExplicitNull, ] = None venue_package: Annotated[ str | None, Field( description='Named collection of DOOH venues or installed surfaces included in this buy' ), + NoExplicitNull, ] = None duration_hours: Annotated[ StrictFloat | None, @@ -66,10 +73,12 @@ class Parameters(AdCPBaseModel): description='Duration of the DOOH slot in hours (e.g., 24 for a full-day takeover)', ge=0.0, ), + NoExplicitNull, ] = None daypart: Annotated[ str | None, Field(description='Named daypart for this slot (e.g., morning_commute, evening_rush)'), + NoExplicitNull, ] = None estimated_impressions: Annotated[ SchemaInt | None, @@ -77,6 +86,7 @@ class Parameters(AdCPBaseModel): description='Estimated audience impressions for this slot (informational, not a delivery guarantee)', ge=0, ), + NoExplicitNull, ] = None @@ -104,6 +114,7 @@ class FlatRatePricingOption(AdCPBaseModel): description='Flat rate cost. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -111,10 +122,12 @@ class FlatRatePricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None parameters: Annotated[ Parameters | None, @@ -122,6 +135,7 @@ class FlatRatePricingOption(AdCPBaseModel): description='DOOH inventory allocation parameters. Sponsorship and takeover flat_rate options omit this field entirely — only include for digital out-of-home inventory.', title='DoohParameters', ), + NoExplicitNull, ] = None min_spend_per_package: Annotated[ StrictFloat | None, @@ -129,16 +143,19 @@ class FlatRatePricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/price_breakdown.py b/src/adcp/types/domains/pricing_options/price_breakdown.py index 88bfc3165..af38807cb 100644 --- a/src/adcp/types/domains/pricing_options/price_breakdown.py +++ b/src/adcp/types/domains/pricing_options/price_breakdown.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/price_breakdown.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -43,6 +43,7 @@ class Adjustment(AdCPBaseModel): gt=0.0, lt=1.0, ), + NoExplicitNull, ] = None amount: Annotated[ StrictFloat | None, @@ -50,6 +51,7 @@ class Adjustment(AdCPBaseModel): description="Adjustment as a fixed monetary amount in the pricing option's currency. Always positive — kind determines the economic effect. Mutually exclusive with rate.", gt=0.0, ), + NoExplicitNull, ] = None description: Annotated[ str | None, @@ -57,6 +59,7 @@ class Adjustment(AdCPBaseModel): description="Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", max_length=256, ), + NoExplicitNull, ] = None beneficiary: Annotated[ str | None, @@ -64,6 +67,7 @@ class Adjustment(AdCPBaseModel): description="Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", max_length=256, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/price_guidance.py b/src/adcp/types/domains/pricing_options/price_guidance.py index 8b96a35d9..151f03a2b 100644 --- a/src/adcp/types/domains/pricing_options/price_guidance.py +++ b/src/adcp/types/domains/pricing_options/price_guidance.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/price_guidance.json -# timestamp: 2026-10-03T15:09:44+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -15,14 +15,22 @@ class PriceGuidance(AdCPBaseModel): extra='allow', ) p25: Annotated[ - StrictFloat | None, Field(description='25th percentile of recent winning bids', ge=0.0) + StrictFloat | None, + Field(description='25th percentile of recent winning bids', ge=0.0), + NoExplicitNull, ] = None p50: Annotated[ - StrictFloat | None, Field(description='Median of recent winning bids', ge=0.0) + StrictFloat | None, + Field(description='Median of recent winning bids', ge=0.0), + NoExplicitNull, ] = None p75: Annotated[ - StrictFloat | None, Field(description='75th percentile of recent winning bids', ge=0.0) + StrictFloat | None, + Field(description='75th percentile of recent winning bids', ge=0.0), + NoExplicitNull, ] = None p90: Annotated[ - StrictFloat | None, Field(description='90th percentile of recent winning bids', ge=0.0) + StrictFloat | None, + Field(description='90th percentile of recent winning bids', ge=0.0), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/revenue_share_option.py b/src/adcp/types/domains/pricing_options/revenue_share_option.py index c9efe72d4..ee114325c 100644 --- a/src/adcp/types/domains/pricing_options/revenue_share_option.py +++ b/src/adcp/types/domains/pricing_options/revenue_share_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/revenue_share_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..enums import event_type as event_type_1 @@ -38,6 +38,7 @@ class RevenueSharePricingOption(AdCPBaseModel): description='Name of the custom event when event_type is custom. Required for custom events and absent otherwise.', min_length=1, ), + NoExplicitNull, ] = None event_source_id: Annotated[ str, diff --git a/src/adcp/types/domains/pricing_options/time_option.py b/src/adcp/types/domains/pricing_options/time_option.py index c8d6f7b26..e08c60594 100644 --- a/src/adcp/types/domains/pricing_options/time_option.py +++ b/src/adcp/types/domains/pricing_options/time_option.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: pricing_options/time_option.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..enums import adjustment_kind @@ -33,7 +33,9 @@ class Parameters(AdCPBaseModel): ), ] min_duration: Annotated[ - SchemaInt | None, Field(description='Minimum booking duration in time_units', ge=1) + SchemaInt | None, + Field(description='Minimum booking duration in time_units', ge=1), + NoExplicitNull, ] = None max_duration: Annotated[ SchemaInt | None, @@ -41,6 +43,7 @@ class Parameters(AdCPBaseModel): description='Maximum booking duration in time_units. Must be >= min_duration when both are present.', ge=1, ), + NoExplicitNull, ] = None @@ -69,6 +72,7 @@ class TimeBasedPricingOption(AdCPBaseModel): description='Cost per time unit. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -76,10 +80,12 @@ class TimeBasedPricingOption(AdCPBaseModel): description='Minimum acceptable bid per time unit for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None parameters: Annotated[Parameters, Field(description='Time-based pricing parameters')] min_spend_per_package: Annotated[ @@ -88,16 +94,19 @@ class TimeBasedPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/pricing_options/vcpm_option.py b/src/adcp/types/domains/pricing_options/vcpm_option.py index eaa2a6992..42c98ce22 100644 --- a/src/adcp/types/domains/pricing_options/vcpm_option.py +++ b/src/adcp/types/domains/pricing_options/vcpm_option.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: pricing_options/vcpm_option.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool, StrictFloat from ..enums import adjustment_kind @@ -38,6 +38,7 @@ class VcpmPricingOption(AdCPBaseModel): description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.', ge=0.0, ), + NoExplicitNull, ] = None floor_price: Annotated[ StrictFloat | None, @@ -45,6 +46,7 @@ class VcpmPricingOption(AdCPBaseModel): description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.', ge=0.0, ), + NoExplicitNull, ] = None max_bid: Annotated[ StrictBool | None, @@ -52,10 +54,12 @@ class VcpmPricingOption(AdCPBaseModel): deprecated=True, description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.', ), + NoExplicitNull, ] = False price_guidance: Annotated[ price_guidance_1.PriceGuidance | None, Field(description='Optional pricing guidance for auction-based bidding'), + NoExplicitNull, ] = None min_spend_per_package: Annotated[ StrictFloat | None, @@ -63,16 +67,19 @@ class VcpmPricingOption(AdCPBaseModel): description='Minimum spend requirement per package using this pricing option, in the specified currency', ge=0.0, ), + NoExplicitNull, ] = None price_breakdown: Annotated[ price_breakdown_1.PriceBreakdown | None, Field( description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.' ), + NoExplicitNull, ] = None eligible_adjustments: Annotated[ list[adjustment_kind.PriceAdjustmentKind] | None, Field( description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/property/authorization_result.py b/src/adcp/types/domains/property/authorization_result.py index 91ca344ee..6f17f6337 100644 --- a/src/adcp/types/domains/property/authorization_result.py +++ b/src/adcp/types/domains/property/authorization_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: property/authorization_result.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field @@ -36,14 +36,17 @@ class AuthorizationResult(AdCPBaseModel): ), ] publisher_domain: Annotated[ - str | None, Field(description='The publisher domain where adagents.json was checked') + str | None, + Field(description='The publisher domain where adagents.json was checked'), + NoExplicitNull, ] = None sales_agent_url: Annotated[ - AnyUrl | None, Field(description='The sales agent URL that was validated') + AnyUrl | None, Field(description='The sales agent URL that was validated'), NoExplicitNull ] = None violation: Annotated[ Violation | None, Field( description='Details about the authorization failure (only present for unauthorized status)' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/property/create_property_list_request.py b/src/adcp/types/domains/property/create_property_list_request.py index ab99feb5a..61a07cb6c 100644 --- a/src/adcp/types/domains/property/create_property_list_request.py +++ b/src/adcp/types/domains/property/create_property_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/create_property_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref, brand_ref @@ -26,27 +27,31 @@ class CreatePropertyListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that will own the list. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. When omitted, this task applies its task-local single-account shortcut: if exactly one account is accessible to the authenticated caller, the seller may assign the list to that account; otherwise it MUST return an account-required or ambiguous-account error. Omission MUST NOT mean an undocumented credential-local default account.' ), + NoExplicitNull, ] = None name: Annotated[str, Field(description='Human-readable name for the list')] - description: Annotated[str | None, Field(description="Description of the list's purpose")] = ( - None - ) + description: Annotated[ + str | None, Field(description="Description of the list's purpose"), NoExplicitNull + ] = None base_properties: Annotated[ list[base_property_source.BasePropertySource] | None, Field( description="Array of property sources to evaluate. Each entry is a discriminated union: publisher_tags (publisher_domain + tags), publisher_ids (publisher_domain + property_ids), or identifiers (direct identifiers). If omitted, queries the agent's entire property database.", min_length=1, ), + NoExplicitNull, ] = None filters: Annotated[ property_list_filters.PropertyListFilters | None, Field(description='Dynamic filters to apply when resolving the list'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference. When provided, the agent automatically applies appropriate rules based on brand characteristics (industry, target_audience, etc.). Resolved at execution time.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -57,5 +62,5 @@ class CreatePropertyListRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/create_property_list_response.py b/src/adcp/types/domains/property/create_property_list_response.py index 79517ce84..4881f6ecf 100644 --- a/src/adcp/types/domains/property/create_property_list_response.py +++ b/src/adcp/types/domains/property/create_property_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/create_property_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -33,6 +34,7 @@ class CreatePropertyListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/delete_property_list_request.py b/src/adcp/types/domains/property/delete_property_list_request.py index fc8d246cf..9ccb59065 100644 --- a/src/adcp/types/domains/property/delete_property_list_request.py +++ b/src/adcp/types/domains/property/delete_property_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/delete_property_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -26,9 +27,10 @@ class DeletePropertyListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/property/delete_property_list_response.py b/src/adcp/types/domains/property/delete_property_list_response.py index 922e36f1c..456e2b969 100644 --- a/src/adcp/types/domains/property/delete_property_list_response.py +++ b/src/adcp/types/domains/property/delete_property_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/delete_property_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -27,6 +28,7 @@ class DeletePropertyListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/delivery_record.py b/src/adcp/types/domains/property/delivery_record.py index d09d59e17..05ba8887e 100644 --- a/src/adcp/types/domains/property/delivery_record.py +++ b/src/adcp/types/domains/property/delivery_record.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/delivery_record.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, ConfigDict, Field from ..core import ext as ext_1 @@ -29,11 +29,13 @@ class DeliveryRecord(AdCPBaseModel): Field( description='Optional client-provided ID for correlating results back to source data' ), + NoExplicitNull, ] = None sales_agent_url: Annotated[ AnyUrl | None, Field( description="URL of the sales agent that sold this inventory. If provided, authorization is validated against the property's adagents.json." ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/get_property_list_request.py b/src/adcp/types/domains/property/get_property_list_request.py index 3d32408f1..01fa88182 100644 --- a/src/adcp/types/domains/property/get_property_list_request.py +++ b/src/adcp/types/domains/property/get_property_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/get_property_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -24,10 +24,12 @@ class Pagination(AdCPBaseModel): max_results: Annotated[ SchemaInt | None, Field(description='Maximum number of identifiers to return per page', ge=1, le=10000), + NoExplicitNull, ] = 1000 cursor: Annotated[ str | None, Field(description='Opaque cursor from a previous response to fetch the next page'), + NoExplicitNull, ] = None @@ -41,18 +43,21 @@ class GetPropertyListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts and the list_id is not globally unique within that scope; optional otherwise.' ), + NoExplicitNull, ] = None resolve: Annotated[ StrictBool | None, Field( description='Whether to apply filters and return resolved identifiers (default: true)' ), + NoExplicitNull, ] = True pagination: Annotated[ Pagination | None, Field( description='Pagination parameters. Uses higher limits than standard pagination because property lists can contain tens of thousands of identifiers.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/get_property_list_response.py b/src/adcp/types/domains/property/get_property_list_response.py index 95d601ef9..be376c68d 100644 --- a/src/adcp/types/domains/property/get_property_list_response.py +++ b/src/adcp/types/domains/property/get_property_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/get_property_list_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import context as context_1 @@ -34,22 +35,25 @@ class GetPropertyListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelop Field( description='Resolved identifiers that passed filters (if resolve=true). Cache these locally for real-time use.' ), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None resolved_at: Annotated[ - AwareDatetime | None, Field(description='When the list was resolved') + AwareDatetime | None, Field(description='When the list was resolved'), NoExplicitNull ] = None cache_valid_until: Annotated[ AwareDatetime | None, Field( description='Cache expiration timestamp. Re-fetch the list after this time to get updated identifiers.' ), + NoExplicitNull, ] = None coverage_gaps: Annotated[ dict[str, _list[identifier.Identifier]] | None, Field( description="Properties included in the list despite missing feature data. Only present when a feature_requirement has if_not_covered='include'. Maps feature_id to list of identifiers not covered for that feature." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/list_property_lists_request.py b/src/adcp/types/domains/property/list_property_lists_request.py index dedb9247c..5ca7cdc51 100644 --- a/src/adcp/types/domains/property/list_property_lists_request.py +++ b/src/adcp/types/domains/property/list_property_lists_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/list_property_lists_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -26,10 +27,13 @@ class ListPropertyListsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Filter to lists owned by this account. When omitted, returns lists across all accounts accessible to the authenticated agent.' ), + NoExplicitNull, ] = None name_contains: Annotated[ - str | None, Field(description='Filter to lists whose name contains this string') + str | None, + Field(description='Filter to lists whose name contains this string'), + NoExplicitNull, ] = None - pagination: pagination_request.PaginationRequest | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/list_property_lists_response.py b/src/adcp/types/domains/property/list_property_lists_response.py index 0fc65146d..5be3ebe63 100644 --- a/src/adcp/types/domains/property/list_property_lists_response.py +++ b/src/adcp/types/domains/property/list_property_lists_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/list_property_lists_response.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -26,6 +27,6 @@ class ListPropertyListsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvel list[property_list.PropertyList], Field(description='Array of property lists (metadata only, not resolved properties)'), ] - pagination: pagination_response.PaginationResponse | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/property_error.py b/src/adcp/types/domains/property/property_error.py index d71392538..98d155a3c 100644 --- a/src/adcp/types/domains/property/property_error.py +++ b/src/adcp/types/domains/property/property_error.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: property/property_error.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import property as property_1 @@ -28,6 +28,8 @@ class PropertyError(AdCPBaseModel): ) code: Annotated[Code, Field(description='Error code')] property: Annotated[ - property_1.Property | None, Field(description='The property that caused the error') + property_1.Property | None, + Field(description='The property that caused the error'), + NoExplicitNull, ] = None message: Annotated[str, Field(description='Human-readable error message')] diff --git a/src/adcp/types/domains/property/property_feature.py b/src/adcp/types/domains/property/property_feature.py index 4d7a1cbef..ca2b2a042 100644 --- a/src/adcp/types/domains/property/property_feature.py +++ b/src/adcp/types/domains/property/property_feature.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/property_feature.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -19,4 +19,5 @@ class PropertyFeature(AdCPBaseModel): source: Annotated[ str | None, Field(description='Source of the feature data (e.g., app_store_privacy_label, tcf_string)'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/property/property_feature_definition.py b/src/adcp/types/domains/property/property_feature_definition.py index 56cbfd1fd..d97ab887e 100644 --- a/src/adcp/types/domains/property/property_feature_definition.py +++ b/src/adcp/types/domains/property/property_feature_definition.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: property/property_feature_definition.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field, StrictFloat from ..core import ext as ext_1 @@ -32,10 +32,14 @@ class Coverage(AdCPBaseModel): extra='forbid', ) property_types: Annotated[ - list[str] | None, Field(description='Property types this feature applies to') + list[str] | None, + Field(description='Property types this feature applies to'), + NoExplicitNull, ] = None countries: Annotated[ - list[str] | None, Field(description='Countries where this feature is available') + list[str] | None, + Field(description='Countries where this feature is available'), + NoExplicitNull, ] = None @@ -51,7 +55,9 @@ class PropertyFeatureDefinition(AdCPBaseModel): ] name: Annotated[str, Field(description='Human-readable name for the feature')] description: Annotated[ - str | None, Field(description='Description of what this feature measures or represents') + str | None, + Field(description='Description of what this feature measures or represents'), + NoExplicitNull, ] = None type: Annotated[ Type, @@ -60,13 +66,19 @@ class PropertyFeatureDefinition(AdCPBaseModel): ), ] range: Annotated[ - Range | None, Field(description='For quantitative features, the valid range of values') + Range | None, + Field(description='For quantitative features, the valid range of values'), + NoExplicitNull, ] = None allowed_values: Annotated[ - list[str] | None, Field(description='For categorical features, the set of valid values') + list[str] | None, + Field(description='For categorical features, the set of valid values'), + NoExplicitNull, ] = None coverage: Annotated[ - Coverage | None, Field(description='What this feature covers (empty arrays = all)') + Coverage | None, + Field(description='What this feature covers (empty arrays = all)'), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl, @@ -75,6 +87,8 @@ class PropertyFeatureDefinition(AdCPBaseModel): ), ] methodology_version: Annotated[ - str | None, Field(description='Version identifier for the methodology (for audit trails)') + str | None, + Field(description='Version identifier for the methodology (for audit trails)'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/property_feature_result.py b/src/adcp/types/domains/property/property_feature_result.py index 2fe9a72f7..e17b9b6d2 100644 --- a/src/adcp/types/domains/property/property_feature_result.py +++ b/src/adcp/types/domains/property/property_feature_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: property/property_feature_result.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -31,6 +31,7 @@ class PropertyFeatureResult(AdCPBaseModel): features: Annotated[ dict[str, property_feature_value.PropertyFeatureValue] | None, Field(description='Map of feature_id to feature value'), + NoExplicitNull, ] = None coverage_status: Annotated[ CoverageStatus, @@ -41,5 +42,6 @@ class PropertyFeatureResult(AdCPBaseModel): last_evaluated: Annotated[ AwareDatetime | None, Field(description='When features were last evaluated for this property'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/property_feature_value.py b/src/adcp/types/domains/property/property_feature_value.py index 0b2e7b40b..9c48e4841 100644 --- a/src/adcp/types/domains/property/property_feature_value.py +++ b/src/adcp/types/domains/property/property_feature_value.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/property_feature_value.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import ext as ext_1 @@ -27,25 +27,33 @@ class PropertyFeatureValue(AdCPBaseModel): Field( description="Unit of measurement for quantitative values (e.g., 'gCO2e/1000_impressions', 'percentage')" ), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, Field(description='Confidence score for this value (0-1)', ge=0.0, le=1.0), + NoExplicitNull, ] = None measured_at: Annotated[ - AwareDatetime | None, Field(description='When this specific value was measured') + AwareDatetime | None, + Field(description='When this specific value was measured'), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='When this certification/value expires (for time-limited certifications)' ), + NoExplicitNull, ] = None methodology_version: Annotated[ - str | None, Field(description='Version of the methodology used to calculate this value') + str | None, + Field(description='Version of the methodology used to calculate this value'), + NoExplicitNull, ] = None details: Annotated[ dict[str, Any] | None, Field(description='Additional vendor-specific details about this measurement'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/property_list.py b/src/adcp/types/domains/property/property_list.py index d68a10c20..8be313aa0 100644 --- a/src/adcp/types/domains/property/property_list.py +++ b/src/adcp/types/domains/property/property_list.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/property_list.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field from ..core import account_ref, brand_ref, vendor_pricing_option @@ -19,34 +19,39 @@ class PropertyList(AdCPBaseModel): ) list_id: Annotated[str, Field(description='Unique identifier for this property list')] name: Annotated[str, Field(description='Human-readable name for the list')] - description: Annotated[str | None, Field(description="Description of the list's purpose")] = ( - None - ) + description: Annotated[ + str | None, Field(description="Description of the list's purpose"), NoExplicitNull + ] = None account: Annotated[ account_ref.AccountReference | None, Field( description='Account that owns this list. Returned as account_id form (seller-assigned identifier).' ), + NoExplicitNull, ] = None base_properties: Annotated[ list[base_property_source.BasePropertySource] | None, Field( description="Array of property sources to evaluate. Each entry is a discriminated union: publisher_tags (publisher_domain + tags), publisher_ids (publisher_domain + property_ids), or identifiers (direct identifiers). If omitted, queries the agent's entire property database." ), + NoExplicitNull, ] = None filters: Annotated[ property_list_filters.PropertyListFilters | None, Field(description='Dynamic filters applied when resolving the list'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand reference used to automatically apply appropriate rules. Resolved to full brand identity at execution time.' ), + NoExplicitNull, ] = None webhook_url: Annotated[ AnyUrl | None, Field(description='URL to receive notifications when the resolved list changes'), + NoExplicitNull, ] = None cache_duration_hours: Annotated[ SchemaInt | None, @@ -54,16 +59,18 @@ class PropertyList(AdCPBaseModel): description='Recommended cache duration for resolved list. Consumers should re-fetch after this period.', ge=1, ), + NoExplicitNull, ] = 24 - created_at: Annotated[AwareDatetime | None, Field(description='When the list was created')] = ( - None - ) + created_at: Annotated[ + AwareDatetime | None, Field(description='When the list was created'), NoExplicitNull + ] = None updated_at: Annotated[ - AwareDatetime | None, Field(description='When the list was last modified') + AwareDatetime | None, Field(description='When the list was last modified'), NoExplicitNull ] = None property_count: Annotated[ SchemaInt | None, Field(description='Number of properties in the resolved list (at time of last resolution)'), + NoExplicitNull, ] = None pricing_options: Annotated[ list[vendor_pricing_option.VendorPricingOption] | None, @@ -71,4 +78,5 @@ class PropertyList(AdCPBaseModel): description='Pricing options for this property list. Present when the requesting account has a billing relationship with the list provider. The buyer passes the selected pricing_option_id in report_usage.', min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/property/property_list_changed_webhook.py b/src/adcp/types/domains/property/property_list_changed_webhook.py index 307f85d72..4392f3bfd 100644 --- a/src/adcp/types/domains/property/property_list_changed_webhook.py +++ b/src/adcp/types/domains/property/property_list_changed_webhook.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/property_list_changed_webhook.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -17,13 +17,17 @@ class ChangeSummary(AdCPBaseModel): extra='forbid', ) properties_added: Annotated[ - SchemaInt | None, Field(description='Number of properties added since last resolution') + SchemaInt | None, + Field(description='Number of properties added since last resolution'), + NoExplicitNull, ] = None properties_removed: Annotated[ - SchemaInt | None, Field(description='Number of properties removed since last resolution') + SchemaInt | None, + Field(description='Number of properties removed since last resolution'), + NoExplicitNull, ] = None total_properties: Annotated[ - SchemaInt | None, Field(description='Total properties in the resolved list') + SchemaInt | None, Field(description='Total properties in the resolved list'), NoExplicitNull ] = None @@ -42,14 +46,19 @@ class PropertyListChangedWebhook(AdCPBaseModel): ] event: Annotated[Literal['property_list_changed'], Field(description='The event type')] = 'property_list_changed' list_id: Annotated[str, Field(description='ID of the property list that changed')] - list_name: Annotated[str | None, Field(description='Name of the property list')] = None + list_name: Annotated[ + str | None, Field(description='Name of the property list'), NoExplicitNull + ] = None change_summary: Annotated[ - ChangeSummary | None, Field(description='Summary of changes to the resolved list') + ChangeSummary | None, + Field(description='Summary of changes to the resolved list'), + NoExplicitNull, ] = None resolved_at: Annotated[AwareDatetime, Field(description='When the list was re-resolved')] cache_valid_until: Annotated[ AwareDatetime | None, Field(description='When the consumer should refresh from the governance agent'), + NoExplicitNull, ] = None signature: Annotated[ str, @@ -58,4 +67,4 @@ class PropertyListChangedWebhook(AdCPBaseModel): description='Deprecated 3.x compatibility marker. Property-list webhooks previously required this field but did not define an implementable signature algorithm, signed bytes, or key-discovery contract. RFC 9421 signatures are carried in the Signature and Signature-Input HTTP headers and bind the complete payload through Content-Digest. New 3.2 senders MUST set this field to the literal value `rfc9421`; receivers MUST accept any string from an older 3.x sender and MUST ignore it when authenticating a delivery. The field remains required through 3.x for schema compatibility and is removed in 4.0.', ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/property_list_filters.py b/src/adcp/types/domains/property/property_list_filters.py index 011cdfa59..a7114cf08 100644 --- a/src/adcp/types/domains/property/property_list_filters.py +++ b/src/adcp/types/domains/property/property_list_filters.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/property_list_filters.json -# timestamp: 2026-10-02T13:37:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field @@ -29,6 +29,7 @@ class PropertyListFilters(AdCPBaseModel): description='Property must have feature data for ALL listed countries (ISO codes). When omitted, no country restriction is applied.', min_length=1, ), + NoExplicitNull, ] = None channels_any: Annotated[ list[channels.MediaChannel] | None, @@ -36,10 +37,12 @@ class PropertyListFilters(AdCPBaseModel): description='Property must support ANY of the listed channels. When omitted, no channel restriction is applied.', min_length=1, ), + NoExplicitNull, ] = None property_types: Annotated[ list[property_type.PropertyType] | None, Field(description='Filter to these property types', min_length=1), + NoExplicitNull, ] = None feature_requirements: Annotated[ list[feature_requirement.FeatureRequirement] | None, @@ -47,8 +50,10 @@ class PropertyListFilters(AdCPBaseModel): description='Feature-based requirements. Property must pass ALL requirements (AND logic).', min_length=1, ), + NoExplicitNull, ] = None exclude_identifiers: Annotated[ list[identifier.Identifier] | None, Field(description='Identifiers to always exclude from results', min_length=1), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/property/update_property_list_request.py b/src/adcp/types/domains/property/update_property_list_request.py index ddd86196a..76d4fad1b 100644 --- a/src/adcp/types/domains/property/update_property_list_request.py +++ b/src/adcp/types/domains/property/update_property_list_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/update_property_list_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import AnyUrl, ConfigDict, Field from ..core import account_ref, brand_ref @@ -27,33 +28,38 @@ class UpdatePropertyListRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), + NoExplicitNull, ] = None - name: Annotated[str | None, Field(description='New name for the list')] = None - description: Annotated[str | None, Field(description='New description')] = None + name: Annotated[str | None, Field(description='New name for the list'), NoExplicitNull] = None + description: Annotated[str | None, Field(description='New description'), NoExplicitNull] = None base_properties: Annotated[ list[base_property_source.BasePropertySource] | None, Field( description='Complete replacement for the base properties list (not a patch). Each entry is a discriminated union: publisher_tags (publisher_domain + tags), publisher_ids (publisher_domain + property_ids), or identifiers (direct identifiers).' ), + NoExplicitNull, ] = None filters: Annotated[ property_list_filters.PropertyListFilters | None, Field(description='Complete replacement for the filters (not a patch)'), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Update brand reference. Resolved to full brand identity at execution time.' ), + NoExplicitNull, ] = None webhook_url: Annotated[ AnyUrl | None, Field( description='Update the webhook URL for list change notifications (set to empty string to remove)' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None idempotency_key: Annotated[ str, Field( diff --git a/src/adcp/types/domains/property/update_property_list_response.py b/src/adcp/types/domains/property/update_property_list_response.py index 27aca96d7..0bbeaca36 100644 --- a/src/adcp/types/domains/property/update_property_list_response.py +++ b/src/adcp/types/domains/property/update_property_list_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: property/update_property_list_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -27,6 +28,7 @@ class UpdatePropertyListResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve Field( description="Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. `replayed` appears only when the request actually resolved through the idempotency cache. Pure reads may ignore an optional `idempotency_key`; when a seller voluntarily caches keyed reads, those responses use the same replay indicator and full cache contract." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/validate_property_delivery_request.py b/src/adcp/types/domains/property/validate_property_delivery_request.py index e4afe9e45..6ddfbe1a6 100644 --- a/src/adcp/types/domains/property/validate_property_delivery_request.py +++ b/src/adcp/types/domains/property/validate_property_delivery_request.py @@ -1,11 +1,12 @@ # generated by datamodel-codegen: # filename: property/validate_property_delivery_request.json -# timestamp: 2026-10-04T00:59:59+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -25,6 +26,7 @@ class ValidatePropertyDeliveryRequest(AdcpVersionEnvelope): Field( description='Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise.' ), + NoExplicitNull, ] = None records: Annotated[ list[delivery_record.DeliveryRecord], @@ -39,6 +41,7 @@ class ValidatePropertyDeliveryRequest(AdcpVersionEnvelope): Field( description='Include compliant records in results (default: only return non_compliant, unmodeled, and unidentified)' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/validate_property_delivery_response.py b/src/adcp/types/domains/property/validate_property_delivery_response.py index ccc3fd0f0..20f912b60 100644 --- a/src/adcp/types/domains/property/validate_property_delivery_response.py +++ b/src/adcp/types/domains/property/validate_property_delivery_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: property/validate_property_delivery_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import context as context_1 @@ -60,17 +60,22 @@ class Aggregate(AdCPBaseModel): score: Annotated[ StrictFloat | None, Field(description='Numeric score (0-100 scale typical, but agent-defined)'), + NoExplicitNull, ] = None grade: Annotated[ str | None, Field(description="Letter grade or category (e.g., 'A+', 'B-', 'Gold', 'Compliant')"), + NoExplicitNull, ] = None label: Annotated[ str | None, Field(description="Human-readable summary (e.g., '85% compliant', 'High quality')"), + NoExplicitNull, ] = None methodology_url: Annotated[ - AnyUrl | None, Field(description='URL explaining how this aggregate was calculated') + AnyUrl | None, + Field(description='URL explaining how this aggregate was calculated'), + NoExplicitNull, ] = None @@ -117,6 +122,7 @@ class ValidatePropertyDeliveryResponse(AdcpVersionEnvelope, ProtocolEnvelope): Field( description='Overall compliance flag for the submitted delivery — true when every record is compliant, false when any record is non_compliant. Derived from summary.non_compliant_records === 0 but surfaced at the root as a convenience signal for buyers. Agents MAY omit this field when the response represents a partial validation (e.g., when include_compliant is false and results only contains non_compliant records); consumers SHOULD fall back to summary counts if compliant is absent.' ), + NoExplicitNull, ] = None list_id: Annotated[str, Field(description='ID of the property list validated against')] summary: Annotated[Summary, Field(description='Aggregate validation statistics')] @@ -125,12 +131,14 @@ class ValidatePropertyDeliveryResponse(AdcpVersionEnvelope, ProtocolEnvelope): Field( description='Optional aggregate measurements computed by the governance agent. Format and meaning are agent-specific.' ), + NoExplicitNull, ] = None authorization_summary: Annotated[ AuthorizationSummary | None, Field( description='Aggregate authorization statistics. Only present if any records included sales_agent_url.' ), + NoExplicitNull, ] = None results: Annotated[ list[validation_result.ValidationResult], @@ -144,6 +152,7 @@ class ValidatePropertyDeliveryResponse(AdcpVersionEnvelope, ProtocolEnvelope): list_resolved_at: Annotated[ AwareDatetime | None, Field(description='Timestamp of the property list resolution used for validation'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/property/validation_result.py b/src/adcp/types/domains/property/validation_result.py index 416fdff27..515f8a0ad 100644 --- a/src/adcp/types/domains/property/validation_result.py +++ b/src/adcp/types/domains/property/validation_result.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: property/validation_result.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import ext as ext_1 @@ -28,13 +28,15 @@ class Requirement(AdCPBaseModel): extra='forbid', ) min_value: Annotated[ - StrictFloat | None, Field(description='Minimum value that was required') + StrictFloat | None, Field(description='Minimum value that was required'), NoExplicitNull ] = None max_value: Annotated[ - StrictFloat | None, Field(description='Maximum value that was allowed') + StrictFloat | None, Field(description='Maximum value that was allowed'), NoExplicitNull ] = None allowed_values: Annotated[ - list[Any] | None, Field(description='Values that would have been acceptable') + list[Any] | None, + Field(description='Values that would have been acceptable'), + NoExplicitNull, ] = None @@ -54,15 +56,19 @@ class Feature(AdCPBaseModel): Field( description='Optional attribution — when this feature was evaluated for the purpose of a specific policy, policy_id references the authorizing PolicyEntry. Property-list agents populate when the validation was motivated by a specific policy. See /docs/governance/policy-attribution.' ), + NoExplicitNull, ] = None explanation: Annotated[ - str | None, Field(description='Directional human-readable explanation of the result.') + str | None, + Field(description='Directional human-readable explanation of the result.'), + NoExplicitNull, ] = None requirement: Annotated[ Requirement | None, Field( description='The feature requirement that was not met. MAY be present on failed features when the caller authored the requirement (e.g., feature_requirements on a property list). The buyer set these thresholds — echoing them back enables fix-and-retry loops without looking up the list definition.' ), + NoExplicitNull, ] = None confidence: Annotated[ StrictFloat | None, @@ -71,6 +77,7 @@ class Feature(AdCPBaseModel): ge=0.0, le=1.0, ), + NoExplicitNull, ] = None @@ -82,7 +89,9 @@ class ValidationResult(AdCPBaseModel): identifier_1.Identifier, Field(description='The identifier that was validated') ] record_id: Annotated[ - str | None, Field(description='Client-provided ID from the delivery record (if provided)') + str | None, + Field(description='Client-provided ID from the delivery record (if provided)'), + NoExplicitNull, ] = None status: Annotated[ Status, @@ -98,11 +107,13 @@ class ValidationResult(AdCPBaseModel): Field( description='Per-feature breakdown for this record. SHOULD include all failed and warning features. MAY include passed features. For property validation the buyer authored the requirement, so the `requirement` that was not met MAY be echoed back on failures — this is contract data, not evaluator IP.' ), + NoExplicitNull, ] = None authorization: Annotated[ authorization_result.AuthorizationResult | None, Field( description='Authorization validation result (only present if sales_agent_url was provided in the delivery record)' ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/get_adcp_capabilities_request.py b/src/adcp/types/domains/protocol/get_adcp_capabilities_request.py index dbc8b70fc..f4261d065 100644 --- a/src/adcp/types/domains/protocol/get_adcp_capabilities_request.py +++ b/src/adcp/types/domains/protocol/get_adcp_capabilities_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_adcp_capabilities_request.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -34,6 +35,7 @@ class GetAdcpCapabilitiesRequest(AdcpRequest, AdcpVersionEnvelope): description='Specific protocols to query capabilities for. If omitted, returns capabilities for all supported protocols.', min_length=1, ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/get_adcp_capabilities_response.py b/src/adcp/types/domains/protocol/get_adcp_capabilities_response.py index 884ea8890..ef884fd58 100644 --- a/src/adcp/types/domains/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/domains/protocol/get_adcp_capabilities_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_adcp_capabilities_response.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from adcp.types._str_enum import StrEnum from typing import Any, Annotated, Dict, Literal -from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey +from adcp.types.base import AdCPBaseModel, GeoPlaceSystemKey, NoExplicitNull from adcp.types._scalar import ScalarInt, ScalarStr from pydantic import ( AnyUrl, @@ -104,12 +104,14 @@ class Idempotency(AdCPBaseModel): ge=1, le=604800, ), + NoExplicitNull, ] = None account_id_is_opaque: Annotated[ StrictBool | None, Field( description="When true, the seller derives `account_id` via an HKDF-based one-way transform of the buyer's natural account key rather than echoing the natural key on the wire. Buyers MUST NOT attempt to invert the opaque id and MUST treat it as a blind handle scoped to this seller. Absent or false, callers should assume `account_id` is the natural key (or a server-assigned but non-opaque id). This flag does not change the wire shape, but it DOES change buyer behavior — buyers MUST NOT cache, log, or treat `account_id` as a natural-key analog when this flag is true. Migration note for sellers already returning an opaque id without this flag: set it to true at the next capabilities refresh so buyers stop inferring natural-key semantics; until set, new-buyer replay/retry logic will misclassify these ids as natural keys." ), + NoExplicitNull, ] = False @@ -184,6 +186,7 @@ class Notifications(AdCPBaseModel): ge=0, le=86400, ), + NoExplicitNull, ] = None @@ -210,12 +213,14 @@ class CapabilityChanges(AdCPBaseModel): max_length=255, min_length=1, ), + NoExplicitNull, ] = None last_modified: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp when the seller last changed the advertised capability document. This is the scoped replacement for legacy top-level `last_updated` when buyers need cache validation semantics.' ), + NoExplicitNull, ] = None cache_ttl_seconds: Annotated[ SchemaInt | None, @@ -224,12 +229,14 @@ class CapabilityChanges(AdCPBaseModel): ge=60, le=604800, ), + NoExplicitNull, ] = None notifications: Annotated[ Notifications | Notifications1 | None, Field( description='Whether the seller supports agent-level capability-change webhooks. When supported, interested consumers register endpoint subscribers with the declared registration_task; sync_principal is preferred when the broader connection surface is available, while sync_agent_notification_configs remains the specialized compatibility task. Each capabilities.changed fire is a small invalidation payload, and consumers repair by re-reading get_adcp_capabilities.' ), + NoExplicitNull, ] = None @@ -341,6 +348,7 @@ class GovernanceEnforcement(AdCPBaseModel): Field( description='Seller-wide advisory default for governance agents this enforcing service accepts as binding counterparties. A candidate satisfying any matcher is accepted by this declaration. The per-account sync_governance response is authoritative and may apply stricter account-specific criteria. Omission means accept any, preserving legacy behavior.' ), + NoExplicitNull, ] = None @@ -386,6 +394,7 @@ class Notifications2(AdCPBaseModel): Field( description='Whether `list_accounts` can return recent delivery attempts in `accounts[].webhook_activity[]` when the buyer requests `include_webhook_activity: true`.' ), + NoExplicitNull, ] = False @@ -479,12 +488,14 @@ class Account(AdCPBaseModel): Field( description="Whether the seller requires operator-level credentials. This declares who must authenticate; it does not by itself declare whether OAuth is used, whether list_accounts is exposed, or which sync_accounts modes are supported. When true, operators authenticate independently with the seller and account-scoped calls use seller/storefront-assigned account_id values because the seller or upstream platform owns the canonical account namespace. If a credential may access more than one account, the seller MUST expose list_accounts and buyers MUST resolve an explicit account_id before the first account-scoped request. If a credential is bound to exactly one account, the seller SHOULD expose list_accounts returning that singleton; a seller MAY omit list_accounts only when it provides the same explicit account_id through another declared path or out-of-band onboarding. When false (default, buyer-declared accounts), the seller trusts the agent's identity claims and account-scoped calls use the advertiser natural key: brand + operator + optional operator_unit, fixed currency, optional buyer-selected account timezone, and sandbox. operator_unit.id is owned by the operator and is distinct from the seller's account_id. The seller normally provisions through sync_accounts, but MAY lazily provision on the first account-scoped request when billing and other required settings are unambiguous from capabilities or onboarding defaults. A lazy-provisioning seller MUST keep accepting the natural key and MUST expose list_accounts for recovery; if buyer input is needed before use, the seller MUST expose sync_accounts." ), + NoExplicitNull, ] = False authorization_endpoint: Annotated[ AnyUrl | None, Field( description='OAuth authorization endpoint for obtaining operator-level credentials. Present when the seller supports OAuth for operator authentication. The agent directs the operator to this URL to authenticate and obtain a bearer token. If absent and require_operator_auth is true, operators obtain credentials out-of-band (e.g., seller portal, API key).' ), + NoExplicitNull, ] = None supported_billing: Annotated[ list[billing_party.BillingParty], @@ -499,48 +510,56 @@ class Account(AdCPBaseModel): description='Required for sellers implementing AdCP 3.2 advertiser-account provisioning, but optional in this shared 3.x response schema so existing 3.0 and 3.1 capability responses remain valid. Declares whether advertiser accounts are bound to one immutable currency (`fixed`), select currency independently per proposal or media buy (`per_media_buy`), or support both models. When only `fixed` is advertised, buyer-declared provisioning entries MUST include `currency`. When only `per_media_buy` is advertised, they MUST omit it. When both are advertised, presence of `currency` selects a fixed-currency account and omission selects per-media-buy currency. Buyers MUST treat absence as an older seller whose currency mode is not discoverable, not as support for either mode.', min_length=1, ), + NoExplicitNull, ] = None timezone: Annotated[ account_timezone_capability.AccountTimezoneCapability | None, Field( description='Required for sellers implementing AdCP 3.2 advertiser-account provisioning, but optional in the shared 3.x response schema for compatibility. Declares whether the account timezone is seller-wide or fixed per account and whether a buyer must select it during sync_accounts provisioning. Account timezone is the default for account-scoped calendar semantics; feature-specific capability fields explicitly declare exceptions.' ), + NoExplicitNull, ] = None required_for_products: Annotated[ StrictBool | None, Field( description='Whether an account reference is required for get_products. When true, the buyer must establish an account before browsing products. When false (default), the buyer can browse products without an account — useful for price comparison and discovery before committing to a seller. Before an account is provisioned, the buyer omits account rather than sending a natural key that does not resolve yet. See /docs/accounts/overview#account-references-before-provisioning.' ), + NoExplicitNull, ] = False account_financials: Annotated[ StrictBool | None, Field( description='Whether this seller exposes the `get_account_financials` task for querying account-level financial status (spend, credit, invoices). Acts as a **pre-call discriminator** — buyers MUST consult this field before issuing `get_account_financials`; when `false` (or absent), sellers MAY reject the call with an `UNSUPPORTED_FEATURE` / `OPERATION_NOT_SUPPORTED` error. Companion pattern to `creative.bills_through_adcp` (issue #2881) — both fields let buyers gate optional capability calls on a single declared boolean rather than probing for support. Only applicable to operator-billed accounts; sellers using buyer-billed flows omit or set to `false`.' ), + NoExplicitNull, ] = False notifications: Annotated[ Notifications2 | Notifications3 | None, Field( description='Whether the seller supports durable account-lifecycle webhooks through account-level `notification_configs[]`. This capability is specifically for account status changes such as approval, rejection, payment-required, suspension, recovery, and closure. When supported, buyers register subscribers with `sync_accounts.accounts[].notification_configs[]`; each `account.status_changed` fire is an invalidation payload, and buyers repair by re-reading `list_accounts` for the account_id.' ), + NoExplicitNull, ] = None change_feed: Annotated[ ChangeFeed | ChangeFeed1 | None, Field( description='Whether the seller exposes a durable, ordered feed of material changes to authoritative account-scoped state. This is distinct from webhook_activity transport diagnostics and from current-state reads. Sellers claiming support MUST retain changes for at least 90 days after recording and MUST produce records regardless of whether a mutation originated through AdCP, a seller surface, another authorized principal, seller automation, or a connected platform within declared coverage. Experimental in 3.2 (RFC #6810): sellers advertising supported: true MUST list account.change_feed in experimental_features.' ), + NoExplicitNull, ] = None identity_updates: Annotated[ IdentityUpdates | IdentityUpdates1 | None, Field( description='Whether the seller accepts buyer-desired operator identity reconciliation through sync_accounts settings-update entries. Sellers declaring support expose the exact identity transitions they implement, MUST return account revisions from sync_accounts and list_accounts, and MUST return identity_change_preview for dry-run identity updates.' ), + NoExplicitNull, ] = None sandbox: Annotated[ StrictBool | None, Field( description='Whether this seller supports sandbox accounts for testing. Buyer-declared accounts use sandbox: true in sync_accounts or, for an unambiguous lazy-provisioning seller, in the natural-key account reference. Sellers with account_id namespaces expose sandbox accounts as pre-existing test accounts through list_accounts or supply them out-of-band. Requests using a sandbox account perform no real platform calls or spend.' ), + NoExplicitNull, ] = False @@ -570,6 +589,7 @@ class AcceptancePolicyDiscovery(AdCPBaseModel): description='Local or registry-referenced catalog profiles that apply seller-wide unless a product adds further profiles. IDs MUST resolve uniquely across profiles and registry_profiles; all referenced profiles compose restrictively.', min_length=1, ), + NoExplicitNull, ] = None @@ -616,6 +636,7 @@ class ProposalRefinement(AdCPBaseModel): ge=2, le=10, ), + NoExplicitNull, ] = None @@ -628,6 +649,7 @@ class PerformanceFeedback(AdCPBaseModel): Field( description='Whether provide_performance_feedback responses include an honest application_status of accepted, applied, or not_applied. accepted is explicitly not an application claim. Sellers that cannot determine optimizer disposition omit this claim or set it false.' ), + NoExplicitNull, ] = False @@ -684,12 +706,14 @@ class RelationshipNotifications(AdCPBaseModel): max_length=1, min_length=1, ), + NoExplicitNull, ] = None supports_webhook_activity: Annotated[ StrictBool | None, Field( description='Whether the declared repair tasks can return recent delivery attempts for these account-level invalidations when include_webhook_activity is true.' ), + NoExplicitNull, ] = False @@ -708,19 +732,22 @@ class GeoMetros(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - nielsen_dma: StrictBool | None = None - uk_itl1: StrictBool | None = None - uk_itl2: StrictBool | None = None - eurostat_nuts2: StrictBool | None = None + nielsen_dma: Annotated[StrictBool | None, NoExplicitNull] = None + uk_itl1: Annotated[StrictBool | None, NoExplicitNull] = None + uk_itl2: Annotated[StrictBool | None, NoExplicitNull] = None + eurostat_nuts2: Annotated[StrictBool | None, NoExplicitNull] = None class AgeRestriction(AdCPBaseModel): supported: Annotated[ - StrictBool | None, Field(description='Whether seller supports age restrictions') + StrictBool | None, + Field(description='Whether seller supports age restrictions'), + NoExplicitNull, ] = None verification_methods: Annotated[ list[age_verification_method.AgeVerificationMethod] | None, Field(description='Age verification methods this seller supports'), + NoExplicitNull, ] = None @@ -795,8 +822,8 @@ class MatchingLatencyHours(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - min: Annotated[SchemaInt | None, Field(ge=0)] = None - max: Annotated[SchemaInt | None, Field(ge=0)] = None + min: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None + max: Annotated[SchemaInt | None, Field(ge=0), NoExplicitNull] = None class SupportedOptimizationMetric(StrEnum): @@ -829,6 +856,7 @@ class VendorMetricOptimization(AdCPBaseModel): description='Target kinds this seller can support for vendor_metric optimization goals on at least one product. Values match optimization_goals[].target.kind for kind: vendor_metric. A target-less vendor_metric goal maximizes the metric within budget and does not require a target-kind declaration.', min_length=1, ), + NoExplicitNull, ] = None @@ -845,6 +873,7 @@ class AttributionWindow(AdCPBaseModel): event_type: Annotated[ event_type_1.EventType | None, Field(description='Event type this window applies to, or omit for default window'), + NoExplicitNull, ] = None post_click: Annotated[ list[duration.Duration], @@ -859,6 +888,7 @@ class AttributionWindow(AdCPBaseModel): description='Available post-view attribution windows (e.g. [{"interval": 1, "unit": "days"}])', min_length=1, ), + NoExplicitNull, ] = None @@ -873,6 +903,7 @@ class ContentStandards(AdCPBaseModel): Field( description="Whether the seller runs a local evaluation model. When false, all artifacts will have local_verdict: 'unevaluated' and the failures_only filter on get_media_buy_artifacts is not useful." ), + NoExplicitNull, ] = None supported_channels: Annotated[ list[channels.MediaChannel] | None, @@ -880,12 +911,14 @@ class ContentStandards(AdCPBaseModel): description='Channels for which the seller can provide content artifacts. Helps buyers understand which parts of a mixed-channel buy will have content standards coverage.', min_length=1, ), + NoExplicitNull, ] = None supports_webhook_delivery: Annotated[ StrictBool | None, Field( description='Whether the seller supports push-based artifact delivery via artifact_webhook configured at buy creation time.' ), + NoExplicitNull, ] = None @@ -914,18 +947,21 @@ class Portfolio(AdCPBaseModel): Field( description="Complete list of AdCP media channels for which this sales agent accepts and can meaningfully answer product-discovery briefs. When present, this is an exhaustive brief-routing allowlist: buyers MAY skip the agent when a brief's requested channels do not intersect it. Omission means channel scope is unknown and MUST NOT be interpreted as support for every channel. This is a routing pre-filter, not a promise of current product availability." ), + NoExplicitNull, ] = None primary_countries: Annotated[ list[PrimaryCountry] | None, Field( description="Complete list of ISO 3166-1 alpha-2 countries for which this sales agent accepts and can meaningfully answer product-discovery briefs. When present, this is an exhaustive brief-routing allowlist: buyers MAY skip the agent when a brief's requested countries do not intersect it. Omission means country scope is unknown and MUST NOT be interpreted as global coverage. This is a routing pre-filter, not a promise of current product availability and not an executable geo-targeting declaration; media_buy.execution.targeting geo capabilities and each product's overlay_support remain authoritative for targeting execution." ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Markdown-formatted description of the inventory portfolio', max_length=5000 ), + NoExplicitNull, ] = None advertising_policies: Annotated[ str | None, @@ -933,6 +969,7 @@ class Portfolio(AdCPBaseModel): description='Advertising content policies, restrictions, and guidelines', max_length=10000, ), + NoExplicitNull, ] = None @@ -956,6 +993,7 @@ class Features(AdCPBaseModel): deprecated=True, description='DEPRECATED. Legacy wire flag for structured signal_ref references to provider-published signal definitions. New agents SHOULD omit this flag; callers MUST NOT require it before using signal_ref with the Signals protocol.', ), + NoExplicitNull, ] = None @@ -967,6 +1005,7 @@ class Signals(AdCPBaseModel): Field( description='Whether this agent accepts get_signals discovery without caller credentials in brief or wholesale mode. true means an anonymous discovery request can produce a successful response, but the response may be a public subset and may differ from results for an authenticated principal or selected account. false means get_signals requires an authenticated principal. Absence means unspecified legacy behavior, so callers probe and handle AUTH_MISSING. A valid authenticated request is never rejected merely because credentials were supplied.' ), + NoExplicitNull, ] = None data_provider_domains: Annotated[ list[DataProviderDomain] | None, @@ -974,6 +1013,7 @@ class Signals(AdCPBaseModel): description="Data provider domains this signals agent is authorized to resell. Buyers should fetch each data provider's adagents.json for published signal definitions and to verify authorization.", min_length=1, ), + NoExplicitNull, ] = None discovery_modes: Annotated[ list[DiscoveryMode] | None, @@ -981,9 +1021,10 @@ class Signals(AdCPBaseModel): description="Discovery modes this signals agent supports on get_signals. 'brief' (default — every signals agent supports this): semantic discovery driven by signal_spec or signal_refs, with deprecated signal_ids accepted for older clients. 'wholesale': raw wholesale signals feed enumeration — caller omits signal_spec, signal_refs, and signal_ids and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries. Agents that do not declare 'wholesale' MAY return INVALID_REQUEST for wholesale calls. Absent declaration is treated as ['brief'].", min_length=1, ), + NoExplicitNull, ] = [DiscoveryMode.brief] features: Annotated[ - Features | None, Field(description='Optional signals features supported') + Features | None, Field(description='Optional signals features supported'), NoExplicitNull ] = None @@ -1038,19 +1079,26 @@ class PropertyFeature(AdCPBaseModel): ), ] range: Annotated[ - Range | None, Field(description='For quantitative features, the valid range') + Range | None, + Field(description='For quantitative features, the valid range'), + NoExplicitNull, ] = None categories: Annotated[ - list[str] | None, Field(description='For categorical features, the valid values') + list[str] | None, + Field(description='For categorical features, the valid values'), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of what this feature measures') + str | None, + Field(description='Human-readable description of what this feature measures'), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='URL to documentation explaining how this feature is calculated or measured. Helps buyers understand and compare methodologies across vendors.' ), + NoExplicitNull, ] = None @@ -1068,19 +1116,26 @@ class CreativeFeature(AdCPBaseModel): ), ] range: Annotated[ - Range | None, Field(description='For quantitative features, the valid range') + Range | None, + Field(description='For quantitative features, the valid range'), + NoExplicitNull, ] = None categories: Annotated[ - list[str] | None, Field(description='For categorical features, the valid values') + list[str] | None, + Field(description='For categorical features, the valid values'), + NoExplicitNull, ] = None description: Annotated[ - str | None, Field(description='Human-readable description of what this feature measures') + str | None, + Field(description='Human-readable description of what this feature measures'), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='URL to documentation explaining how this feature is calculated or measured.' ), + NoExplicitNull, ] = None @@ -1090,6 +1145,7 @@ class Governance(AdCPBaseModel): Field( description='Signal-activation policy for the portable attestation support declared in adcp.attestations. Presence means check_governance accepts runtime_attestations[] for purchase_type signal_activation. The claim_types list MUST be a subset of adcp.attestations.accepted_claim_types; issuer, resolver, verifier, proof-format, and delivery allowlists remain authoritative in the shared adcp.attestations block rather than being duplicated here.' ), + NoExplicitNull, ] = None aggregation_window_days: Annotated[ SchemaInt | None, @@ -1098,18 +1154,21 @@ class Governance(AdCPBaseModel): ge=1, le=365, ), + NoExplicitNull, ] = None property_features: Annotated[ list[PropertyFeature] | None, Field( description='Property features this governance agent can evaluate. Each feature describes a score, rating, or certification the agent can provide for properties.' ), + NoExplicitNull, ] = None creative_features: Annotated[ list[CreativeFeature] | None, Field( description='Creative features this governance agent can evaluate. Each feature describes a score, rating, or assessment the agent can provide for creatives (e.g., security scanning, creative quality, content categorization).' ), + NoExplicitNull, ] = None @@ -1135,7 +1194,9 @@ class Endpoint(AdCPBaseModel): ), ] preferred: Annotated[ - Type6 | None, Field(description='Preferred transport when host supports multiple') + Type6 | None, + Field(description='Preferred transport when host supports multiple'), + NoExplicitNull, ] = None @@ -1156,16 +1217,19 @@ class Multiplicity(AdCPBaseModel): Field( description='When true, build_creative accepts max_creatives (one distinct creative per catalog item).' ), + NoExplicitNull, ] = False max_creatives_limit: Annotated[ SchemaInt | None, Field(description='Ceiling on max_creatives. Omitted means no advertised ceiling.', ge=1), + NoExplicitNull, ] = None supports_signal_fanout: Annotated[ StrictBool | None, Field( description='Experimental (x-status: experimental) — agents setting this true MUST also list `creative.signal_fanout` in `experimental_features`; the surface MAY change between 3.x releases with notice (see docs/reference/experimental-status). When true, build_creative accepts signal_conditions[] (one distinct creative group per signal condition, keep-all). Mirrors supports_catalog_fanout.' ), + NoExplicitNull, ] = False max_signal_conditions_limit: Annotated[ SchemaInt | None, @@ -1173,26 +1237,31 @@ class Multiplicity(AdCPBaseModel): description='Ceiling on len(signal_conditions). Over-limit requests are CLAMPED (not rejected), like max_creatives_limit. Omitted means no advertised ceiling.', ge=1, ), + NoExplicitNull, ] = None supports_variants: Annotated[ StrictBool | None, Field( description='When true, build_creative accepts max_variants > 1 / variant_axis (alternatives per creative).' ), + NoExplicitNull, ] = False max_variants_limit: Annotated[ SchemaInt | None, Field(description='Ceiling on max_variants. Omitted means no advertised ceiling.', ge=1), + NoExplicitNull, ] = None variant_dimensions: Annotated[ list[VariantDimension] | None, Field(description='Which variant_axis.dimension values this agent supports.'), + NoExplicitNull, ] = None selection_strategies: Annotated[ list[creative_selection_strategy.CreativeSelectionStrategy] | None, Field( description='Which selection_strategy values this agent supports when sampling max_creatives < items_total. Sibling to variant_dimensions. Part of the experimental signal-fanout surface (feature id `creative.signal_fanout`).' ), + NoExplicitNull, ] = None @@ -1250,6 +1319,7 @@ class Localization(AdCPBaseModel): ge=0, le=50, ), + NoExplicitNull, ] = None locale_matching: Annotated[ Literal['rfc4647_lookup'], @@ -1316,6 +1386,7 @@ class RequestSigning(AdCPBaseModel): Field( description="Policy for content-digest coverage in request signatures. In AdCP 3.2 and later, an agent with request_signing.supported=true MUST explicitly emit 'required': every accepted signature on a request with a body covers content-digest, and a body-unbound signature is rejected with request_signature_components_incomplete. Omission retains the legacy effective default of 'either' only for a 3.0/3.1 response; 3.2 responses require this field explicitly. 'either' and 'forbidden' are deprecated legacy 3.0/3.1 postures retained only for version negotiation with pre-3.2 peers; they MUST NOT be advertised as a 3.2 signing posture and are removed in 4.0. A shared endpoint MUST select the verifier policy from trusted endpoint configuration and negotiated capabilities before dispatch, never from an unbound request-body version field." ), + NoExplicitNull, ] = None required_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, @@ -1323,6 +1394,7 @@ class RequestSigning(AdCPBaseModel): description="AdCP protocol operation names (e.g., 'create_media_buy') for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. A transport-specific rename of an operation is not a valid entry (MCP tools and the AdCP A2A profile use the operation name verbatim, so those names coincide with it) — verifiers MUST NOT accept operation names that are not defined by the AdCP protocol spec. Over MCP the verifier matches the tools/call params.name; over A2A it matches the skill of the sole DataPart of the message (SendMessage, SendStreamingMessage, message/send, message/stream, and the HTTP+JSON and gRPC equivalents). The A2A profile uses the AdCP operation name verbatim as skill, so that is the same name space. A request that does not resolve to exactly one operation is rejected with request_body_malformed; see docs/building/by-layer/L1/security.mdx#operation-resolution-over-a2a. JSON-RPC protocol method names like `tasks/cancel` belong in `protocol_methods_required_for`, not here. Empty in 3.0 by default; sellers populate selectively during per-counterparty pilots. In 4.0 this list MUST include all spend-committing operations the agent supports (create_media_buy, acquire_*, etc.). Every operation listed MUST also appear in `supported_for`; see `x-adcp-validation`.", validate_default=True, ), + NoExplicitNull, ] = [] warn_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, @@ -1330,12 +1402,14 @@ class RequestSigning(AdCPBaseModel): description='AdCP protocol operation names for shadow-mode verification. The verifier records missing signatures and well-formed signatures that fail verification or body binding, but MUST NOT establish verified-signer identity from a failed signature; processing continues only when an independent bearer, API-key, or mTLS authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. Used as a bridge between supported_for and required_for. Precedence: required_for > warn_for > supported_for. An operation MUST NOT appear in both warn_for and required_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] supported_for: Annotated[ list[Annotated[str, Field(pattern='^[a-z][a-z0-9_]*$')]] | None, Field( description='AdCP protocol operation names for which this agent verifies signatures when present but does not require them. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects even though an unsigned request may use configured fallback authentication. Typically a superset of required_for and warn_for.' ), + NoExplicitNull, ] = None protocol_methods_supported_for: Annotated[ list[ProtocolMethodsSupportedForItem] | None, @@ -1343,6 +1417,7 @@ class RequestSigning(AdCPBaseModel): description="JSON-RPC protocol method names for which this agent verifies signatures when present. Values MUST use exact, case-sensitive equality after JSON decoding: slash-path names such as 'tasks/cancel' and 'tasks/pushNotificationConfig/set' for A2A 0.3, or PascalCase names such as 'CancelTask' and 'CreateTaskPushNotificationConfig' for A2A 1.0. A dual-stack agent lists each supported wire name independently; implementations MUST NOT translate or normalize between protocol versions. The reserved MCP envelope method 'tools/call' is forbidden because its AdCP operation identity is params.name and belongs in supported_for. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects. Disjoint from supported_for, which carries lower_snake_case AdCP operation names only.", validate_default=True, ), + NoExplicitNull, ] = [] protocol_methods_warn_for: Annotated[ list[ProtocolMethodsWarnForItem] | None, @@ -1350,6 +1425,7 @@ class RequestSigning(AdCPBaseModel): description='Exact JSON-RPC protocol method names for shadow-mode verification, mirroring warn_for in the AdCP-operation namespace. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Missing signatures and well-formed signatures that fail verification or body binding are recorded but MUST NOT establish verified-signer identity; processing continues only when an independent authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. An item MUST NOT appear in both protocol_methods_warn_for and protocol_methods_required_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] protocol_methods_required_for: Annotated[ list[ProtocolMethodsRequiredForItem] | None, @@ -1357,6 +1433,7 @@ class RequestSigning(AdCPBaseModel): description='Exact JSON-RPC protocol method names for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Separate namespace from required_for: this bucket binds against the JSON-RPC method field, not tools/call params.name. Every listed method MUST also appear in protocol_methods_supported_for; see x-adcp-validation.', validate_default=True, ), + NoExplicitNull, ] = [] @@ -1377,6 +1454,7 @@ class WebhookSigning(AdCPBaseModel): Field( description='Identifier of the webhook-signing profile version the agent emits. Value MUST match the `tag=` parameter emitted in the RFC 9421 `Signature-Input` header (see docs/building/implementation/webhooks.mdx) so receivers can statically validate the declared profile against the on-wire tag. Closed enum; future profile revisions will extend this enum in a follow-up schema bump.' ), + NoExplicitNull, ] = None algorithms: Annotated[ list[Algorithm] | None, @@ -1384,6 +1462,7 @@ class WebhookSigning(AdCPBaseModel): description="Signature algorithms this agent uses on outbound webhooks. 3.0 profile permits 'ed25519' and 'ecdsa-p256-sha256' only; other values are reserved for future profile versions and MUST NOT be emitted under adcp/webhook-signing/v1.", min_length=1, ), + NoExplicitNull, ] = None legacy_hmac_fallback: Annotated[ StrictBool | None, @@ -1391,6 +1470,7 @@ class WebhookSigning(AdCPBaseModel): deprecated=True, description='Whether this agent will fall back to HMAC-SHA256 on the legacy push_notification_config.authentication, accounts[].notification_configs[].authentication, sync_principal.configuration.notification_configs[].authentication, or sync_agent_notification_configs.notification_configs[].authentication paths for receivers that have not adopted RFC 9421. Deprecated; removed in AdCP 4.0.', ), + NoExplicitNull, ] = False delivery_retry_horizon_seconds: Annotated[ SchemaInt | None, @@ -1399,6 +1479,7 @@ class WebhookSigning(AdCPBaseModel): ge=86400, le=604800, ), + NoExplicitNull, ] = None @@ -1409,21 +1490,26 @@ class KeyOrigins(AdCPBaseModel): governance_signing: Annotated[ AnyUrl | None, Field(description='Origin (scheme + host) serving the governance-signing JWKS.'), + NoExplicitNull, ] = None request_signing: Annotated[ - AnyUrl | None, Field(description='Origin (scheme + host) serving the request-signing JWKS.') + AnyUrl | None, + Field(description='Origin (scheme + host) serving the request-signing JWKS.'), + NoExplicitNull, ] = None webhook_signing: Annotated[ AnyUrl | None, Field( description='Origin (scheme + host) serving the JWKS used for webhook delivery. Webhooks are signed with `adcp_use: "request-signing"` keys; the deprecated `adcp_use: "webhook-signing"` value remains accepted during the backward-compatibility window.' ), + NoExplicitNull, ] = None tmp_signing: Annotated[ AnyUrl | None, Field( description='Origin (scheme + host) serving the TMP-signing JWKS, when this operator participates in TMP.' ), + NoExplicitNull, ] = None @@ -1434,12 +1520,14 @@ class CompromiseNotification(AdCPBaseModel): emits: Annotated[ StrictBool | None, Field(description='Whether this agent emits `identity.compromise_notification` events.'), + NoExplicitNull, ] = False accepts: Annotated[ StrictBool | None, Field( description='Whether this agent subscribes to `identity.compromise_notification` events from counterparties it verifies signatures from.' ), + NoExplicitNull, ] = False @@ -1452,24 +1540,28 @@ class Identity(AdCPBaseModel): Field( description="HTTPS URL of the operator's brand.json (typically `https://{operator-domain}/.well-known/brand.json`). Trust-root pointer for this agent's signing keys. See [security.mdx §Discovering an agent's signing keys via `brand_json_url`](https://adcontextprotocol.org/docs/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url) for the verifier algorithm and `x-adcp-validation` for structured constraints. Distinct from `sponsored_intelligence.brand_url`, which is a rendering pointer for SI agent visuals — verifiers MUST use this field for key discovery and MUST NOT fall back to `sponsored_intelligence.brand_url` as a trust-root pointer." ), + NoExplicitNull, ] = None per_principal_key_isolation: Annotated[ StrictBool | None, Field( description="When true, this multi-principal operator scopes signing keys per-principal so a single principal's key compromise does not silently re-scope across principals served by the same operator. `kid` values remain opaque to verifiers per RFC 7517; any operator-side naming convention (e.g., `{operator}:{principal}:{key_version}`) is internal bookkeeping and MUST NOT be parsed by verifiers. See docs/building/understanding/security-model.mdx." ), + NoExplicitNull, ] = False key_origins: Annotated[ KeyOrigins | None, Field( description='Map of signing-key surface/purpose → publishing origin, so counterparties can verify origin separation (e.g., governance keys served from a separate origin than transport/webhook keys) at onboarding. Absent means the operator has not declared a separation scheme; receivers SHOULD assume shared-origin. Every entry listed MUST have a corresponding signing posture declared elsewhere — `request_signing` requires non-empty `request_signing.supported_for`/`required_for`/`protocol_methods_supported_for`/`protocol_methods_required_for`; `webhook_signing` requires `webhook_signing.supported === true` and names the webhook delivery surface, not a required live `adcp_use: "webhook-signing"` key purpose — otherwise the consistency check at signature-verification time has nothing to anchor against. See `x-adcp-validation` and docs/building/by-layer/L1/security.mdx §Origin separation.' ), + NoExplicitNull, ] = None compromise_notification: Annotated[ CompromiseNotification | None, Field( description='Whether this agent emits the `identity.compromise_notification` webhook event on key revocation due to known or suspected compromise (as opposed to scheduled rotation). Subscribers use this to bound the window between compromise detected and verifiers converging on revocation. See docs/building/implementation/webhooks.mdx §identity.compromise_notification.' ), + NoExplicitNull, ] = None @@ -1507,18 +1599,21 @@ class Accreditation(AdCPBaseModel): Field( description="Optional identifier for the certification in the accrediting body's records (when one exists; many bodies do not issue stable IDs)." ), + NoExplicitNull, ] = None valid_until: Annotated[ date | None, Field( description="Optional ISO 8601 date when the current accreditation expires. Buyers MAY treat post-expiry data as un-accredited. Absence means the vendor does not assert an expiry — buyers SHOULD verify currency at the accrediting body's directory." ), + NoExplicitNull, ] = None evidence_url: Annotated[ AnyUrl | None, Field( description="Optional URL pointing at the accrediting body's public listing for this certification (the buyer's path to verify the claim independently)." ), + NoExplicitNull, ] = None @@ -1555,12 +1650,14 @@ class WholesaleFeedVersioning(AdCPBaseModel): Field( description="Whether the agent tracks pricing_version independently of wholesale_feed_version. When true, the agent returns both tokens and honors if_pricing_version separately — useful for rate-card sweeps that don't change product and signal metadata. When false or absent, the agent collapses both into wholesale_feed_version; callers SHOULD NOT send if_pricing_version (it will be ignored and may produce INVALID_REQUEST when sent without if_wholesale_feed_version per the dependencies rule)." ), + NoExplicitNull, ] = None cache_scope_account: Annotated[ StrictBool | None, Field( description="Whether the agent ever returns cache_scope: 'account' (i.e., publishes per-account overlays distinct from the public rate card). When true, buyers MUST be prepared to maintain account-overlay caches alongside the public layer. When false or absent, all responses are cache_scope: 'public' regardless of whether account was provided — the agent's rate card is universal. Confidentiality note: declaring true advertises that the agent runs custom-pricing deals (low-grade market-posture signal); agents preferring not to disclose this MAY omit the field and let consumers detect-on-call via cache_scope on response." ), + NoExplicitNull, ] = None @@ -1592,6 +1689,7 @@ class WholesaleFeedWebhooks(AdCPBaseModel): description='Wholesale feed webhook event types this agent can emit. Sales agents emit product.* events and MUST expose list_products or the deprecated 3.x wholesale get_products compatibility path. Signals agents emit signal.* events and MUST support wholesale get_signals. wholesale_feed.bulk_change requires at least one corresponding repair path.', min_length=1, ), + NoExplicitNull, ] = None @@ -1623,6 +1721,7 @@ class ReportingDestinationOffering(AdCPBaseModel): description='Physical file formats the seller can produce. Only meaningful for file_transfer.', min_length=1, ), + NoExplicitNull, ] = None access_modes: Annotated[ list[AccessMode] | None, @@ -1630,6 +1729,7 @@ class ReportingDestinationOffering(AdCPBaseModel): description='Dataset-share access families the seller can provision. Only meaningful for dataset_share.', min_length=1, ), + NoExplicitNull, ] = None verification_profiles: Annotated[ reporting_verification_profile_set.ReportingVerificationProfileSet, @@ -1665,6 +1765,7 @@ class Principal(AdCPBaseModel): ge=1, le=64, ), + NoExplicitNull, ] = None reporting_destination_offerings: Annotated[ list[ReportingDestinationOffering] | None, @@ -1673,6 +1774,7 @@ class Principal(AdCPBaseModel): max_length=3, min_length=1, ), + NoExplicitNull, ] = None suspension_interval_seconds: Annotated[ SchemaInt | None, @@ -1681,6 +1783,7 @@ class Principal(AdCPBaseModel): ge=1, le=86400, ), + NoExplicitNull, ] = None caller_event_types: Annotated[ list[notification_type.NotificationType] | None, @@ -1688,6 +1791,7 @@ class Principal(AdCPBaseModel): description='Notification types this seller can emit against caller-level subscribers, so buyers select from the offering instead of probing by rejection. Account-anchored entries fire with fire-time authorization scoping. When absent, only capabilities.changed is assumed. A caller-level subscriber requesting a type outside this set fails the sync request with UNSUPPORTED_FEATURE.', min_length=1, ), + NoExplicitNull, ] = None optimistic_concurrency: Annotated[ StrictBool, @@ -1701,6 +1805,7 @@ class TrustedMatch(AdCPBaseModel): surfaces: Annotated[ list[property_type.PropertyType] | None, Field(description='Surface types this seller supports via TMP.'), + NoExplicitNull, ] = None @@ -1708,12 +1813,14 @@ class Language(AdCPBaseModel): supported: Annotated[ StrictBool | None, Field(description='Whether seller supports language-preference targeting'), + NoExplicitNull, ] = None supported_languages: Annotated[ list[locale_tag.LanguageTag] | None, Field( description='Exact canonical BCP 47 language ranges accepted in targeting.language. Omission means the seller makes no exhaustive language-list declaration.' ), + NoExplicitNull, ] = None @@ -1743,18 +1850,21 @@ class GeoProximity(AdCPBaseModel): Field( description='Whether seller supports simple radius targeting (distance circle from a point)' ), + NoExplicitNull, ] = None travel_time: Annotated[ StrictBool | None, Field( description='Whether seller supports travel time isochrone targeting (requires a routing engine)' ), + NoExplicitNull, ] = None geometry: Annotated[ StrictBool | None, Field( description='Whether seller supports pre-computed GeoJSON geometry (buyer provides the polygon)' ), + NoExplicitNull, ] = None transport_modes: Annotated[ list[transport_mode.TransportMode] | None, @@ -1762,6 +1872,7 @@ class GeoProximity(AdCPBaseModel): description='Transport modes supported for travel_time isochrones. Only relevant when travel_time is true.', min_length=1, ), + NoExplicitNull, ] = None @@ -1769,30 +1880,35 @@ class Targeting(AdCPBaseModel): geo_countries: Annotated[ StrictBool | None, Field(description='Country-level targeting using ISO 3166-1 alpha-2 codes'), + NoExplicitNull, ] = None geo_regions: Annotated[ StrictBool | geo_region_support.GeographicRegionSupport | None, Field( description='ISO 3166-2 subdivision inclusion targeting. A legacy boolean is a coarse seller-wide declaration. Structured country/value entries are individually supported within the response scope, but do not promise joint composability or availability through the same execution route or account. Only Product.overlay_support supplies the binding set of executable targeting permissions for a configured Product.' ), + NoExplicitNull, ] = None geo_regions_exclude: Annotated[ StrictBool | geo_region_support.GeographicRegionSupport | None, Field( description='ISO 3166-2 subdivision exclusion targeting, declared independently from inclusion. Structured country/value entries are individually supported within the response scope, but do not promise joint composability or availability through the same execution route or account. Only Product.overlay_support supplies the binding set of executable targeting permissions for a configured Product; absence means buyers cannot infer exclusion support from geo_regions alone.' ), + NoExplicitNull, ] = None geo_metros: Annotated[ GeoMetros | None, Field( description='Metro area targeting. Properties indicate which classification systems are supported.' ), + NoExplicitNull, ] = None geo_postal_areas: Annotated[ postal_area_support.PostalAreaSupport | None, Field( description='Postal area targeting. Prefer the native country-keyed map where each ISO 3166-1 alpha-2 country lists supported country-local postal systems. Deprecated legacy country-fused postal-system boolean aliases may be emitted alongside native country keys during migration.' ), + NoExplicitNull, ] = None geo_places: Annotated[ dict[ @@ -1804,70 +1920,82 @@ class Targeting(AdCPBaseModel): description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.', min_length=1, ), + NoExplicitNull, ] = None age_restriction: Annotated[ AgeRestriction | None, Field(description='Age restriction capabilities for compliance (alcohol, gambling)'), + NoExplicitNull, ] = None demographics: Annotated[ Demographics | None, Field( description='Seller-wide discovery rollup for canonical demographic targeting. supported=true means at least one product implements the demographic targeting contract; it does not authorize demographic targeting on every product. Buyers MUST inspect Product.demographic_targeting for exact execution modes, bounds, intervals, and unknown-age behavior.' ), + NoExplicitNull, ] = None language: Annotated[ StrictBool | Language | None, Field( description="Language-preference targeting support. A legacy boolean is a coarse declaration. The structured form can enumerate the exact canonical BCP 47 targeting ranges the seller accepts. Buyers MUST treat supported_languages as exact selectable values: a declared 'fr' does not by itself authorize a request for 'fr-CA'. Sellers MUST reject unsupported requested values rather than silently widening or dropping them." ), + NoExplicitNull, ] = None keyword_targets: Annotated[ KeywordTargets | None, Field( description='Keyword targeting capabilities. Presence indicates support for targeting_overlay.keyword_targets and keyword_targets_add/remove in update_media_buy.' ), + NoExplicitNull, ] = None negative_keywords: Annotated[ NegativeKeywords | None, Field( description='Negative keyword capabilities. Presence indicates support for targeting_overlay.negative_keywords and negative_keywords_add/remove in update_media_buy.' ), + NoExplicitNull, ] = None placement_selection: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.placement_selection. False or absence makes no product-level promise. Product.overlay_support and Product.placements remain authoritative for a selected product.' ), + NoExplicitNull, ] = None property_list: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.property_list inclusion targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None property_list_exclude: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.property_list_exclude targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None collection_list: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.collection_list inclusion targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None collection_list_exclude: Annotated[ StrictBool | None, Field( description='When true, seller-wide rollup indicating at least one product supports targeting_overlay.collection_list_exclude targeting. False or absence makes no product-level promise. Product.overlay_support is authoritative.' ), + NoExplicitNull, ] = None geo_proximity: Annotated[ GeoProximity | None, Field( description='Proximity targeting capabilities from arbitrary coordinates via targeting_overlay.geo_proximity.' ), + NoExplicitNull, ] = None @@ -1880,12 +2008,14 @@ class ConversionTracking(AdCPBaseModel): Field( description='Whether this seller can deduplicate conversion events across multiple event sources within a single goal. When true, the seller honors the deduplication semantics in optimization_goals event_sources arrays — the same event_id from multiple sources counts once. When false or absent, buyers should use a single event source per goal; multi-source arrays will be treated as first-source-wins. Most social platforms cannot deduplicate across independently-managed pixel and CAPI sources.' ), + NoExplicitNull, ] = None per_creative_attribution: Annotated[ StrictBool | None, Field( description='Whether the seller can attribute conversions to specific creatives within a package and surface that breakdown via media_buy_deliveries[].by_package[].by_creative[].conversions in get_media_buy_delivery. Defaults to false when omitted. Sellers that report conversions only at the line / package / placement / campaign granularity (retail-media, MMP-mediated mobile, CTV performance) declare false (or omit) and the per-creative scenario grades not_applicable for them. Sellers that surface ad-level conversion attribution (most social platforms) declare true and the scenario asserts the breakdown is populated end-to-end. Defaults to false to preserve backward compatibility.' ), + NoExplicitNull, ] = None supported_event_types: Annotated[ list[event_type_1.EventType] | None, @@ -1893,6 +2023,7 @@ class ConversionTracking(AdCPBaseModel): description='Event types this seller can track and attribute. If omitted, all standard event types are supported.', min_length=1, ), + NoExplicitNull, ] = None supported_targets: Annotated[ list[SupportedTarget1] | None, @@ -1900,10 +2031,12 @@ class ConversionTracking(AdCPBaseModel): description='Event-goal target kinds this seller can compute against. Buyers should only submit event-kind optimization goals whose target.kind is listed here — sellers MUST reject goals with unlisted target kinds. When omitted, only target-less event goals (maximize conversion count within budget) are guaranteed; sellers MAY accept specific target kinds but buyers should not rely on it. Named to parallel `metric_optimization.supported_targets` at the product level — same concept (which target kinds are supported), one at seller-capability granularity and one at product granularity.', min_length=1, ), + NoExplicitNull, ] = None supported_uid_types: Annotated[ list[uid_type.UidType] | None, Field(description='Universal ID types accepted for user matching', min_length=1), + NoExplicitNull, ] = None supported_hashed_identifiers: Annotated[ list[SupportedIdentifierType] | None, @@ -1911,16 +2044,19 @@ class ConversionTracking(AdCPBaseModel): description='Hashed PII types accepted for user matching. Buyers must hash before sending (SHA-256, normalized).', min_length=1, ), + NoExplicitNull, ] = None supported_action_sources: Annotated[ list[action_source.ActionSource] | None, Field(description='Action sources this seller accepts events from', min_length=1), + NoExplicitNull, ] = None attribution_windows: Annotated[ list[AttributionWindow] | None, Field( description='Attribution windows available from this seller. Single-element arrays indicate fixed windows; multi-element arrays indicate configurable options the buyer can choose from via attribution_window on optimization goals.' ), + NoExplicitNull, ] = None @@ -1934,6 +2070,7 @@ class FrequencyCapping(AdCPBaseModel): description='Entity granularities the seller can enforce caps against. Values from the reach-unit enum. Omit to indicate all reach-unit values are supported.', min_length=1, ), + NoExplicitNull, ] = None supported_window_units: Annotated[ list[str] | None, @@ -1941,6 +2078,7 @@ class FrequencyCapping(AdCPBaseModel): description="Duration units the seller supports for frequency cap windows. Values must match the duration.json unit enum (e.g., 'hours', 'days', 'campaign'). Omit to indicate all duration units are supported.", min_length=1, ), + NoExplicitNull, ] = None @@ -1974,12 +2112,14 @@ class BudgetCapping(AdCPBaseModel): description='Feature-specific default cap-day timezone used when timezone_basis is fixed. Use UTC or an IANA timezone identifier.', min_length=1, ), + NoExplicitNull, ] = None buyer_timezone_override: Annotated[ StrictBool | None, Field( description='When true, buyers MAY override the default cap-day boundary via media-buy-level budget_cap_timezone. The accepted timezone applies to aggregate and package caps alike. When false or absent, a submitted override is rejected with UNSUPPORTED_FEATURE.' ), + NoExplicitNull, ] = None @@ -1990,7 +2130,9 @@ class SponsoredIntelligence(AdCPBaseModel): Field(description='Modalities, components, and commerce capabilities'), ] brand_url: Annotated[ - AnyUrl | None, Field(description='URL to brand.json with colors, fonts, logos, tone') + AnyUrl | None, + Field(description='URL to brand.json with colors, fonts, logos, tone'), + NoExplicitNull, ] = None @@ -2003,24 +2145,29 @@ class Brand(AdCPBaseModel): Field( description='Supports get_rights and acquire_rights for rights discovery and clearance' ), + NoExplicitNull, ] = False right_types: Annotated[ list[right_type.RightType] | None, Field(description='Types of rights available through this agent'), + NoExplicitNull, ] = None available_uses: Annotated[ list[right_use.RightUse] | None, Field(description="Rights uses available across this agent's roster"), + NoExplicitNull, ] = None generation_providers: Annotated[ list[str] | None, Field(description='LLM/generation providers this agent can issue credentials for'), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description="Description of the agent's brand protocol capabilities", max_length=5000 ), + NoExplicitNull, ] = None @@ -2058,12 +2205,14 @@ class Metric(AdCPBaseModel): Field( description='Optional URI pointing at the published standard this metric IMPLEMENTS (e.g., IAB Attention Measurement Guidelines, MRC Viewable Impression Measurement, GARM emissions framework). Distinct from `accreditations[]` — `standard_reference` is what the metric is built against; `accreditations[]` is third-party certification that the implementation actually conforms. Buyer agents normalizing across vendors SHOULD apply the AdCP URL canonicalization rules before comparing — vendors implementing the same standard MAY use different URL forms for the same canonical document.' ), + NoExplicitNull, ] = None accreditations: Annotated[ list[Accreditation] | None, Field( description="Third-party accreditations this metric holds (MRC, ARF, JIC, ABC, BARB, AGOF, etc.). Distinct from `standard_reference`: a metric can implement a standard without being independently accredited. Buyers asking 'is this MRC-accredited?' SHOULD check this array, not just `standard_reference`. Each entry names the accrediting body and optionally pins a certification ID, validity date, and evidence URL." ), + NoExplicitNull, ] = None unit: Annotated[ str | None, @@ -2071,18 +2220,21 @@ class Metric(AdCPBaseModel): description='Unit of the metric value when reported via `vendor_metric_values.value` (e.g., `score`, `seconds`, `persons`, `gCO2e`, `lift_percent`, `USD`). Buyers SHOULD render the unit alongside the value rather than computing units locally; sellers populating `vendor_metric_values.unit` MUST match this declaration when present.', examples=['score', 'seconds', 'persons', 'gCO2e', 'lift_percent', 'index', 'USD'], ), + NoExplicitNull, ] = None description: Annotated[ str | None, Field( description='Human-readable description of what this metric measures and any relevant methodology notes. Surfaced in buyer-agent UX when explaining the metric to humans.' ), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description="URL to the vendor's full methodology documentation for this metric. Buyers SHOULD link or fetch this when human review of the methodology is in scope (compliance, RFP review, accreditation audit). Field name mirrors `governance.property_features[].methodology_url`." ), + NoExplicitNull, ] = None methodology_version: Annotated[ str | None, @@ -2090,8 +2242,9 @@ class Metric(AdCPBaseModel): description='Optional version identifier (semver, ISO date, or vendor-defined version string) for the methodology this metric currently implements. When present, buyer agents pin the contracted version via `committed_metrics[].methodology_version` (vendor scope) so silent vendor methodology changes are detectable; absence means the vendor does not version their methodology and buyers MUST treat any change as untracked.', examples=['v2.1', '2026-Q1', '1.0'], ), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None class Measurement(AdCPBaseModel): @@ -2100,6 +2253,7 @@ class Measurement(AdCPBaseModel): Field( description="Whether this measurement agent produces compact provide_performance_feedback assertions for a buyer-controlled orchestrator gateway. In the first experimental tier the provider reads delivery through the gateway's get_media_buy_delivery task and returns assertions through its provide_performance_feedback task. The gateway decides what to forward to each seller." ), + NoExplicitNull, ] = False metrics: Annotated[ list[Metric], @@ -2117,6 +2271,7 @@ class CreativeSpecs(AdCPBaseModel): description='Seller-wide VAST execution ceiling. Each product format option declares its binding accepted subset in `params.vast_versions`.', min_length=1, ), + NoExplicitNull, ] = None macro_resolution_capabilities: Annotated[ list[macro_resolution_capability.MacroProcessingCapability] | None, @@ -2124,22 +2279,29 @@ class CreativeSpecs(AdCPBaseModel): description='Seller-wide ceiling for exact macro processing tuples (dialect identity/revision, semantic mapping, operation, actor, context, and encoding). It never proves a product execution path supports the same tuple and does not claim tracker firing; inspect the selected format option and, when standardized, product tracker capabilities.', min_length=1, ), + NoExplicitNull, ] = None mraid_versions: Annotated[ list[MraidVersion] | None, Field(description='MRAID versions supported for rich media mobile creatives'), + NoExplicitNull, ] = None vpaid: Annotated[ - StrictBool | None, Field(description='VPAID support for interactive video ads') + StrictBool | None, + Field(description='VPAID support for interactive video ads'), + NoExplicitNull, ] = None simid: Annotated[ - StrictBool | None, Field(description='SIMID support for interactive video ads') + StrictBool | None, + Field(description='SIMID support for interactive video ads'), + NoExplicitNull, ] = None vast_validation: Annotated[ VastValidation | None, Field( description="Level of VAST asset validation the seller performs at sync_creatives (including dry_run): 'structural' checks manifest shape and format requirements only and never inspects the VAST document; 'document' additionally parses the VAST document and can return VAST_PARSE_FAILED / VAST_VERSION_MISMATCH; 'wrapper' additionally resolves the wrapper chain and can return VAST_WRAPPER_DEPTH_EXCEEDED. Absent means 'structural'. See the VAST Validation section of the video channel documentation for the normative checks at each level." ), + NoExplicitNull, ] = VastValidation.structural @@ -2149,6 +2311,7 @@ class Execution(AdCPBaseModel): Field( description='Trusted Match Protocol (TMP) support. Presence of this object indicates the seller has TMP infrastructure deployed. Check individual products via get_products for per-product TMP capabilities.' ), + NoExplicitNull, ] = None axe_integrations: Annotated[ list[AnyUrl] | None, @@ -2156,15 +2319,17 @@ class Execution(AdCPBaseModel): deprecated=True, description='Deprecated. Legacy AXE integrations. Use trusted_match for new integrations.', ), + NoExplicitNull, ] = None creative_specs: Annotated[ - CreativeSpecs | None, Field(description='Creative specification support') + CreativeSpecs | None, Field(description='Creative specification support'), NoExplicitNull ] = None targeting: Annotated[ Targeting | None, Field( description='Targeting capabilities. If declared true/supported, buyer can use these targeting parameters and seller MUST honor them.' ), + NoExplicitNull, ] = None @@ -2178,6 +2343,7 @@ class SupportedFormat(AdCPBaseModel): description="Stable identifier for this format capability within the creative agent (e.g., 'audio_narration_30s'). The value MUST be unique across this agent's supported_formats catalog. Route the advertised operation with this ID: build_creative.target_capability_id(s) for build, preview_creative.target_capability_id for preview, or validate_input targets[] kind capability for validate. Capability IDs are agent-local routes and do not appear on portable manifests or products.", pattern='^[a-zA-Z0-9_-]+$', ), + NoExplicitNull, ] = None format: Annotated[ creative_operation_format_declaration.CreativeOperationFormatDeclaration, @@ -2191,6 +2357,7 @@ class SupportedFormat(AdCPBaseModel): description='Creative operations this capability supports. `build` means the agent can produce a conforming manifest via build_creative; `validate` means it can evaluate inputs against the declaration; `preview` means it can render a preview. New 3.2 producers MUST emit this field so buyers and registries can distinguish producers, validators, and renderers without probing tasks. Consumers interpret omission from a legacy 3.x entry as `["build"]`.', min_length=1, ), + NoExplicitNull, ] = [Operation.build] @@ -2203,60 +2370,70 @@ class Creative(AdCPBaseModel): Field( description='When true, this creative agent can process briefs with compliance requirements (required_disclosures, prohibited_claims) and will validate that disclosures can be satisfied by the target format.' ), + NoExplicitNull, ] = None has_creative_library: Annotated[ StrictBool | None, Field( description='When true, this agent hosts a creative library and supports list_creatives and creative_id references in build_creative. Creative agents with a library should also implement the accounts protocol (sync_accounts / list_accounts) so buyers can establish access.' ), + NoExplicitNull, ] = False supports_revisions: Annotated[ StrictBool | None, Field( description='When true, this agent accepts buyer-assigned revision_id on sync_creatives, enforces immutable revision content, echoes accepted revision identity, returns it from list_creatives, and attributes delivered executions to it. Revision support does not imply revision history, rollback, or staged activation.' ), + NoExplicitNull, ] = False supports_generation: Annotated[ StrictBool | None, Field( description='When true, this agent can generate creatives from natural language briefs via build_creative. The buyer provides a message with creative direction, and the agent produces a manifest with generated assets. When false, build_creative only supports transformation or library retrieval.' ), + NoExplicitNull, ] = False supports_transformation: Annotated[ StrictBool | None, Field( description='When true, this agent can transform or resize existing canonical manifests via build_creative. The buyer supplies a creative_manifest and an advertised target_capability_id.' ), + NoExplicitNull, ] = False representation_resolution: Annotated[ RepresentationResolution | None, Field( description='Explicit opt-in for deterministic seller-bound selection from `build_creative.creative_representation_set`. Only the destination sales agent may advertise and exercise this capability because resolution requires its current product, placement/publisher narrowings, and seller-wide execution ceilings. A standalone creative agent may help a buyer select locally but MUST NOT advertise this capability or claim seller deliverability. Absence means the caller selects a representation before sending a seller-bound manifest; the agent MUST NOT guess silently.' ), + NoExplicitNull, ] = None supports_transformers: Annotated[ StrictBool | None, Field( description='When true, this agent exposes account-scoped creative transformers via list_transformers (the creative analog of media-buy products) and accepts transformer_id + config on build_creative. Buyers SHOULD call list_transformers to discover available transformers, their typed config params (and account-scoped enumerable option values via expand_params), and pricing. When false or absent, the agent does not offer the transformer surface.' ), + NoExplicitNull, ] = False supports_refinement: Annotated[ StrictBool | None, Field( description="When true, this agent retains produced build_variant leaves for an agent-defined retention window and can re-build from one via build_creative's refine_from_build_variant_id — applying a natural-language instruction in message plus an optional config delta, returning new lineage-linked variants. A build-time agent capability independent of generation/transformation. When false or absent, refine_from_build_variant_id is rejected with UNSUPPORTED_FEATURE; buyers refine instead via the transform path (creative_manifest + message)." ), + NoExplicitNull, ] = False supports_spend_controls: Annotated[ StrictBool | None, Field( description='When true, build_creative honors a per-call `max_spend` ceiling (producing partial paid results and returning budget_status:"capped" + a BUDGET_CAP_REACHED advisory rather than overspending) AND supports mode:"estimate" dry-runs (a projected cost band, producing/billing nothing). When false or absent, max_spend / mode:estimate are rejected with UNSUPPORTED_FEATURE. Out-of-band billers (bills_through_adcp:false) have no AdCP cost truth to cap against, so this is meaningful only alongside bills_through_adcp:true.' ), + NoExplicitNull, ] = False supports_evaluator: Annotated[ StrictBool | None, Field( description="Experimental (x-status: experimental) — agents setting this true MUST also list `creative.evaluator` in `experimental_features`; the surface MAY change between 3.x releases with notice (see docs/reference/experimental-status). When true, build_creative accepts an advisory `evaluator` input (exemplars / account-arranged evaluator_id / agent_url, plus an optional `feature_requirement[]` gate, a `rank_by` ordering, and an allowlisted `feature_agent` pointer). Feature discovery uses this response's governance.creative_features catalog: rank_by, feature_requirement, and eval.features[] all share the same creative-feature vocabulary as get_creative_features. evaluator_id is not discovered from this catalog; it is a pre-provisioned account preset whose emitted feature_ids still come from it. The evaluator populates a per-leaf `eval` block of creative-feature values (creative-feature-result[], the same shape get_creative_features returns) on BuildCreativeVariantSuccess leaves, which is what the recommended/rank it sets on the best_of_n axis are computed over. The agent runs a gate-then-rank pipeline over its best_of_n exploration: it evaluates each leaf, DROPS leaves failing `feature_requirement[]` from its recommended survivors, then orders survivors by `rank_by`. The gate is internal pruning of which leaves the agent recommends/returns from its own exploration — it never blocks an already-produced billable leaf: what is produced and billed is governed by max_variants/max_creatives/max_spend, not the evaluator. When the evaluator names an external agent, it MUST appear in `creative_policy.accepted_verifiers[]` (off-list → EVALUATOR_AGENT_NOT_ACCEPTED), and the producing agent authenticates the outbound evaluator call on the transport. Evaluator credentials and caller-supplied trust material MUST NOT be passed in the build_creative payload; credential- or trust-material payload keys should be rejected with CREDENTIAL_IN_ARGS. When false or absent, the `evaluator` input is ignored and no `eval` block is emitted." ), + NoExplicitNull, ] = False refinable_retention_seconds: Annotated[ SchemaInt | None, @@ -2264,36 +2441,42 @@ class Creative(AdCPBaseModel): description='When supports_refinement is true, the GUARANTEED-MINIMUM window (a floor, not a ceiling) during which a produced build_variant_id remains refinable via refine_from_build_variant_id: a ref within this window from production SHOULD resolve; the agent MAY retain longer. Omit when the retention window is agent-defined and not advertised — buyers then treat refinability as best-effort and handle REFERENCE_NOT_FOUND.', ge=0, ), + NoExplicitNull, ] = None multiplicity: Annotated[ Multiplicity | None, Field( description="Pre-call discriminators for build_creative fan-out, so a buyer knows BEFORE sending max_creatives / max_variants whether this agent supports them and the ceilings. Over-limit requests are CLAMPED to these ceilings (the agent produces up to the limit and signals the shortfall via items_returned < items_total on BuildCreativeVariantSuccess), not rejected — consistent with item_limit's 'use the lesser' rule. Absent means no fan-out: build_creative produces a single creative and max_creatives/max_variants>1 are not supported." ), + NoExplicitNull, ] = None supported_formats: Annotated[ list[SupportedFormat] | None, Field( description='Canonical-format capability catalog for this creative agent. This is the 3.2 source of truth for discovering which format contracts the agent can build, validate, or preview; it replaces the deprecated `list_creative_formats` task. Each entry uses the authority-free `CreativeOperationFormatDeclaration` projection of a product format declaration: canonical shape and creative-route macro processing are preserved, while seller production commitments are excluded. New 3.2 producers MUST publish a stable agent-local `capability_id` and explicit `operations` for task routing. Every emitted `capability_id` MUST be unique within this catalog so a route selects exactly one entry. During the 3.x compatibility window, consumers MUST also accept legacy entries that omit either field; absent `operations` means `["build"]`, while an absent `capability_id` means the entry is discoverable by canonical contract but cannot be selected through a capability-ID route.\n\n**Publisher-specific support.** To claim exact support for a publisher declaration, `format` carries the declaration\'s `{publisher_domain, format_option_id}` pair plus its canonical `format_kind` and narrowed `params`. Generic creative agents MAY instead advertise a canonical parameter envelope without publisher identity. A generic capability matches a target declaration only when the capability can satisfy every target constraint; matching canonical names alone is insufficient. Registries MAY reverse-index these entries by `format.format_kind`, `format.publisher_domain`, and `format.format_option_id`.\n\nThis catalog describes creative operations, not sales-agent inventory deliverability. Sales agents publish the purchasable closed set on each `Product.format_options[]`; publisher acceptance lives in `adagents.json.formats[]`.' ), + NoExplicitNull, ] = None preview: Annotated[ Preview | None, Field( description='Per-route preview_creative capability metadata. New 3.2 producers whose supported_formats[] explicitly advertises a routable preview operation MUST emit this block. rendering_origin describes how each route is implemented but is informational and never grants presentation authority: only a matching publisher-origin placement preview_provider delegation can do that. routes[].capability_id MUST equal the set of capability IDs on supported_formats[] entries whose operations contains preview.' ), + NoExplicitNull, ] = None localization: Annotated[ Localization | None, Field( description='Materialized creative-localization support for sync_creatives/list_creatives, including source-only monolingual topology. Presence opts the agent into exact locale-variant round-trip, strict RFC 4647 Lookup, optional buyer-declared language-family fallback rules, explicit final default/unmatched behavior, creative-wide review, transactional replacement, seller product-format locale-policy enforcement, and delivery attribution. This is a coarse structural capability, not a promise that every locale/format/account combination is accepted; sellers publish accepted ranges on product format declarations and validate each write before mutation. Omit this object when localization is unsupported.' ), + NoExplicitNull, ] = None bills_through_adcp: Annotated[ StrictBool | None, Field( description='When true, this creative agent bills through the AdCP rate-card surface: list_creatives returns pricing_options when include_pricing=true with an authenticated account, build_creative populates pricing_option_id and vendor_cost on the response, and report_usage accepts records against the rate card. When false or absent, the agent bills out of band (flat license, SaaS contract, bundled enterprise agreement) and buyers should skip pricing fields and tolerate report_usage returning accepted: 0 with errors carrying BILLING_OUT_OF_BAND. A pre-call discriminator so buyer agents can route across many creative agents without first establishing an account to probe pricing.' ), + NoExplicitNull, ] = False canonical_catalog_version: Annotated[ str | None, @@ -2301,6 +2484,7 @@ class Creative(AdCPBaseModel): description="Optional. The AdCP canonical-formats catalog version this agent's runtime is built against (e.g., `3.1`, `3.2.0`). Lets buyer SDKs detect canonical-catalog skew between their generated types and the seller's actual support. SDKs MAY declare the version they were generated against (typically the AdCP version they ship for); when seller and SDK versions disagree, SDKs SHOULD soft-warn rather than fail (the open-enum semantics on `canonical-format-kind.json` make unknown canonicals safe to retain, so skew is not a hard error — it just means the older side might not understand newer canonical values). Omitted by sellers who haven't yet generated against a versioned catalog; absence is interpreted as the AdCP version advertised by the broader capabilities response.", pattern='^\\d+\\.\\d+(\\.\\d+)?$', ), + NoExplicitNull, ] = None @@ -2320,6 +2504,7 @@ class AudienceTargeting(AdCPBaseModel): Field( description="Whether the seller accepts the buyer's CRM/loyalty ID as a matchable identifier. Only applicable when the seller operates a closed ecosystem with a shared ID namespace (e.g., a retailer matching against their loyalty program). When true, buyers can include platform_customer_id values in AudienceMember.identifiers for matching against the seller's identity graph. Reporting on matched platform_customer_ids typically requires a clean room or the seller's own reporting surface." ), + NoExplicitNull, ] = None supported_uid_types: Annotated[ list[uid_type.UidType] | None, @@ -2327,6 +2512,7 @@ class AudienceTargeting(AdCPBaseModel): description='Universal ID types accepted for audience matching (MAIDs, RampID, UID2, etc.). MAID support varies significantly by platform — check this field before sending uids with type: maid.', min_length=1, ), + NoExplicitNull, ] = None minimum_audience_size: Annotated[ SchemaInt, @@ -2341,12 +2527,14 @@ class AudienceTargeting(AdCPBaseModel): description="Union of audience_activation.methods across the seller's products. Fast-fail discovery: a buyer reads this once and skips the catalog walk when nothing overlaps its pipeline. Per-product declarations are the source of truth; sellers MUST keep this consistent with the catalog. Operational coordinates are account-scoped: the union MAY omit consumer_identities and destination_ref until bilateral account setup establishes them, and MUST NOT expose another account's coordinates. Absence of this field with media_buy.audience_activation listed in experimental_features means walk the catalog; only a present, non-overlapping union is a fast-fail signal. Experimental (x-status: experimental): sellers implementing audience activation declarations MUST list media_buy.audience_activation in experimental_features. Per docs/reference/experimental-status, this surface MAY change between 3.x releases with notice.", min_length=1, ), + NoExplicitNull, ] = None matching_latency_hours: Annotated[ MatchingLatencyHours | None, Field( description='Expected matching latency range in hours after upload. Use to calibrate polling cadence and set appropriate expectations before configuring push_notification_config.' ), + NoExplicitNull, ] = None @@ -2356,12 +2544,14 @@ class MediaBuy(AdCPBaseModel): Field( description='Whether this seller accepts product discovery without caller credentials. This applies to list_products and to get_products in brief or wholesale mode; it does not apply to proposal refinement or finalization, purchasing, account-scoped reads, or mutations. true means an anonymous discovery request can produce a successful response, but the response may be a public subset and may differ from results for an authenticated principal or selected account. false means these discovery calls require an authenticated principal. Absence means unspecified legacy behavior, so callers probe and handle AUTH_MISSING. A valid authenticated request is never rejected merely because credentials were supplied. true is inconsistent with account.required_for_products=true because an anonymous caller cannot select protected account context.' ), + NoExplicitNull, ] = None acceptance_policy_discovery: Annotated[ AcceptancePolicyDiscovery | None, Field( description='Registry-backed seller acceptance-policy discovery. Presence means the seller publishes a versioned catalog; it does not claim that the seller evaluates acceptance_context during discovery. Discovery is advisory, exact task responses remain authoritative, and absent capability means support is unknown rather than unrestricted acceptance.' ), + NoExplicitNull, ] = None supported_pricing_models: Annotated[ list[pricing_model.PricingModel] | None, @@ -2369,6 +2559,7 @@ class MediaBuy(AdCPBaseModel): description='Pricing models this seller supports across its product portfolio. Buyers can use this for pre-flight filtering before querying individual products. Individual products may support a subset of these models.', min_length=1, ), + NoExplicitNull, ] = None buying_modes: Annotated[ list[BuyingMode] | None, @@ -2376,18 +2567,21 @@ class MediaBuy(AdCPBaseModel): description="Buying modes this seller supports on get_products. 'brief' (semantic discovery driven by the brief) is universally supported and implicit. 'wholesale' (raw wholesale product feed enumeration — caller omits brief and the seller returns the full priced product feed, paginated) is opt-in and SHOULD be declared explicitly so buyers can probe before issuing wholesale calls. 'refine' lets buyers iterate on prior products/proposals and is also the vehicle for finalizing draft proposals when the seller returns them. Sellers MAY declare ['brief', 'wholesale'] to signal wholesale support; absent declaration is treated as ['brief'] for wholesale-feed probing purposes and sellers MAY return INVALID_REQUEST for wholesale calls they do not support. Symmetric with signals.discovery_modes.", min_length=1, ), + NoExplicitNull, ] = [BuyingMode.brief] measurement_terms_acceptance: Annotated[ StrictBool | None, Field( description="Whether this seller can accept the default measurement_terms it advertises on a product. A value of true means the seller can return a product carrying measurement_terms for a measurement-specific brief and accept those terms unchanged on a package for that product. This opts the seller into conformance scenarios that discover and replay the product's own terms. False or absent means acceptance is outside the seller's advertised scope, but the seller must still reject unsupported terms with TERMS_REJECTED rather than being graded on an acceptance path it did not claim." ), + NoExplicitNull, ] = False availability_horizon: Annotated[ StrictBool | None, Field( description='Whether this seller supports flexible-window availability discovery: parsing offer_filters.availability_horizon and answering with time-dimensioned forecast points that carry availability_status. Sellers declaring true MUST apply the full window contract — half-open non-overlapping windows that partition the requested horizon (or signal gaps via incomplete[]), with availability_status computed from all booking eligibility constraints, not only competing holds. false or absent means flexible-window support is unknown: buyers SHOULD use exact start_date/end_date filtering, and sellers MAY ignore the field or reject it. Conformance storyboards gate flexible-window checks on this declaration.' ), + NoExplicitNull, ] = False lifecycle_tools: Annotated[ list[LifecycleTool] | None, @@ -2395,12 +2589,14 @@ class MediaBuy(AdCPBaseModel): description='Compact product and MediaBuy lifecycle operation names this seller supports. Added in AdCP 3.2 as task-specific contracts that form the 4.0 lifecycle foundation. Sellers may advertise any supported subset while retaining the deprecated get_products/create_media_buy/update_media_buy facades throughout 3.x. Each stateful split task has its own idempotency identity; callers MUST retry with the same tool name.', min_length=1, ), + NoExplicitNull, ] = None proposal_refinement: Annotated[ ProposalRefinement | None, Field( description='Pre-flight support for typed refine_proposals revision dimensions. These declarations mean the seller can parse and mechanically validate a dimension; they never promise that the seller will commercially concede it. Absence means typed-dimension support is unknown and buyers must handle per-result partial or unable outcomes. A dimension omitted from an explicit supported_dimensions list MUST be rejected at task level with UNSUPPORTED_FEATURE before any proposal is created.' ), + NoExplicitNull, ] = None reporting_delivery_methods: Annotated[ list[CapabilityReportingDeliveryMethod] | None, @@ -2408,12 +2604,14 @@ class MediaBuy(AdCPBaseModel): description="How this seller delivers reporting data to buyers. Polling via get_media_buy_delivery is always available as a baseline regardless of this field. This array declares additional push-based delivery methods the seller supports. 'webhook': seller pushes to buyer-provided URL (configured per buy via reporting_webhook). 'offline': seller pushes batch files to a cloud storage bucket (seller-provisioned per account via reporting_bucket on the account object). When absent, only polling is available.", min_length=1, ), + NoExplicitNull, ] = None performance_feedback: Annotated[ PerformanceFeedback | None, Field( description='Structured seller performance-feedback support beyond the legacy scalar contract. Presence means the seller accepts compact baseline/metric/provenance fields from a buyer orchestrator and returns a feedback_id.' ), + NoExplicitNull, ] = None offline_delivery_protocols: Annotated[ list[cloud_storage_protocol.CloudStorageProtocol] | None, @@ -2421,30 +2619,35 @@ class MediaBuy(AdCPBaseModel): description="Cloud storage protocols this seller supports for offline file delivery. Only meaningful when reporting_delivery_methods includes 'offline'. Buyers express a protocol preference in sync_accounts; the seller provisions the account's reporting_bucket using a supported protocol.", min_length=1, ), + NoExplicitNull, ] = None reporting_delivery: Annotated[ reporting_delivery_capabilities.ReportingDeliveryCapabilities | None, Field( description='AdCP 3.2 Reliable Reporting capability. The affirmative machine answer to ‘Do you support Reliable Reporting?’ requires this block with supported: true and reliable_reporting_version: 1.0 plus media_buy.reporting_delivery in experimental_features. Core exposes seller get_reporting_status; during the published migration window, consumer_status_task separately advertises opt-in buyer-to-seller sync_reporting_status and becomes required Core only in the next eligible minor. managed_delivery and reconciled_billing identify optional tiers. This generalizes, but does not remove, the legacy reporting_delivery_methods/offline_delivery_protocols surface; those legacy fields and declarations from other protocols do not imply Reliable Reporting support.' ), + NoExplicitNull, ] = None supports_proposals: Annotated[ StrictBool | None, Field( description='Conformance declaration that this seller supports proposals through either the compact request/refine/finalize lifecycle or the legacy get_products facade. accept_proposal, or the create_media_buy compatibility facade, consumes a finalized committed proposal_id before expires_at.' ), + NoExplicitNull, ] = False outcome_target: Annotated[ StrictBool | None, Field( description="Whether this seller supports reverse-forecast planning: parsing criteria.outcome_target (a compact goal — a forecastable-metric delivery metric or an event-type conversion event — plus a desired volume, a cost_per target, or both) and planning against it, answering with total_budget_guidance and forecasts whose points carry the goal's key in metrics; see outcome-target.json for cost_per answers and rejections. This flag does not distinguish sellers that plan cost_per; buyers rely on the negotiated adcp_version together with features.bidding_policy. false or absent means support is unknown: buyers SHOULD express outcome goals in brief prose instead, and sellers reject a structured outcome_target on proposal requests with UNSUPPORTED_FEATURE rather than silently ignoring it (list_products ignores it for every seller)." ), + NoExplicitNull, ] = False governance_aware: Annotated[ StrictBool | None, Field( description='Compatibility claim used by existing media-buy conformance runners. A value of true corresponds only to online governance consultation for create_media_buy, the historically graded surface. Agents use adcp.governance_enforcement for explicit task-scoped claims, including update_media_buy and cross-role signed-context enforcement.' ), + NoExplicitNull, ] = False propagation_surfaces: Annotated[ list[PropagationSurface] | None, @@ -2452,12 +2655,14 @@ class MediaBuy(AdCPBaseModel): description='Where this seller surfaces dependency-resource impairments (creative suspended/rejected post-approval, audience suspended, catalog item withdrawn, event source insufficient, property depublished) to buyers. Non-exclusive: a seller mirroring impairments on both the buy snapshot AND firing webhooks declares `["snapshot", "webhook"]` (the common case for premium guaranteed sellers). Each value names one surface where buyers can observe an impairment:\n\n- **`snapshot`** — seller propagates resource transitions into `media_buy.health` and `media_buy.impairments[]` on the next `get_media_buys` read. The `impairment.coherence` compliance assertion grades this surface; storyboards that exercise it (`media_buy_seller/dependency_impairment`, `media_buy_seller/dependency_impairment_cardinality`) require `"snapshot"` to be declared, else they grade `not_applicable`.\n- **`webhook`** — seller fires `notification-type: impairment` webhooks (configured via `push_notification_config`). Sellers declaring `"webhook"` MUST satisfy the persistent-channel webhook contract for the impairment event type. A seller declaring `["webhook"]` without `"snapshot"` is webhook-only — buyers reconcile state from the push channel alone, and snapshot-coherence storyboards grade `not_applicable`.\n- **`out_of_band`** — seller propagates via channels outside the AdCP protocol surface entirely (email to trafficker, separate dashboard, partner-specific notification feed). Long-tail and enterprise-bundled platforms commonly use this when impairment workflows are managed in human channels. Sellers declaring only `["out_of_band"]` are not graded by snapshot or webhook compliance — their bar is the offline agreement, not a protocol assertion. If a seller has impairment data in their API under a non-AdCP field name (a mapping gap, not truly out-of-band), they SHOULD document the mapping rather than declare `out_of_band` — the spec\'s gap, not the seller\'s posture, is what `out_of_band` legitimately covers.\n\nDefault: `["snapshot"]` when absent (preserves the existing snapshot-coherence contract for sellers that don\'t declare). Empty array `[]` is invalid (`minItems: 1`) — omit the field to inherit the default rather than declaring no surfaces. Pick the surfaces that honestly describe where buyers will see impairments on this agent. Mixing is normative — `["snapshot", "webhook"]` is the documented common case; `["snapshot", "webhook", "out_of_band"]` is valid for sellers that ship all three surfaces (rare but legal). See lifecycle.mdx § Compliance for the per-surface contract.', min_length=1, ), + NoExplicitNull, ] = [PropagationSurface.snapshot] creative_approval_mode: Annotated[ CreativeApprovalMode | None, Field( description="Tenant-wide applicability signal for media-buy creative approval behavior. This is not a notification or new approval workflow. `auto_approve` means human review does not block serving eligibility after creatives are assigned and automated validation passes. `require_human` means one or more products/accounts may require manual review before creatives become eligible to serve; buyers and compliance runners MUST treat this as a worst-case ceiling across this seller's portfolio unless a future product-level override says otherwise. Compliance runners use this mainly to decide whether auto-approval-dependent storyboards apply. When absent, approval behavior is legacy-unspecified; runners SHOULD NOT treat omission as an affirmative auto-approval claim. `ai_assisted` is intentionally not part of the enum until a behavioral contract is defined." ), + NoExplicitNull, ] = None supported_indicator_types: Annotated[ list[indicator_type.IndicatorType] | None, @@ -2465,34 +2670,41 @@ class MediaBuy(AdCPBaseModel): description="Indicator types this seller can expose on get_media_buys media-buy/package/creative-assignment snapshots. Each type's meaning is defined by the negotiated AdCP release; indicator types do not carry independent sub-versions. This is availability, not complete upstream coverage. Poll-only sellers may declare this field without relationship_notifications. If relationship_notifications includes indicators.changed, this field is required so receivers know which durable indicator types can be repaired.", min_length=1, ), + NoExplicitNull, ] = None relationship_notifications: Annotated[ RelationshipNotifications | None, Field( description='Optional durable account-level invalidations for indicator, creative-assignment, and assignment-approval changes. A seller may expose indicators only through polling and omit this block. A seller without an indicator catalog may declare creative.assignment_changed alone. get_media_buys is the complete authoritative repair read. creative.assignment_changed is independent of the optional bounded list_creatives reverse projection, so inline-only sellers can advertise approval and assignment invalidations. Presence means the seller accepts the declared subscriptions through sync_accounts notification_configs. Timestamp-only reevaluation does not fire. Poll-based upstream integrations fire when they detect a change; this declaration does not promise upstream detection latency.' ), + NoExplicitNull, ] = None - features: media_buy_features.MediaBuyFeatures | None = None + features: Annotated[media_buy_features.MediaBuyFeatures | None, NoExplicitNull] = None execution: Annotated[ - Execution | None, Field(description='Technical execution capabilities for media buying') + Execution | None, + Field(description='Technical execution capabilities for media buying'), + NoExplicitNull, ] = None audience_evidence: Annotated[ AudienceEvidence | None, Field( description='Support for structured product audience evidence and buyer-authored evidence policy. Presence means the seller can publish Product.audience_evidence, preserve immutable snapshots through package readback, and evaluate the declared policy modes. It does not declare audience-targeting capability.' ), + NoExplicitNull, ] = None rights_attestations: Annotated[ RightsAttestations | None, Field( description='Seller evaluation policy for portable rights-grant attestations carried on creative rights constraints. Presence requires adcp.attestations and the rights-grant claim URI in accepted_claim_types. The seller remains verifier-of-record and never treats verification_url or a buyer-authored evaluation as authorization.' ), + NoExplicitNull, ] = None audience_targeting: Annotated[ AudienceTargeting | None, Field( description='Audience targeting capabilities. Presence of this object indicates the seller supports audience targeting, including sync_audiences and audience_include/audience_exclude in targeting overlays.' ), + NoExplicitNull, ] = None supported_optimization_metrics: Annotated[ list[SupportedOptimizationMetric] | None, @@ -2500,48 +2712,56 @@ class MediaBuy(AdCPBaseModel): description='Optimization metrics this seller can support on at least one of their products. Seller-level rollup of product-level metric_optimization.supported_metrics declarations (core/product.json). Buyers SHOULD filter their requested optimization goals against this list before submitting briefs. Sellers MUST keep this in sync with their product catalog — if no products support a metric, it must not appear here. Omitting this field means the seller declares no specific guarantees about which metrics they support; buyers should fall back to per-product inspection of metric_optimization.supported_metrics.', min_length=1, ), + NoExplicitNull, ] = None vendor_metric_optimization: Annotated[ VendorMetricOptimization | None, Field( description='Seller-level rollup of vendor-metric optimization capabilities supported by at least one product. Product-level vendor_metric_optimization.supported_metrics[] remains authoritative for the specific (vendor, metric_id) pairs and target kinds a buyer may bind on a package; this seller-level object exists so buyers and compliance runners can discover whether vendor_metric goals are in scope before walking the catalog. Sellers MUST keep this in sync with product-level vendor_metric_optimization declarations.' ), + NoExplicitNull, ] = None conversion_tracking: Annotated[ ConversionTracking | None, Field( description='Seller-level conversion tracking capabilities. Presence of this object indicates the seller supports sync_event_sources and log_event for conversion event tracking.' ), + NoExplicitNull, ] = None frequency_capping: Annotated[ FrequencyCapping | None, Field( description='Seller-wide package frequency-capping infrastructure. Presence means the seller honors targeting_overlay.frequency_cap on packages, with an independent counter per package, and MUST reject caps it cannot enforce rather than silently dropping them. Product.overlay_support is the binding per-product declaration. A cap whose single counter spans every package in a MediaBuy is advertised separately through aggregate_frequency_capping.' ), + NoExplicitNull, ] = None aggregate_frequency_capping: Annotated[ media_buy_frequency_cap_capability.MediaBuyFrequencyCapCapability | None, Field( description='Seller-wide support for a maximum-impression cap whose single counter is shared across every package in a MediaBuy. Presence is required before a buyer sends a root frequency_cap. Products additionally declare media_buy_support.frequency_cap and any narrower constraints.' ), + NoExplicitNull, ] = None budget_capping: Annotated[ BudgetCapping | None, Field( description='Hard daily budget-cap capabilities. Presence declares only the scopes listed in supported_scopes; sellers MUST reject a daily_budget_cap at an undeclared scope with UNSUPPORTED_FEATURE before mutation and MUST NOT silently drop or soften it. A cap is always a hard ceiling. Sellers that offer a best-effort pacing target must expose that under a separately named feature rather than interpreting daily_budget_cap as soft. Daily caps are orthogonal to pacing.' ), + NoExplicitNull, ] = None content_standards: Annotated[ ContentStandards | None, Field( description='Content standards implementation details. Presence of this object indicates the seller supports content_standards configuration including sampling rates and category filtering. Gives buyers pre-buy visibility into local evaluation and artifact delivery capabilities. This is a seller-side media-buy capability; governance agents providing content standards services declare `specialisms: ["content-standards"]` instead.' ), + NoExplicitNull, ] = None portfolio: Annotated[ Portfolio | None, Field( description="Information about the seller's media inventory portfolio. Media-buy sellers SHOULD publish primary_channels and primary_countries as their complete brief-routing scope. Buyers use publisher_domains to verify authorization via adagents.json. Omitted routing arrays mean unknown scope and MUST NOT be interpreted as global coverage." ), + NoExplicitNull, ] = None @@ -2561,6 +2781,7 @@ class Adcp(AdCPBaseModel): examples=[['3.0'], ['3.0', '3.1'], ['3.0', '3.1-beta']], min_length=1, ), + NoExplicitNull, ] = None build_version: Annotated[ str | None, @@ -2575,6 +2796,7 @@ class Adcp(AdCPBaseModel): ], pattern='^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$', ), + NoExplicitNull, ] = None idempotency: Annotated[ Idempotency | Idempotency1, @@ -2587,24 +2809,28 @@ class Adcp(AdCPBaseModel): Field( description='Caller-scoped durable connection configuration accepted by this agent. This is the buyer-to-seller configuration half of negotiation, not a second seller capability document: the seller advertises its objective offering here, while each authenticated caller submits its desired webhooks and reusable destinations through sync_principal and reads them back through get_principal. Per-account authority and feed selection remain in account/reporting configuration. Sellers exposing this block MUST list protocol.principal in experimental_features.' ), + NoExplicitNull, ] = None capability_changes: Annotated[ CapabilityChanges | None, Field( description='Freshness metadata and optional invalidation webhooks for this `get_adcp_capabilities` document. Buyers and registries MAY cache capabilities for up to `cache_ttl_seconds` when present, SHOULD compare `capabilities_version` across refreshes when present, and SHOULD re-run `get_adcp_capabilities` after receiving a `capabilities.changed` webhook. This block describes the agent-wide capability document, not per-caller authorization or account-scoped settings. A material capability change is any externally advertised contract change that can affect routing, validation, conformance coverage, task availability, auth/account handling, sandbox support, billing support, reporting delivery methods, creative-library support, targeting support, protocol versions, or other buyer-visible feature gates. Non-contract operational changes that do not alter the response body do not require a revision or webhook fire.' ), + NoExplicitNull, ] = None governance_enforcement: Annotated[ GovernanceEnforcement | None, Field( description='Cross-role declaration that this agent enforces buyer-provided governance authorization when performing consequential tasks. This is distinct from the top-level `governance` capability block, which describes an agent that provides governance services. An agent can enforce governance without implementing the governance protocol itself. Absence means the agent makes no governance-enforcement conformance claim.' ), + NoExplicitNull, ] = None attestations: Annotated[ attestation_capabilities.AttestationCapabilities | None, Field( description='Portable-attestation trust and delivery capabilities for this evaluator. Present only when the agent accepts AttestationReference inputs on one or more domain task surfaces. This block is an allowlist: presenters cannot expand accepted issuers, resolver endpoints, verifier agents, claim types, or proof formats by supplying values in a request.' ), + NoExplicitNull, ] = None @@ -2625,123 +2851,145 @@ class GetAdcpCapabilitiesResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnv Field( description='Account management capabilities. Describes how accounts are established, what billing models are supported, and whether an account is required before browsing products.' ), + NoExplicitNull, ] = None media_buy: Annotated[ MediaBuy | None, Field( description='Media-buy protocol capabilities. Expected when media_buy is in supported_protocols. Sellers declaring media_buy should also include account with supported_billing.' ), + NoExplicitNull, ] = None signals: Annotated[ Signals | None, Field( description='Signals protocol capabilities. Only present if signals is in supported_protocols.' ), + NoExplicitNull, ] = None governance: Annotated[ Governance | None, Field( description='Governance protocol capabilities. Only present if governance is in supported_protocols. Governance agents provide property and creative data like compliance scores, brand safety ratings, sustainability metrics, and creative quality assessments.' ), + NoExplicitNull, ] = None sponsored_intelligence: Annotated[ SponsoredIntelligence | None, Field( description='Sponsored Intelligence protocol capabilities. Only present if sponsored_intelligence is in supported_protocols. SI agents handle conversational brand experiences.' ), + NoExplicitNull, ] = None brand: Annotated[ Brand | None, Field( description='Brand protocol capabilities. Only present if brand is in supported_protocols. Brand agents provide identity data (logos, colors, tone, assets) and optionally rights clearance for licensable content (talent, music, stock media).' ), + NoExplicitNull, ] = None creative: Annotated[ Creative | None, Field( description='Creative protocol capabilities. Only present if creative is in supported_protocols.' ), + NoExplicitNull, ] = None oauth: Annotated[ Oauth | None, Field( description='Introduced in AdCP 3.2. OAuth 2.0 protected-resource support for inbound transport authentication. This is a capability claim, not a requirement that every caller use OAuth: `supported: true` means the agent publishes RFC 9728 protected-resource metadata for its advertised endpoint and RFC 8414 metadata for every referenced authorization server, and opts into the universal `oauth_setup` conformance storyboard. Agents that use only static Bearer, Basic, mTLS, or RFC 9421 authentication omit this block or declare `supported: false`. Operator credential acquisition remains separately described by `account.authorization_endpoint` when applicable.' ), + NoExplicitNull, ] = None request_signing: Annotated[ RequestSigning | None, Field( description='RFC 9421 HTTP Signatures support for incoming requests. Signing remains optional through 3.2, but every accepted 3.2 signature on a request with a body MUST cover content-digest. Request signing becomes required for spend-committing operations in 4.0. The full profile is defined in docs/building/by-layer/L1/security.mdx (Signed Requests (Transport Layer)).' ), + NoExplicitNull, ] = None webhook_signing: Annotated[ WebhookSigning | None, Field( description='RFC 9421 webhook-signature and delivery-retry support for outbound webhook callbacks (top-level peer of request_signing). Declares which AdCP webhook-signing profile version and algorithms this agent produces on delivery, whether it supports the legacy HMAC-SHA256 fallback for receivers that have not yet adopted RFC 9421, and the maximum retry horizon receivers use to retain immutable delivery evidence. See docs/building/by-layer/L3/webhooks.mdx.' ), + NoExplicitNull, ] = None identity: Annotated[ Identity | None, Field( description='Operator identity posture — trust-root pointer (`brand_json_url`) plus key-scoping and compromise-response controls the agent operates. `brand_json_url` is **load-bearing** for signature verification: when the agent declares any signing posture (`request_signing.supported_for`/`required_for` non-empty, `webhook_signing.supported === true`, or any `key_origins` subfield), `brand_json_url` MUST be present (storyboard-enforced in 3.x; schema-required in 4.0). Verifiers use it to bootstrap from the agent URL to the operator\'s brand.json (and from there to signing keys); see [security.mdx §Discovering an agent\'s signing keys](https://adcontextprotocol.org/docs/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url). The remaining fields (`per_principal_key_isolation`, `key_origins`, `compromise_notification`) are advisory and receivers use them to reason about blast radius and revocation latency at onboarding. Empty-object semantics: `identity: {}` means "posture block present but no posture claimed" — schema-valid but advisory-neutral and receivers MUST treat it as equivalent to omitting the block, **except** that an agent declaring a signing posture elsewhere in the response with an empty `identity` MUST be rejected by storyboard runners as missing `brand_json_url`.' ), + NoExplicitNull, ] = None measurement_gateway: Annotated[ MeasurementGateway | None, Field( description='Buyer-controlled task gateway between an orchestrator and measurement providers. In the first experimental tier, providers read buyer-approved cross-seller delivery through get_media_buy_delivery and return compact assertions through provide_performance_feedback, without receiving seller credentials. Orchestrators implementing this block MUST include measurement in supported_protocols and measurement.gateway in experimental_features; this gateway role does not claim the media_buy seller protocol.' ), + NoExplicitNull, ] = None measurement: Annotated[ Measurement | None, Field( description="Experimental measurement capability block. Presence indicates this agent computes one or more quantitative metrics about ad delivery, exposure, or effect, and is willing to be discovered as a measurement vendor. Agents implementing this block MUST list `measurement.core` in experimental_features. Returns metric definitions and whether the provider produces compact performance feedback, not pricing/coverage (negotiated via `measurement_terms` on `create_media_buy`) or raw/live datasets. Per-buy vendor values remain on delivery reports; optimizer-ready projections use provide_performance_feedback. AgenticAdvertising.org crawls each measurement agent's `metrics[]` on a TTL to populate the federated cross-vendor index." ), + NoExplicitNull, ] = None compliance_testing: Annotated[ ComplianceTesting | None, Field( description="Compliance testing capabilities. The presence of this block declares that the agent supports deterministic testing via comply_test_controller for lifecycle state machine validation. Omit the block entirely if the agent does not support compliance testing. Sellers SHOULD list every canonical controller scenario they implement so buyers and runners can distinguish full deterministic coverage from partial coverage without probing each scenario one by one; the runtime source of truth remains comply_test_controller with scenario: 'list_scenarios'." ), + NoExplicitNull, ] = None specialisms: Annotated[ list[specialism.AdcpSpecialism] | None, Field( description="Optional — specialized compliance claims this agent supports. Values MUST be kebab-case enum IDs (e.g., 'creative-generative', 'sales-non-guaranteed'). An agent that implements a specialism's tools but omits its ID from this array will receive 'No applicable tracks found' from the compliance runner — tracks for that specialism are not evaluated even if every tool works. Omitting the field means the agent declares no specialism claims (it still passes the universal + domain-baseline storyboards implied by supported_protocols). Each specialism maps to a storyboard bundle at /compliance/{version}/specialisms/{id}/ that the AAO compliance runner executes to verify the claim. Each specialism rolls up to one of the protocols in supported_protocols — the runner rejects a specialism claim whose parent protocol is missing. Only list specialisms your agent actually implements — the AAO Verified badge enumerates which specialisms were demonstrably passed." ), + NoExplicitNull, ] = None extensions_supported: Annotated[ list[ExtensionsSupportedItem] | None, Field( description='Extension namespaces this agent supports. Buyers can expect meaningful data in ext.{namespace} fields on responses from this agent. Extension schemas are published in the AdCP extension registry.' ), + NoExplicitNull, ] = None experimental_features: Annotated[ list[experimental_feature_id.ExperimentalFeatureId] | None, Field( description='Experimental AdCP surfaces this agent implements. A surface is experimental when its schema carries x-status: experimental and the working group has not yet frozen it. Sellers that implement any experimental surface MUST list its feature id here. Buyers inspect this array before relying on experimental surfaces — a seller that does not list a surface is asserting it does not implement it. Experimental surfaces MAY break between any two 3.x releases with at least 6 weeks notice; the full contract is in docs/reference/experimental-status.' ), + NoExplicitNull, ] = None wholesale_feed_versioning: Annotated[ WholesaleFeedVersioning | None, Field( description="Conditional-fetch token capabilities for get_products and get_signals. Independent of wholesale feed webhooks: an agent MAY support cheap version probes via if_wholesale_feed_version without pushing change payloads (and vice versa). When supported is true, the agent returns wholesale_feed_version on every get_products / get_signals response and honors if_wholesale_feed_version on subsequent requests. When absent or supported is false, callers MAY still send if_wholesale_feed_version — pre-3.1 agents that ignore it just return the full payload (correct, just inefficient). Pre-flight declaration here lets buyers fast-path which agents to bother caching versions for. See get_products / get_signals 'Wholesale feed versioning' sections." ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='ISO 8601 timestamp of when capabilities were last updated. Buyers can use this for cache invalidation.' ), + NoExplicitNull, ] = None errors: Annotated[ - list[error.Error] | None, Field(description='Task-specific errors and warnings') + list[error.Error] | None, + Field(description='Task-specific errors and warnings'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None wholesale_feed_webhooks: Annotated[ WholesaleFeedWebhooks | None, Field( description='Per-agent wholesale product-feed and wholesale signals-feed webhook capabilities. Consumers register durable sync_accounts notification subscribers and receive actual product.*, signal.*, or wholesale_feed.bulk_change payloads without polling. Product mirrors bootstrap and repair through list_products(if_feed_version); signal mirrors use get_signals(if_wholesale_feed_version). Deprecated wholesale get_products remains the 3.x product compatibility path. Webhook emission MUST apply the same caller/account authorization and cache-scope predicate as the corresponding read.' ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/protocol/get_principal_request.py b/src/adcp/types/domains/protocol/get_principal_request.py index 68750f185..455eee3cf 100644 --- a/src/adcp/types/domains/protocol/get_principal_request.py +++ b/src/adcp/types/domains/protocol/get_principal_request.py @@ -1,11 +1,14 @@ # generated by datamodel-codegen: # filename: protocol/get_principal_request.json -# timestamp: 2026-10-04T02:05:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types.base import AdcpRequest +from typing import Annotated + +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict from ..core import context as context_1 @@ -17,5 +20,5 @@ class GetPrincipalRequest(AdcpRequest, AdcpVersionEnvelope): model_config = ConfigDict( extra='allow', ) - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/get_principal_response.py b/src/adcp/types/domains/protocol/get_principal_response.py index 9efec1470..0cf955231 100644 --- a/src/adcp/types/domains/protocol/get_principal_response.py +++ b/src/adcp/types/domains/protocol/get_principal_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_principal_response.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -91,8 +91,8 @@ class GetPrincipalResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): extra='allow', ) result: Result2 | Result3 | Result | Result5 - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None # Stable aliases for anonymous result arms (selected by discriminator). diff --git a/src/adcp/types/domains/protocol/get_task_status_request.py b/src/adcp/types/domains/protocol/get_task_status_request.py index 4ff18133b..7680753d1 100644 --- a/src/adcp/types/domains/protocol/get_task_status_request.py +++ b/src/adcp/types/domains/protocol/get_task_status_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_task_status_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import account_ref @@ -26,18 +27,21 @@ class GetTaskStatusRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Account scope for the task lookup. Sellers MUST return REFERENCE_NOT_FOUND for a task_id that exists only under a different account or principal. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account.' ), + NoExplicitNull, ] = None include_history: Annotated[ StrictBool | None, Field( description='Include full conversation history for this task (may increase response size)' ), + NoExplicitNull, ] = False include_result: Annotated[ StrictBool | None, Field( description="Include the task's canonical terminal result payload when one exists. Defaults to false for lightweight status-only polls. When true, sellers MUST include result for completed, failed, or rejected terminal tasks when that task produced a terminal artifact; canceled tasks may have no result. The legacy singular error field remains a convenience for failed tasks but does not replace the canonical terminal result." ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/get_task_status_response.py b/src/adcp/types/domains/protocol/get_task_status_response.py index 596218463..941006d3e 100644 --- a/src/adcp/types/domains/protocol/get_task_status_response.py +++ b/src/adcp/types/domains/protocol/get_task_status_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/get_task_status_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool, StrictFloat from ..core import context as context_1 @@ -25,15 +25,21 @@ class Progress(AdCPBaseModel): extra='allow', ) percentage: Annotated[ - StrictFloat | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0) + StrictFloat | None, + Field(description='Completion percentage (0-100)', ge=0.0, le=100.0), + NoExplicitNull, ] = None current_step: Annotated[ - str | None, Field(description='Current step or phase of the operation') + str | None, Field(description='Current step or phase of the operation'), NoExplicitNull ] = None total_steps: Annotated[ - SchemaInt | None, Field(description='Total number of steps in the operation', ge=1) + SchemaInt | None, + Field(description='Total number of steps in the operation', ge=1), + NoExplicitNull, + ] = None + step_number: Annotated[ + SchemaInt | None, Field(description='Current step number', ge=1), NoExplicitNull ] = None - step_number: Annotated[SchemaInt | None, Field(description='Current step number', ge=1)] = None class Details(AdCPBaseModel): @@ -41,11 +47,15 @@ class Details(AdCPBaseModel): extra='allow', ) protocol: Annotated[ - adcp_protocol.AdcpProtocol | None, Field(description='AdCP protocol where error occurred') + adcp_protocol.AdcpProtocol | None, + Field(description='AdCP protocol where error occurred'), + NoExplicitNull, + ] = None + operation: Annotated[ + str | None, Field(description='Specific operation that failed'), NoExplicitNull ] = None - operation: Annotated[str | None, Field(description='Specific operation that failed')] = None specific_context: Annotated[ - dict[str, Any] | None, Field(description='Domain-specific error context') + dict[str, Any] | None, Field(description='Domain-specific error context'), NoExplicitNull ] = None @@ -55,7 +65,9 @@ class Error(AdCPBaseModel): ) code: Annotated[str, Field(description='Error code for programmatic handling')] message: Annotated[str, Field(description='Detailed error message')] - details: Annotated[Details | None, Field(description='Additional error context')] = None + details: Annotated[ + Details | None, Field(description='Additional error context'), NoExplicitNull + ] = None class Type(StrEnum): @@ -95,30 +107,38 @@ class GetTaskStatusResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) Field( description='When the task completed (ISO 8601, only for completed/failed/canceled tasks)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ - StrictBool | None, Field(description='Whether this task has webhook configuration') + StrictBool | None, + Field(description='Whether this task has webhook configuration'), + NoExplicitNull, ] = None progress: Annotated[ - Progress | None, Field(description='Progress information for long-running tasks') + Progress | None, + Field(description='Progress information for long-running tasks'), + NoExplicitNull, ] = None error: Annotated[ Error | None, Field( description='Convenience summary for failed tasks. When include_result was true and the canonical terminal result is also present, this error MUST agree with the canonical fatal error in result. A legacy poll carrying only this singular summary proves failure status but not equivalence to a richer terminal webhook artifact.' ), + NoExplicitNull, ] = None history: Annotated[ list[HistoryItem] | None, Field( description='Complete conversation history for this task (only included if include_history was true in request)' ), + NoExplicitNull, ] = None result: Annotated[ dict[str, Any] | None, Field( description='Canonical task-specific terminal payload. Present when include_result was true and a completed, failed, or rejected task produced a terminal artifact; canceled tasks may omit it. For failed tasks, the singular error field is a convenience summary and MUST agree with the canonical fatal error represented here. Consumers and sellers MUST resolve and validate the exact schema through manifest.task_result_resolution: use terminal_schema_overrides[task_type] when present, otherwise tools[task_type].response_schema. The polling envelope keeps this field generic so get_task_status does not embed every task response schema.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/list_tasks_request.py b/src/adcp/types/domains/protocol/list_tasks_request.py index b152c9939..7e028e47e 100644 --- a/src/adcp/types/domains/protocol/list_tasks_request.py +++ b/src/adcp/types/domains/protocol/list_tasks_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/list_tasks_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..core import account_ref @@ -34,53 +34,69 @@ class Filters(AdCPBaseModel): extra='allow', ) protocol: Annotated[ - adcp_protocol.AdcpProtocol | None, Field(description='Filter by single AdCP protocol') + adcp_protocol.AdcpProtocol | None, + Field(description='Filter by single AdCP protocol'), + NoExplicitNull, ] = None protocols: Annotated[ list[adcp_protocol.AdcpProtocol] | None, Field(description='Filter by multiple AdCP protocols', min_length=1), + NoExplicitNull, ] = None status: Annotated[ - task_status.TaskStatus | None, Field(description='Filter by single task status') + task_status.TaskStatus | None, + Field(description='Filter by single task status'), + NoExplicitNull, ] = None statuses: Annotated[ list[task_status.TaskStatus] | None, Field(description='Filter by multiple task statuses', min_length=1), + NoExplicitNull, ] = None task_type: Annotated[ - task_type_1.TaskType | None, Field(description='Filter by single task type') + task_type_1.TaskType | None, Field(description='Filter by single task type'), NoExplicitNull ] = None task_types: Annotated[ list[task_type_1.TaskType] | None, Field(description='Filter by multiple task types', min_length=1), + NoExplicitNull, ] = None created_after: Annotated[ - AwareDatetime | None, Field(description='Filter tasks created after this date (ISO 8601)') + AwareDatetime | None, + Field(description='Filter tasks created after this date (ISO 8601)'), + NoExplicitNull, ] = None created_before: Annotated[ - AwareDatetime | None, Field(description='Filter tasks created before this date (ISO 8601)') + AwareDatetime | None, + Field(description='Filter tasks created before this date (ISO 8601)'), + NoExplicitNull, ] = None updated_after: Annotated[ AwareDatetime | None, Field(description='Filter tasks last updated after this date (ISO 8601)'), + NoExplicitNull, ] = None updated_before: Annotated[ AwareDatetime | None, Field(description='Filter tasks last updated before this date (ISO 8601)'), + NoExplicitNull, ] = None task_ids: Annotated[ list[str] | None, Field(description='Filter by specific task IDs', max_length=100, min_length=1), + NoExplicitNull, ] = None context_contains: Annotated[ str | None, Field( description='Filter tasks where context contains this text (searches media_buy_id, signal_id, etc.)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ StrictBool | None, Field(description='Filter tasks that have webhook configuration when true'), + NoExplicitNull, ] = None @@ -88,9 +104,11 @@ class Sort(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - field: Annotated[Field1 | None, Field(description='Field to sort by')] = Field1.created_at + field: Annotated[Field1 | None, Field(description='Field to sort by'), NoExplicitNull] = ( + Field1.created_at + ) direction: Annotated[ - sort_direction.SortDirection | None, Field(description='Sort direction') + sort_direction.SortDirection | None, Field(description='Sort direction'), NoExplicitNull ] = sort_direction.SortDirection.desc @@ -103,17 +121,19 @@ class ListTasksRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Account scope for task reconciliation. Sellers MUST only return tasks created for the caller's authenticated account + principal pair. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account." ), + NoExplicitNull, ] = None - filters: Annotated[Filters | None, Field(description='Filter criteria for querying tasks')] = ( - None - ) - sort: Annotated[Sort | None, Field(description='Sorting parameters')] = None - pagination: pagination_request.PaginationRequest | None = None + filters: Annotated[ + Filters | None, Field(description='Filter criteria for querying tasks'), NoExplicitNull + ] = None + sort: Annotated[Sort | None, Field(description='Sorting parameters'), NoExplicitNull] = None + pagination: Annotated[pagination_request.PaginationRequest | None, NoExplicitNull] = None include_history: Annotated[ StrictBool | None, Field( description='Include full conversation history for each task (may significantly increase response size)' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/list_tasks_response.py b/src/adcp/types/domains/protocol/list_tasks_response.py index 53bcf2b51..483a0bd97 100644 --- a/src/adcp/types/domains/protocol/list_tasks_response.py +++ b/src/adcp/types/domains/protocol/list_tasks_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/list_tasks_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -28,12 +28,17 @@ class DomainBreakdown(AdCPBaseModel): media_buy: Annotated[ SchemaInt | None, Field(alias='media-buy', description='Number of media-buy tasks in results', ge=0), + NoExplicitNull, ] = None signals: Annotated[ - SchemaInt | None, Field(description='Number of signals tasks in results', ge=0) + SchemaInt | None, + Field(description='Number of signals tasks in results', ge=0), + NoExplicitNull, ] = None creative: Annotated[ - SchemaInt | None, Field(description='Number of creative tasks in results', ge=0) + SchemaInt | None, + Field(description='Number of creative tasks in results', ge=0), + NoExplicitNull, ] = None @@ -57,21 +62,26 @@ class QuerySummary(AdCPBaseModel): total_matching: Annotated[ SchemaInt | None, Field(description='Total number of tasks matching filters (across all pages)', ge=0), + NoExplicitNull, ] = None returned: Annotated[ - SchemaInt | None, Field(description='Number of tasks returned in this response', ge=0) + SchemaInt | None, + Field(description='Number of tasks returned in this response', ge=0), + NoExplicitNull, ] = None domain_breakdown: Annotated[ - DomainBreakdown | None, Field(description='Count of tasks by domain') + DomainBreakdown | None, Field(description='Count of tasks by domain'), NoExplicitNull ] = None status_breakdown: Annotated[ - dict[str, SchemaInt] | None, Field(description='Count of tasks by status') + dict[str, SchemaInt] | None, Field(description='Count of tasks by status'), NoExplicitNull ] = None filters_applied: Annotated[ - list[str] | None, Field(description='List of filters that were applied to the query') + list[str] | None, + Field(description='List of filters that were applied to the query'), + NoExplicitNull, ] = None sort_applied: Annotated[ - SortApplied | None, Field(description='Sort order that was applied') + SortApplied | None, Field(description='Sort order that was applied'), NoExplicitNull ] = None @@ -100,9 +110,12 @@ class Task(AdCPBaseModel): Field( description='When the task completed (ISO 8601, only for completed/failed/canceled tasks)' ), + NoExplicitNull, ] = None has_webhook: Annotated[ - StrictBool | None, Field(description='Whether this task has webhook configuration') + StrictBool | None, + Field(description='Whether this task has webhook configuration'), + NoExplicitNull, ] = None @@ -115,5 +128,5 @@ class ListTasksResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): ] tasks: Annotated[list[Task], Field(description='Array of tasks matching the query criteria')] pagination: pagination_response.PaginationResponse - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/sync_agent_notification_configs_request.py b/src/adcp/types/domains/protocol/sync_agent_notification_configs_request.py index cd9186358..8085fcec4 100644 --- a/src/adcp/types/domains/protocol/sync_agent_notification_configs_request.py +++ b/src/adcp/types/domains/protocol/sync_agent_notification_configs_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/sync_agent_notification_configs_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import agent_notification_config @@ -41,6 +42,7 @@ class SyncAgentNotificationConfigsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='When true, validate the replacement and report what would be persisted without applying it or sending endpoint proof-of-control challenges.' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/sync_agent_notification_configs_response.py b/src/adcp/types/domains/protocol/sync_agent_notification_configs_response.py index f29354949..67fbcac0f 100644 --- a/src/adcp/types/domains/protocol/sync_agent_notification_configs_response.py +++ b/src/adcp/types/domains/protocol/sync_agent_notification_configs_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/sync_agent_notification_configs_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import agent_notification_config @@ -33,6 +34,7 @@ class SyncAgentNotificationConfigsResponse(AdcpResponse, AdcpVersionEnvelope, Pr dry_run: Annotated[ StrictBool | None, Field(description='Whether this was a dry run and no persisted subscriber set changed.'), + NoExplicitNull, ] = None action: Annotated[ Action, @@ -46,12 +48,14 @@ class SyncAgentNotificationConfigsResponse(AdcpResponse, AdcpVersionEnvelope, Pr description='Current persisted agent-level notification subscribers owned by the authenticated caller after the request. Entries are keyed by `subscriber_id`; `authentication.credentials` is omitted on every entry because credentials are write-only. Present on successful actions and MAY be present on `failed` responses to show the unchanged prior caller-scoped set.', max_length=16, ), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Operation-level errors and warnings. Present when action is `failed`, and MAY include non-fatal warnings on successful responses.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/sync_principal_request.py b/src/adcp/types/domains/protocol/sync_principal_request.py index dd4d850c0..b6d09c524 100644 --- a/src/adcp/types/domains/protocol/sync_principal_request.py +++ b/src/adcp/types/domains/protocol/sync_principal_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/sync_principal_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import agent_notification_config, agent_reporting_destination @@ -29,6 +29,7 @@ class Configuration(AdCPBaseModel): description='Complete desired agent-level subscriber set. The same caller-scoping, proof-of-control, secret handling, and replacement rules as sync_agent_notification_configs apply.', max_length=16, ), + NoExplicitNull, ] = None reporting_destinations: Annotated[ list[agent_reporting_destination.AgentReportingDestination] | None, @@ -36,12 +37,14 @@ class Configuration(AdCPBaseModel): description='Complete desired reusable reporting destination set. Omitting a previously present destination_id revokes it, and [] revokes every destination: the seller halts new deliveries to all of its generations within the advertised suspension_interval_seconds and retains it as a retired generation for reporting history. Revocation does not delete caller-owned data already delivered. destination_id values MUST be unique.', max_length=64, ), + NoExplicitNull, ] = None declarations: Annotated[ principal_declarations.AgentDeclarations | None, Field( description='Complete declared consumption facts for this principal record. A present object replaces the declared set wholesale; {} clears it; omission leaves it unchanged. The seller computes and returns the accepted intersection in state.' ), + NoExplicitNull, ] = None @@ -65,12 +68,14 @@ class SyncPrincipalRequest(AdcpRequest, AdcpVersionEnvelope): max_length=255, min_length=1, ), + NoExplicitNull, ] = None expected_principal_kind: Annotated[ principal_kind.PrincipalKind | None, Field( description='Optional assertion fence, not identity input: the caller states which party kind it believes it is authenticating as. When present and different from the seller-resolved principal_kind, the seller rejects the whole request with CONFLICT before mutation. The field never influences resolution.' ), + NoExplicitNull, ] = None configuration: Annotated[ Configuration, @@ -83,6 +88,7 @@ class SyncPrincipalRequest(AdcpRequest, AdcpVersionEnvelope): Field( description='Validate the proposed replacements and report the would-be action without persisting them, issuing durable identifiers or grants, or sending endpoint proof challenges.' ), + NoExplicitNull, ] = False - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/protocol/sync_principal_response.py b/src/adcp/types/domains/protocol/sync_principal_response.py index 126ac711e..44bdb696b 100644 --- a/src/adcp/types/domains/protocol/sync_principal_response.py +++ b/src/adcp/types/domains/protocol/sync_principal_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/sync_principal_response.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -40,7 +40,7 @@ class Result(AdCPBaseModel): kind: Literal['validated'] = 'validated' action: Action33 dry_run: Literal[True] - warnings: Annotated[list[error.Error] | None, Field(max_length=16)] = None + warnings: Annotated[list[error.Error] | None, Field(max_length=16), NoExplicitNull] = None class Result15(AdCPBaseModel): @@ -86,7 +86,7 @@ class Result13(AdCPBaseModel): ), ] configuration: principal_state.PrincipalState - warnings: Annotated[list[error.Error] | None, Field(max_length=16)] = None + warnings: Annotated[list[error.Error] | None, Field(max_length=16), NoExplicitNull] = None class SyncPrincipalResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): @@ -94,8 +94,8 @@ class SyncPrincipalResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) extra='allow', ) result: Result13 | Result | Result15 - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None # Stable aliases for anonymous result arms (selected by discriminator). diff --git a/src/adcp/types/domains/registries/v1_canonical_mapping.py b/src/adcp/types/domains/registries/v1_canonical_mapping.py index 3aeeb020f..e6917f58a 100644 --- a/src/adcp/types/domains/registries/v1_canonical_mapping.py +++ b/src/adcp/types/domains/registries/v1_canonical_mapping.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: registries/v1_canonical_mapping.json -# timestamp: 2026-10-05T13:49:50+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -22,8 +22,8 @@ class V1Pattern(AdCPBaseModel): class Dimensions(AdCPBaseModel): - width: SchemaInt | None = None - height: SchemaInt | None = None + width: Annotated[SchemaInt | None, NoExplicitNull] = None + height: Annotated[SchemaInt | None, NoExplicitNull] = None class Structural(AdCPBaseModel): @@ -35,13 +35,15 @@ class Structural(AdCPBaseModel): Field( description="Set of asset_type values that must appear in the format's slots (in any order, any count)." ), + NoExplicitNull, ] = None vast_versions: Annotated[ list[str] | None, Field(description="VAST version constraints. Strings like '>=4.0', '4.x', '4.2'."), + NoExplicitNull, ] = None - daast_versions: list[str] | None = None - dimensions: Dimensions | None = None + daast_versions: Annotated[list[str] | None, NoExplicitNull] = None + dimensions: Annotated[Dimensions | None, NoExplicitNull] = None class V1Pattern1(AdCPBaseModel): @@ -73,6 +75,7 @@ class ParameterMapping(AdCPBaseModel): Field( description='Value transform applied during forwarding. identity copies the value; singleton_array wraps it in a one-element array.' ), + NoExplicitNull, ] = Transform.identity @@ -83,6 +86,7 @@ class V2(AdCPBaseModel): Field( description='Optional parameters that narrow the canonical (e.g., width/height, vast_version). When present, become the params on the projected v2 ProductFormatDeclaration. The shape MUST be valid params for the named canonical.' ), + NoExplicitNull, ] = None parameter_mappings: Annotated[ list[ParameterMapping] | None, @@ -90,6 +94,7 @@ class V2(AdCPBaseModel): description='Machine-readable forwarding rules for parameters carried by a structured v1 format_id. SDKs copy each present source_field to the target_parameter after applying transform; absent source fields leave the target parameter omitted. Static v2.parameters are applied before these forwarded values.', min_length=1, ), + NoExplicitNull, ] = None @@ -104,10 +109,12 @@ class Mapping(AdCPBaseModel): Field( description='When true, this mapping is retained for backward-compatibility but should not be used for new mappings. SDKs SHOULD emit lint warnings when matching a deprecated entry.' ), + NoExplicitNull, ] = False notes: Annotated[ str | None, Field(description='Optional human-readable explanation, examples, or rationale.'), + NoExplicitNull, ] = None @@ -116,7 +123,7 @@ class V1V2CanonicalFormatMappingRegistry(AdCPBaseModel): str, Field(description='Semver of this registry. Bumped on every published change.') ] last_updated: Annotated[ - date | None, Field(description='ISO date of the last published change.') + date | None, Field(description='ISO date of the last published change.'), NoExplicitNull ] = None mappings: Annotated[ list[Mapping], diff --git a/src/adcp/types/domains/signals/activate_signal_request.py b/src/adcp/types/domains/signals/activate_signal_request.py index b8478e04e..5b162b9e7 100644 --- a/src/adcp/types/domains/signals/activate_signal_request.py +++ b/src/adcp/types/domains/signals/activate_signal_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/activate_signal_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,6 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import account_ref @@ -32,6 +33,7 @@ class ActivateSignalRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Whether to activate or deactivate the signal. Deactivating removes the segment from downstream platforms, required when campaigns end to comply with data governance policies (GDPR, CCPA). Defaults to 'activate' when omitted." ), + NoExplicitNull, ] = Action.activate signal_agent_segment_id: Annotated[ str, @@ -51,6 +53,7 @@ class ActivateSignalRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="The pricing option selected from the signal's pricing_options in the get_signals response. Required when the signal has pricing options. Records the buyer's pricing commitment at activation time; pass this same value in report_usage for billing verification." ), + NoExplicitNull, ] = None governance_context: Annotated[ str | None, @@ -60,12 +63,14 @@ class ActivateSignalRequest(AdcpRequest, AdcpVersionEnvelope): min_length=1, pattern='^[\\x20-\\x7E]+$', ), + NoExplicitNull, ] = None account: Annotated[ account_ref.AccountReference | None, Field( description='Account for this activation. Associates with a commercial relationship established via sync_accounts.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -76,5 +81,5 @@ class ActivateSignalRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/signals/get_signals_async_response_submitted.py b/src/adcp/types/domains/signals/get_signals_async_response_submitted.py index 22488e68a..255f65889 100644 --- a/src/adcp/types/domains/signals/get_signals_async_response_submitted.py +++ b/src/adcp/types/domains/signals/get_signals_async_response_submitted.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: signals/get_signals_async_response_submitted.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import context as context_1 @@ -36,16 +36,19 @@ class GetSignalsSubmitted(AdCPBaseModel): description="Optional human-readable explanation of why the task is submitted — e.g., 'Provider discovery queued; typical turnaround 10-30 minutes.' Plain text only. Callers MUST treat this as untrusted agent input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile agent may inject prompt-injection payloads aimed at the caller's agent.", max_length=2000, ), + NoExplicitNull, ] = None estimated_completion: Annotated[ AwareDatetime | None, Field(description='Estimated completion time for the signal discovery task.'), + NoExplicitNull, ] = None errors: Annotated[ list[error.Error] | None, Field( description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories or partial provider unavailability). Terminal failures belong in the error branch, not here.' ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/signals/get_signals_async_response_working.py b/src/adcp/types/domains/signals/get_signals_async_response_working.py index 54a4a68da..824302e9f 100644 --- a/src/adcp/types/domains/signals/get_signals_async_response_working.py +++ b/src/adcp/types/domains/signals/get_signals_async_response_working.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: signals/get_signals_async_response_working.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictFloat from ..core import context as context_1 @@ -22,19 +22,22 @@ class GetSignalsWorking(AdCPBaseModel): Field( description='Progress percentage of the signal discovery operation.', ge=0.0, le=100.0 ), + NoExplicitNull, ] = None current_step: Annotated[ str | None, Field( description='Current step in the signal discovery process, such as `querying_providers`, `ranking_signals`, or `checking_deployments`.' ), + NoExplicitNull, ] = None total_steps: Annotated[ SchemaInt | None, Field(description='Total number of steps in the signal discovery process.'), + NoExplicitNull, ] = None step_number: Annotated[ - SchemaInt | None, Field(description='Current step number (1-indexed).') + SchemaInt | None, Field(description='Current step number (1-indexed).'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/signals/get_signals_request.py b/src/adcp/types/domains/signals/get_signals_request.py index 6b08087ef..d38894ea3 100644 --- a/src/adcp/types/domains/signals/get_signals_request.py +++ b/src/adcp/types/domains/signals/get_signals_request.py @@ -1,16 +1,16 @@ # generated by datamodel-codegen: # filename: signals/get_signals_request.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types.base import AdcpRequest, SchemaInt -from adcp.types._scalar import ScalarStr - from adcp.types._str_enum import StrEnum from typing import Annotated +from adcp.types.base import NoExplicitNull +from adcp.types._scalar import ScalarStr from pydantic import ConfigDict, Field from ..core import account_ref @@ -79,18 +79,21 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Declares caller intent for this request. 'brief' (default): semantic discovery — signal_spec, signal_refs, or legacy signal_ids is required and the agent performs inference/RAG. 'wholesale': raw wholesale signals feed enumeration — signal_spec, signal_refs, and signal_ids MUST NOT be provided and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries when present. Sellers receiving requests from pre-v3.1 clients without discovery_mode MUST default to 'brief'. Timing semantics: 'wholesale' is a wholesale signals feed read — agents SHOULD respond synchronously and MUST NOT route a 'wholesale' request through the async/Submitted arm; partial completion is signalled via the response's incomplete[] field, not via a task-handoff envelope. Agents that do not implement wholesale enumeration MAY return INVALID_REQUEST for wholesale calls; callers SHOULD probe via get_adcp_capabilities (signals.discovery_modes) first." ), + NoExplicitNull, ] = DiscoveryMode.brief account: Annotated[ account_ref.AccountReference | None, Field( description="Account for this request. When provided, the signals agent returns per-account pricing options if configured. In 'wholesale' mode, this is the rate-card scope: when omitted in wholesale mode, agents return their default rate-card pricing or omit pricing_options entirely. A reference that does not resolve returns ACCOUNT_NOT_FOUND rather than default pricing, unless the seller provisions lazily. See /docs/accounts/overview#account-references-before-provisioning." ), + NoExplicitNull, ] = None signal_spec: Annotated[ str | None, Field( description="Natural language description of the desired signals. When used alone, enables semantic discovery. When combined with signal_refs, provides context for the agent but signal_ref matches are returned first. MUST NOT be provided when discovery_mode is 'wholesale'." ), + NoExplicitNull, ] = None signal_refs: Annotated[ list[signal_ref_1.SignalRef] | None, @@ -98,6 +101,7 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): description="Specific signals to look up by reference. Returns exact matches for the requested SignalRef values. When combined with signal_spec, these signals anchor the starting set and signal_spec guides adjustments. MUST NOT be provided when discovery_mode is 'wholesale'.", min_length=1, ), + NoExplicitNull, ] = None signal_ids: Annotated[ list[signal_id_1.SignalId] | None, @@ -106,6 +110,7 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): description="DEPRECATED. Use signal_refs instead. Legacy exact lookup field using SignalId objects. MUST NOT be provided when discovery_mode is 'wholesale'.", min_length=1, ), + NoExplicitNull, ] = None destinations: Annotated[ list[destination.Destination] | None, @@ -113,6 +118,7 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): description='Filter signals to those activatable on specific agents/platforms. When omitted, returns all signals available on the current agent. If the authenticated caller matches one of these destinations, activation keys will be included in the response.', min_length=1, ), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -120,14 +126,16 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): description='Countries where signals will be used (ISO 3166-1 alpha-2 codes). When omitted, no geographic filter is applied.', min_length=1, ), + NoExplicitNull, ] = None - filters: signal_filters.SignalFilters | None = None + filters: Annotated[signal_filters.SignalFilters | None, NoExplicitNull] = None fields: Annotated[ list[Field1] | None, Field( description="Specific signal fields to include in the response, aligned with get_products.fields. Required identity and activation fields such as signal_ref or signal_id, signal_agent_segment_id, name, description, signal_type, coverage_percentage, and deployments are always included when required by the response schema. Use for progressive disclosure of rich signal-definition metadata: request fields such as demographic_predicate, taxonomy, data_sources, methodology, segmentation_criteria, criteria_url, refresh_cadence, lookback_window, onboarder, modeling, audience_expansion, device_expansion, countries, consent_basis, restricted_attributes, policy_categories, art9_basis, data_subject_rights, and last_updated when the buyer needs them inline. Omit for the agent's default discovery projection. Agents SHOULD honor requested fields for exact lookup, refinement, small custom-signal result sets, and private/source-native signals when available. fields is a projection request, not an entitlement grant; agents MAY redact requested definition fields unless the caller is authorized for the underlying lineage, methodology, and rights-routing metadata. When demographic_predicate, consent_basis, or art9_basis is projected for another provider's signal, the value remains provider-declared signal-definition posture; sellers and federating agents MUST NOT substitute their own semantics or processing basis. For broad discovery and wholesale pages, agents MAY return compact pointers instead of inlining large resources, especially when provider-published definitions can be resolved from signal_ref, taxonomy.ref, criteria_url, disclosure_url, and validators such as resolved URL plus catalog_etag, HTTP ETag/Last-Modified, or taxonomy.etag.", min_length=1, ), + NoExplicitNull, ] = None max_results: Annotated[ SchemaInt | None, @@ -136,30 +144,35 @@ class GetSignalsRequest(AdcpRequest, AdcpVersionEnvelope): description='DEPRECATED: Use pagination.max_results instead. When both fields are present, agents MUST honor pagination.max_results. When only this field is present without a pagination envelope, agents SHOULD treat it as the page size subject to a maximum of 100 results. This field will be removed in AdCP 4.0.', ge=1, ), + NoExplicitNull, ] = None pagination: Annotated[ pagination_request.PaginationRequest | None, Field( description='Pagination parameters. Use pagination.max_results (max: 100, default: 50) and pagination.cursor for cursor-based page walks. When the deprecated top-level max_results field is also present, pagination.max_results takes precedence.' ), + NoExplicitNull, ] = None push_notification_config: Annotated[ push_notification_config_1.PushNotificationConfig | None, Field( description='Optional webhook configuration for async terminal completion/failure notifications on semantic signal discovery. Meaningful only for `discovery_mode: "brief"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: agents MUST NOT route `discovery_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.' ), + NoExplicitNull, ] = None if_wholesale_feed_version: Annotated[ str | None, Field( description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_signals response from this agent. Only valid when discovery_mode is wholesale. When provided, the agent compares against its current wholesale signals feed version for the caller's cache_scope and MAY return an unchanged: true response (with signals omitted) if nothing has changed. The token is scope-keyed: callers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full sync pattern." ), + NoExplicitNull, ] = None if_pricing_version: Annotated[ str | None, Field( description="Opaque pricing_version token from a prior get_signals response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → agent returns the full payload; (2) if_wholesale_feed_version matches but if_pricing_version mismatches → agent returns the full payload so the caller sees updated pricing_options; (3) both match → agent MAY return unchanged: true. Agents that don't track pricing separately ignore this and fall back to if_wholesale_feed_version semantics." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/signals/get_signals_response.py b/src/adcp/types/domains/signals/get_signals_response.py index 9b8e1ff2d..0186bd4e7 100644 --- a/src/adcp/types/domains/signals/get_signals_response.py +++ b/src/adcp/types/domains/signals/get_signals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: signals/get_signals_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:25:44+00:00 from __future__ import annotations @@ -10,7 +10,7 @@ from collections.abc import Sequence from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, StrictBool, StrictFloat @@ -42,8 +42,8 @@ class Value(AdCPBaseModel): extra='forbid', ) id: Annotated[str, Field(min_length=1)] - path: str | None = None - modifiers: list[str] | None = None + path: Annotated[str | None, NoExplicitNull] = None + modifiers: Annotated[list[str] | None, NoExplicitNull] = None class ValueMapping(AdCPBaseModel): @@ -52,8 +52,8 @@ class ValueMapping(AdCPBaseModel): ) value: str taxonomy_value_id: str - path: str | None = None - modifiers: list[str] | None = None + path: Annotated[str | None, NoExplicitNull] = None + modifiers: Annotated[list[str] | None, NoExplicitNull] = None class ParentMatchBehavior(StrEnum): @@ -67,12 +67,12 @@ class Taxonomy(AdCPBaseModel): extra='forbid', ) ref: AnyUrl - version: str | None = None - segtax: Annotated[SchemaInt | None, Field(ge=1)] = None - etag: str | None = None + version: Annotated[str | None, NoExplicitNull] = None + segtax: Annotated[SchemaInt | None, Field(ge=1), NoExplicitNull] = None + etag: Annotated[str | None, NoExplicitNull] = None values: Annotated[list[Value], Field(min_length=1)] - value_mappings: Annotated[list[ValueMapping] | None, Field(min_length=1)] = None - parent_match_behavior: ParentMatchBehavior | None = None + value_mappings: Annotated[list[ValueMapping] | None, Field(min_length=1), NoExplicitNull] = None + parent_match_behavior: Annotated[ParentMatchBehavior | None, NoExplicitNull] = None class DataSource(StrEnum): @@ -139,9 +139,11 @@ class Onboarder(AdCPBaseModel): extra='forbid', ) match_keys: Annotated[list[MatchKey], Field(min_length=1)] - pre_onboarding_audience_expansion: StrictBool | None = None - pre_onboarding_device_expansion: StrictBool | None = None - pre_onboarding_precision_level: PreOnboardingPrecisionLevel | None = None + pre_onboarding_audience_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_device_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + pre_onboarding_precision_level: Annotated[ + PreOnboardingPrecisionLevel | None, NoExplicitNull + ] = None class Country(ScalarStr): @@ -207,10 +209,10 @@ class Channel(AdCPBaseModel): extra='forbid', ) rights: Annotated[list[Right], Field(min_length=1)] - url: AnyUrl | None = None - email: EmailStr | None = None - languages: list[str] | None = None - countries: list[Country] | None = None + url: Annotated[AnyUrl | None, NoExplicitNull] = None + email: Annotated[EmailStr | None, NoExplicitNull] = None + languages: Annotated[list[str] | None, NoExplicitNull] = None + countries: Annotated[list[Country] | None, NoExplicitNull] = None class DataSubjectRights(AdCPBaseModel): @@ -223,10 +225,11 @@ class DataSubjectRights(AdCPBaseModel): max_length=253, pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]{0,61}[A-Za-z0-9])$', ), + NoExplicitNull, ] = None channels: Annotated[list[Channel], Field(min_length=1)] - response_sla_days: Annotated[SchemaInt | None, Field(ge=1, le=90)] = None - ccpa_opt_out_url: AnyUrl | None = None + response_sla_days: Annotated[SchemaInt | None, Field(ge=1, le=90), NoExplicitNull] = None + ccpa_opt_out_url: Annotated[AnyUrl | None, NoExplicitNull] = None class Scope(StrEnum): @@ -253,6 +256,7 @@ class IncompleteItem(AdCPBaseModel): Field( description='How much additional time would resolve this scope. Allows the caller to decide whether to retry with a larger time_budget.' ), + NoExplicitNull, ] = None @@ -269,7 +273,9 @@ class Modeling(AdCPBaseModel): seed_source: SeedSource training_data_jurisdictions: Annotated[list[TrainingDataJurisdiction], Field(min_length=1)] ai_act_risk_class: AiActRiskClass - disclosure: signal_modeling_disclosure.SignalModelingDisclosure | None = None + disclosure: Annotated[ + signal_modeling_disclosure.SignalModelingDisclosure | None, NoExplicitNull + ] = None class Signal(AdCPBaseModel): @@ -282,12 +288,14 @@ class Signal(AdCPBaseModel): deprecated=True, description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.', ), + NoExplicitNull, ] = None signal_ref: Annotated[ signal_ref_1.SignalRef | None, Field( description="Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined in a data provider's published adagents.json signals[]; use scope 'signal_source' with signal_source_url for a source-native signal." ), + NoExplicitNull, ] = None signal_agent_segment_id: Annotated[ str, @@ -312,6 +320,7 @@ class Signal(AdCPBaseModel): Field( description="The data type of this signal's values. Required when signal_ref_1.scope is 'product'." ), + NoExplicitNull, ] = None categories: Annotated[ list[str] | None, @@ -319,10 +328,12 @@ class Signal(AdCPBaseModel): description="Valid values for categorical signals. Present when value_type is 'categorical'.", min_length=1, ), + NoExplicitNull, ] = None range: Annotated[ Range | None, Field(description="Valid range for numeric signals. Present when value_type is 'numeric'."), + NoExplicitNull, ] = None signal_type: Annotated[ signal_catalog_type.SignalAvailabilityType, @@ -333,6 +344,7 @@ class Signal(AdCPBaseModel): Field( description='Human-readable source name for the signal, when applicable. For data_provider-scoped signals this is the data provider name; for signal_source-scoped signals it may identify the signal source or proprietary origin.' ), + NoExplicitNull, ] = None coverage_percentage: Annotated[ StrictFloat | None, @@ -342,12 +354,14 @@ class Signal(AdCPBaseModel): ge=0.0, le=100.0, ), + NoExplicitNull, ] = None coverage_forecast: Annotated[ signal_coverage_forecast.SignalCoverageForecast | None, Field( description='Optional forecast-shaped signal availability guidance. When present, this is authoritative for signal-level discovery coverage. Use this to disclose the denominator, bucket semantics, not-present bucket, aggregate present bucket, and per-value coverage distribution for the signal.' ), + NoExplicitNull, ] = None deployments: Annotated[ Sequence[deployment.Deployment], Field(description='Array of deployment targets') @@ -358,82 +372,95 @@ class Signal(AdCPBaseModel): description='Pricing options available for this signal when it has an incremental price. The buyer selects one and passes its pricing_option_id in report_usage or package-level signal_targeting_groups for billing verification. Omit when pricing is unavailable to the caller, bundled into the destination product, or has no incremental cost.', min_length=1, ), + NoExplicitNull, ] = None methodology_url: Annotated[ AnyUrl | None, Field( description='Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition.' ), + NoExplicitNull, ] = None last_updated: Annotated[ AwareDatetime | None, Field( description='When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.' ), + NoExplicitNull, ] = None restricted_attributes: Annotated[ list[restricted_attribute.RestrictedAttribute] | None, Field(description='Restricted attribute categories this signal touches.', min_length=1), + NoExplicitNull, ] = None demographic_predicate: Annotated[ demographic_predicate_1.DemographicPredicate | None, Field( description="Projected authoritative demographic meaning for the signal. When projected from another provider, this MUST match the provider's definition exactly. Signal names alone never establish demographic semantics." ), + NoExplicitNull, ] = None policy_categories: Annotated[ list[str] | None, Field(description='Policy categories this signal is sensitive for.', min_length=1), + NoExplicitNull, ] = None taxonomy: Annotated[ Taxonomy | None, Field( description='Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy.' ), + NoExplicitNull, ] = None - segmentation_criteria: Annotated[str | None, Field(max_length=500)] = None - criteria_url: AnyUrl | None = None - data_sources: Annotated[list[DataSource] | None, Field(min_length=1)] = None - methodology: Methodology | None = None - audience_expansion: StrictBool | None = None - device_expansion: StrictBool | None = None - refresh_cadence: RefreshCadence | None = None - lookback_window: RefreshCadence | None = None - onboarder: Onboarder | None = None - countries: Annotated[list[Country] | None, Field(min_length=1)] = None + segmentation_criteria: Annotated[str | None, Field(max_length=500), NoExplicitNull] = None + criteria_url: Annotated[AnyUrl | None, NoExplicitNull] = None + data_sources: Annotated[list[DataSource] | None, Field(min_length=1), NoExplicitNull] = None + methodology: Annotated[Methodology | None, NoExplicitNull] = None + audience_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + device_expansion: Annotated[StrictBool | None, NoExplicitNull] = None + refresh_cadence: Annotated[RefreshCadence | None, NoExplicitNull] = None + lookback_window: Annotated[RefreshCadence | None, NoExplicitNull] = None + onboarder: Annotated[Onboarder | None, NoExplicitNull] = None + countries: Annotated[list[Country] | None, Field(min_length=1), NoExplicitNull] = None consent_basis: Annotated[ list[consent_basis_1.ConsentBasis] | None, Field( description="Data provider's declared GDPR Article 6 lawful basis or consent basis for the underlying signal definition, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own processing basis for the provider-declared basis.", min_length=1, ), + NoExplicitNull, ] = None art9_basis: Annotated[ Art9Basis | None, Field( description="Data provider's declared GDPR Article 9 basis for the underlying signal definition when special-category data is involved and Article 9 applies, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own Article 9 basis for the provider-declared basis." ), + NoExplicitNull, ] = None - modeling: Modeling | None = None + modeling: Annotated[Modeling | None, NoExplicitNull] = None data_subject_rights: Annotated[ DataSubjectRights | None, Field( description='Per-signal data-subject-rights routing. This is a contact/routing reference, not a machine-callable AdCP API.' ), + NoExplicitNull, ] = None - dts_compliant_version: str | None = None + dts_compliant_version: Annotated[str | None, NoExplicitNull] = None class GetSignalsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict( extra='allow', ) - signals: Annotated[Sequence[Signal] | None, Field(description='Array of matching signals')] = None + signals: Annotated[ + Sequence[Signal] | None, Field(description='Array of matching signals'), NoExplicitNull + ] = None errors: Annotated[ list[error.Error] | None, Field( description='Task-specific errors and warnings (e.g., signal discovery or pricing issues)' ), + NoExplicitNull, ] = None incomplete: Annotated[ list[IncompleteItem] | None, @@ -441,35 +468,41 @@ class GetSignalsResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): description="Declares what the agent could not finish within the caller's time_budget or due to internal limits. Each entry identifies a scope that is missing or partial. Absent when the response is fully complete.", min_length=1, ), + NoExplicitNull, ] = None wholesale_feed_version: Annotated[ str | None, Field( description="Opaque token representing the version of the wholesale signals feed state used to compose this response. Agents that implement conditional-fetch (if_wholesale_feed_version) MUST return this on every wholesale-mode response so callers can cache and probe later. Callers MUST treat the value as opaque — no format, no ordering, no inspection. The token is scope-keyed: it describes a version for the cache_scope declared on this response, NOT a global agent version. A caller caches `(cache_scope, wholesale_feed_version)` pairs and presents the matching token on the next request. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full cache layering model." ), + NoExplicitNull, ] = None pricing_version: Annotated[ str | None, Field( description='Opaque token representing the version of the pricing layer. When the agent supports independent pricing versioning, pricing_version changes when prices move but wholesale_feed_version changes only when structure/metadata moves. Same cache_scope keying as wholesale_feed_version. Agents not separating these MAY omit pricing_version and use wholesale_feed_version for both.' ), + NoExplicitNull, ] = None cache_scope: Annotated[ CacheScope | None, Field( description="Declares whether the wholesale_feed_version and pricing_version on this response describe a universal layer or an account-specific overlay. REQUIRED on every 3.1+ response (the 3.1 schema enforces this — the safety property of the two-layer cache model depends on it). 'public': this response describes the agent's published rate card; the caller MAY dedupe under (agent, discovery_mode, filters, destinations, countries) without scoping by account. 'account': this response includes account-specific overrides; the caller MUST cache the version under that tuple plus account_id. When the request did NOT include `account`, the agent MUST return `cache_scope: 'public'`. When the request included `account`, the agent MUST return either 'public' (this account prices off the public rate card — caller dedupes) or 'account' (account-specific overrides exist — caller caches under the account key). Agents MAY return 'public' on an account-scoped request that previously had overrides — callers SHOULD interpret this as a downgrade. Without schema-required cache_scope, an agent silently omitting the field on an account-scoped response would cause callers to mis-key the cache and serve account-overlay payloads to other accounts — the canonical safety invariant of the entire cache layering model. **Backward-compatibility note for 3.1 validators:** SDKs validating strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version`. For responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed — pre-3.1 agents correctly emit no cache_scope and remain conformant to their declared version. This is a tightening within 3.1, not a 3.0 break." ), + NoExplicitNull, ] = CacheScope.public unchanged: Annotated[ Literal[True] | None, Field( description="Present and `true` ONLY on wholesale-mode responses when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the agent's current version for the caller's cache_scope, in which case signals[] MUST be omitted; wholesale_feed_version (echoed), cache_scope (echoed), and pricing_version (echoed when used) MUST still be present. Callers receiving unchanged: true MUST NOT mutate their local wholesale signals mirror. **One shape per state:** agents MUST NOT emit `unchanged: false` — the absence of the field IS the signal that the response carries signals. **Cross-scope isolation:** the comparator that decides `unchanged` MUST be keyed on `(cache_scope, wholesale_feed_version)`, not on the token value alone. An agent MUST NOT emit `unchanged: true` when it resolves the request to a different `cache_scope` than the one whose token the caller echoed in `if_wholesale_feed_version` (and/or `if_pricing_version`): because the token is scope-keyed, a value minted for `cache_scope: 'public'` cannot match the agent's current token for `cache_scope: 'account'` (or vice-versa), so such a request MUST return the full feed for the resolved scope with that scope's own token." ), + NoExplicitNull, ] = None - pagination: pagination_response.PaginationResponse | None = None + pagination: Annotated[pagination_response.PaginationResponse | None, NoExplicitNull] = None sandbox: Annotated[ StrictBool | None, Field(description='When true, this response contains simulated data from sandbox mode.'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_capabilities.py b/src/adcp/types/domains/sponsored_intelligence/si_capabilities.py index bf86e4c49..43b704b4c 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_capabilities.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_capabilities.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_capabilities.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StrictBool @@ -16,9 +16,11 @@ class Voice(AdCPBaseModel): extra='allow', ) provider: Annotated[ - str | None, Field(description='TTS provider (elevenlabs, openai, etc.)') + str | None, Field(description='TTS provider (elevenlabs, openai, etc.)'), NoExplicitNull ] = None - voice_id: Annotated[str | None, Field(description='Brand voice identifier')] = None + voice_id: Annotated[str | None, Field(description='Brand voice identifier'), NoExplicitNull] = ( + None + ) class Video(AdCPBaseModel): @@ -26,10 +28,12 @@ class Video(AdCPBaseModel): extra='allow', ) formats: Annotated[ - list[str] | None, Field(description='Supported video formats (mp4, webm, etc.)') + list[str] | None, + Field(description='Supported video formats (mp4, webm, etc.)'), + NoExplicitNull, ] = None max_duration_seconds: Annotated[ - SchemaInt | None, Field(description='Maximum video duration') + SchemaInt | None, Field(description='Maximum video duration'), NoExplicitNull ] = None @@ -38,9 +42,13 @@ class Avatar(AdCPBaseModel): extra='allow', ) provider: Annotated[ - str | None, Field(description='Avatar provider (d-id, heygen, synthesia, etc.)') + str | None, + Field(description='Avatar provider (d-id, heygen, synthesia, etc.)'), + NoExplicitNull, + ] = None + avatar_id: Annotated[ + str | None, Field(description='Brand avatar identifier'), NoExplicitNull ] = None - avatar_id: Annotated[str | None, Field(description='Brand avatar identifier')] = None class Modalities(AdCPBaseModel): @@ -48,16 +56,22 @@ class Modalities(AdCPBaseModel): extra='allow', ) conversational: Annotated[ - StrictBool | None, Field(description='Pure text exchange - the baseline modality') + StrictBool | None, + Field(description='Pure text exchange - the baseline modality'), + NoExplicitNull, ] = True voice: Annotated[ - StrictBool | Voice | None, Field(description='Audio-based interaction using brand voice') + StrictBool | Voice | None, + Field(description='Audio-based interaction using brand voice'), + NoExplicitNull, ] = None video: Annotated[ - StrictBool | Video | None, Field(description='Brand video content playback') + StrictBool | Video | None, Field(description='Brand video content playback'), NoExplicitNull ] = None avatar: Annotated[ - StrictBool | Avatar | None, Field(description='Animated video presence with brand avatar') + StrictBool | Avatar | None, + Field(description='Animated video presence with brand avatar'), + NoExplicitNull, ] = None @@ -77,10 +91,12 @@ class Components(AdCPBaseModel): standard: Annotated[ list[StandardEnum] | None, Field(description='Standard components that all SI hosts must render'), + NoExplicitNull, ] = None extensions: Annotated[ dict[str, Any] | None, Field(description='Platform-specific extensions (chatgpt_apps_sdk, maps, forms, etc.)'), + NoExplicitNull, ] = None @@ -91,6 +107,7 @@ class Commerce(AdCPBaseModel): acp_checkout: Annotated[ StrictBool | None, Field(description='Supports ACP (Agentic Commerce Protocol) checkout handoff'), + NoExplicitNull, ] = None @@ -99,11 +116,12 @@ class A2ui(AdCPBaseModel): extra='allow', ) supported: Annotated[ - StrictBool | None, Field(description='Supports A2UI surface rendering') + StrictBool | None, Field(description='Supports A2UI surface rendering'), NoExplicitNull ] = False catalogs: Annotated[ list[str] | None, Field(description="Supported A2UI component catalogs (e.g., 'si-standard', 'standard')"), + NoExplicitNull, ] = None @@ -112,14 +130,19 @@ class SiCapabilities(AdCPBaseModel): extra='allow', ) modalities: Annotated[ - Modalities | None, Field(description='Interaction modalities supported') + Modalities | None, Field(description='Interaction modalities supported'), NoExplicitNull + ] = None + components: Annotated[ + Components | None, Field(description='Visual components supported'), NoExplicitNull + ] = None + commerce: Annotated[ + Commerce | None, Field(description='Commerce capabilities'), NoExplicitNull + ] = None + a2ui: Annotated[ + A2ui | None, Field(description='A2UI (Agent-to-UI) capabilities'), NoExplicitNull ] = None - components: Annotated[Components | None, Field(description='Visual components supported')] = ( - None - ) - commerce: Annotated[Commerce | None, Field(description='Commerce capabilities')] = None - a2ui: Annotated[A2ui | None, Field(description='A2UI (Agent-to-UI) capabilities')] = None mcp_apps: Annotated[ StrictBool | None, Field(description='Supports MCP Apps for rendering A2UI surfaces in iframes'), + NoExplicitNull, ] = False diff --git a/src/adcp/types/domains/sponsored_intelligence/si_get_offering_request.py b/src/adcp/types/domains/sponsored_intelligence/si_get_offering_request.py index d7728d5fc..34faf3140 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_get_offering_request.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_get_offering_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_get_offering_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -27,13 +28,17 @@ class SiGetOfferingRequest(AdcpRequest, AdcpVersionEnvelope): Field( description="Optional natural language description of user intent for personalized results (e.g., 'mens size 14 near Cincinnati'). Must be anonymous - no PII." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None include_products: Annotated[ - StrictBool | None, Field(description='Whether to include matching products in the response') + StrictBool | None, + Field(description='Whether to include matching products in the response'), + NoExplicitNull, ] = False product_limit: Annotated[ SchemaInt | None, Field(description='Maximum number of matching products to return', ge=1, le=50), + NoExplicitNull, ] = 5 - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_get_offering_response.py b/src/adcp/types/domains/sponsored_intelligence/si_get_offering_response.py index 991dbce26..e09183a07 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_get_offering_response.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_get_offering_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_get_offering_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -24,24 +24,37 @@ class Offering(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - offering_id: Annotated[str | None, Field(description='Offering identifier')] = None - title: Annotated[str | None, Field(description='Offering title')] = None - summary: Annotated[str | None, Field(description='Brief summary of the offering')] = None - tagline: Annotated[str | None, Field(description='Short promotional tagline')] = None - expires_at: Annotated[AwareDatetime | None, Field(description='When this offering expires')] = ( + offering_id: Annotated[str | None, Field(description='Offering identifier'), NoExplicitNull] = ( None ) + title: Annotated[str | None, Field(description='Offering title'), NoExplicitNull] = None + summary: Annotated[ + str | None, Field(description='Brief summary of the offering'), NoExplicitNull + ] = None + tagline: Annotated[ + str | None, Field(description='Short promotional tagline'), NoExplicitNull + ] = None + expires_at: Annotated[ + AwareDatetime | None, Field(description='When this offering expires'), NoExplicitNull + ] = None availability_status: Annotated[ offering_availability_status.OfferingAvailabilityStatus | None, Field( description="Machine-readable availability state for the offering. Optional; when omitted, derive availability from the top-level 'available' boolean and 'unavailable_reason'." ), + NoExplicitNull, ] = None price_hint: Annotated[ - str | None, Field(description="Price indication (e.g., 'from $199', '50% off')") + str | None, + Field(description="Price indication (e.g., 'from $199', '50% off')"), + NoExplicitNull, ] = None - image_url: Annotated[AnyUrl | None, Field(description='Hero image for the offering')] = None - landing_url: Annotated[AnyUrl | None, Field(description='Landing page URL')] = None + image_url: Annotated[ + AnyUrl | None, Field(description='Hero image for the offering'), NoExplicitNull + ] = None + landing_url: Annotated[AnyUrl | None, Field(description='Landing page URL'), NoExplicitNull] = ( + None + ) class MatchingProduct(AdCPBaseModel): @@ -50,22 +63,30 @@ class MatchingProduct(AdCPBaseModel): ) product_id: Annotated[str, Field(description='Product identifier')] name: Annotated[str, Field(description='Product name')] - price: Annotated[str | None, Field(description="Display price (e.g., '$129', '$89.99')")] = None - original_price: Annotated[str | None, Field(description='Original price if on sale')] = None - image_url: Annotated[AnyUrl | None, Field(description='Product image')] = None + price: Annotated[ + str | None, Field(description="Display price (e.g., '$129', '$89.99')"), NoExplicitNull + ] = None + original_price: Annotated[ + str | None, Field(description='Original price if on sale'), NoExplicitNull + ] = None + image_url: Annotated[AnyUrl | None, Field(description='Product image'), NoExplicitNull] = None availability_summary: Annotated[ str | None, Field( description="Brief availability info (e.g., 'In stock', 'Size 14 available', '3 left')" ), + NoExplicitNull, ] = None availability_status: Annotated[ offering_availability_status.OfferingAvailabilityStatus | None, Field( description='Machine-readable availability state for this product. Structured counterpart to availability_summary.' ), + NoExplicitNull, ] = None - url: Annotated[AnyUrl | None, Field(description='Product detail page URL')] = None + url: Annotated[AnyUrl | None, Field(description='Product detail page URL'), NoExplicitNull] = ( + None + ) class SiGetOfferingResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): @@ -80,6 +101,7 @@ class SiGetOfferingResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) Field( description="Token to pass to si_initiate_session for session continuity. Brand stores the full query context server-side (products shown, order, context) so they can resolve references like 'the second one' when the session starts." ), + NoExplicitNull, ] = None ttl_seconds: Annotated[ SchemaInt | None, @@ -87,22 +109,29 @@ class SiGetOfferingResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) description='How long this offering information is valid (seconds). Host should re-fetch after TTL expires.', ge=0, ), + NoExplicitNull, ] = None checked_at: Annotated[ - AwareDatetime | None, Field(description='When this offering information was retrieved') + AwareDatetime | None, + Field(description='When this offering information was retrieved'), + NoExplicitNull, ] = None - offering: Annotated[Offering | None, Field(description='Offering details')] = None + offering: Annotated[Offering | None, Field(description='Offering details'), NoExplicitNull] = ( + None + ) matching_products: Annotated[ list[MatchingProduct] | None, Field( description='Products matching the request context. Only included if include_products was true.' ), + NoExplicitNull, ] = None sponsored_context: Annotated[ si_sponsored_context.SiSponsoredContext | None, Field( description='Declaration for the sponsored context carried by this offering response. When present, it applies to the returned offering and matching_products package as a whole unless a future extension narrows the declaration to individual items. Hosts MUST either honor the declared context_use and disclosure_obligation or reject the context before using it.' ), + NoExplicitNull, ] = None total_matching: Annotated[ SchemaInt | None, @@ -110,19 +139,22 @@ class SiGetOfferingResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) description='Total number of products matching the context (may be more than returned in matching_products)', ge=0, ), + NoExplicitNull, ] = None unavailable_reason: Annotated[ str | None, Field( description="If not available, why (e.g., 'expired', 'sold_out', 'region_restricted')" ), + NoExplicitNull, ] = None alternative_offering_ids: Annotated[ list[str] | None, Field(description='Alternative offerings to consider if this one is unavailable'), + NoExplicitNull, ] = None errors: Annotated[ - list[error.Error] | None, Field(description='Errors during offering lookup') + list[error.Error] | None, Field(description='Errors during offering lookup'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_identity.py b/src/adcp/types/domains/sponsored_intelligence/si_identity.py index 0dc750eac..a9ca5f49d 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_identity.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_identity.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_identity.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, StrictBool @@ -24,10 +24,10 @@ class PrivacyPolicyAcknowledged(AdCPBaseModel): extra='allow', ) brand_policy_url: Annotated[ - AnyUrl | None, Field(description="URL to brand's privacy policy") + AnyUrl | None, Field(description="URL to brand's privacy policy"), NoExplicitNull ] = None brand_policy_version: Annotated[ - str | None, Field(description='Version of policy acknowledged') + str | None, Field(description='Version of policy acknowledged'), NoExplicitNull ] = None @@ -35,23 +35,29 @@ class ShippingAddress(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - street: str | None = None - city: str | None = None - state: str | None = None - postal_code: str | None = None - country: str | None = None + street: Annotated[str | None, NoExplicitNull] = None + city: Annotated[str | None, NoExplicitNull] = None + state: Annotated[str | None, NoExplicitNull] = None + postal_code: Annotated[str | None, NoExplicitNull] = None + country: Annotated[str | None, NoExplicitNull] = None class User(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - email: Annotated[EmailStr | None, Field(description="User's email address")] = None - name: Annotated[str | None, Field(description="User's display name")] = None - locale: Annotated[str | None, Field(description="User's locale (e.g., en-US)")] = None - phone: Annotated[str | None, Field(description="User's phone number")] = None + email: Annotated[EmailStr | None, Field(description="User's email address"), NoExplicitNull] = ( + None + ) + name: Annotated[str | None, Field(description="User's display name"), NoExplicitNull] = None + locale: Annotated[ + str | None, Field(description="User's locale (e.g., en-US)"), NoExplicitNull + ] = None + phone: Annotated[str | None, Field(description="User's phone number"), NoExplicitNull] = None shipping_address: Annotated[ - ShippingAddress | None, Field(description="User's shipping address for accurate pricing") + ShippingAddress | None, + Field(description="User's shipping address for accurate pricing"), + NoExplicitNull, ] = None @@ -63,18 +69,27 @@ class SiIdentity(AdCPBaseModel): StrictBool, Field(description='Whether user consented to share identity') ] consent_timestamp: Annotated[ - AwareDatetime | None, Field(description='When consent was granted (ISO 8601)') + AwareDatetime | None, + Field(description='When consent was granted (ISO 8601)'), + NoExplicitNull, ] = None consent_scope: Annotated[ - list[ConsentScopeEnum] | None, Field(description='What data was consented to share') + list[ConsentScopeEnum] | None, + Field(description='What data was consented to share'), + NoExplicitNull, ] = None privacy_policy_acknowledged: Annotated[ - PrivacyPolicyAcknowledged | None, Field(description='Brand privacy policy acknowledgment') + PrivacyPolicyAcknowledged | None, + Field(description='Brand privacy policy acknowledgment'), + NoExplicitNull, ] = None user: Annotated[ - User | None, Field(description='User data (only present if consent_granted is true)') + User | None, + Field(description='User data (only present if consent_granted is true)'), + NoExplicitNull, ] = None anonymous_session_id: Annotated[ str | None, Field(description='Session ID for anonymous users (when consent_granted is false)'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_request.py b/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_request.py index 4a42ae51f..adb9dbbbe 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_request.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_initiate_session_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Annotated +from adcp.types.base import NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -26,35 +27,41 @@ class SiInitiateSessionRequest(AdcpRequest, AdcpVersionEnvelope): description='Natural language description of user intent — the conversation handoff from the host describing what the user needs from the brand agent' ), ] - context: context_1.ContextObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None identity: si_identity.SiIdentity media_buy_id: Annotated[ - str | None, Field(description='AdCP media buy ID if session was triggered by advertising') + str | None, + Field(description='AdCP media buy ID if session was triggered by advertising'), + NoExplicitNull, ] = None placement: Annotated[ str | None, Field( description="Where this session was triggered (e.g., 'chatgpt_search', 'claude_chat')" ), + NoExplicitNull, ] = None offering_id: Annotated[ - str | None, Field(description='Brand-specific offering identifier to apply') + str | None, Field(description='Brand-specific offering identifier to apply'), NoExplicitNull ] = None supported_capabilities: Annotated[ si_capabilities.SiCapabilities | None, Field(description='What capabilities the host supports'), + NoExplicitNull, ] = None offering_token: Annotated[ str | None, Field( description="Token from si_get_offering response for session continuity. Brand uses this to recall what products were shown to the user, enabling natural references like 'the second one' or 'that blue shoe'." ), + NoExplicitNull, ] = None sponsored_context_receipt: Annotated[ si_sponsored_context_receipt.SiSponsoredContextReceipt | None, Field( description='Host receipt for sponsored context accepted from a prior si_get_offering response or other pre-session context package. This records the accepted context_use, disclosure commitment, paying_principal, and host receipt for audit.' ), + NoExplicitNull, ] = None idempotency_key: Annotated[ str, @@ -65,4 +72,4 @@ class SiInitiateSessionRequest(AdcpRequest, AdcpVersionEnvelope): pattern='^[A-Za-z0-9_.:-]{16,255}$', ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_response.py b/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_response.py index aca8ffc4b..2663817a1 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_response.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_initiate_session_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_initiate_session_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -24,11 +24,13 @@ class Response(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - message: Annotated[str | None, Field(description='Conversational message from brand agent')] = ( - None - ) + message: Annotated[ + str | None, Field(description='Conversational message from brand agent'), NoExplicitNull + ] = None ui_elements: Annotated[ - list[si_ui_element.SiUiElement] | None, Field(description='Visual components to render') + list[si_ui_element.SiUiElement] | None, + Field(description='Visual components to render'), + NoExplicitNull, ] = None @@ -39,16 +41,20 @@ class SiInitiateSessionResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvel session_id: Annotated[ str, Field(description='Unique session identifier for subsequent messages') ] - response: Annotated[Response | None, Field(description="Brand agent's initial response")] = None + response: Annotated[ + Response | None, Field(description="Brand agent's initial response"), NoExplicitNull + ] = None negotiated_capabilities: Annotated[ si_capabilities.SiCapabilities | None, Field(description='Intersection of brand and host capabilities for this session'), + NoExplicitNull, ] = None sponsored_context: Annotated[ si_sponsored_context.SiSponsoredContext | None, Field( description='Declaration for sponsored context carried by the initial brand-agent response. Hosts MUST either honor the declared context_use and disclosure_obligation or reject the context before presenting, comparing, or otherwise using it.' ), + NoExplicitNull, ] = None session_status: Annotated[ si_session_status.SiSessionStatus, @@ -62,9 +68,12 @@ class SiInitiateSessionResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvel description='Session inactivity timeout in seconds. After this duration without a message, the brand agent may terminate the session. Hosts SHOULD warn users before timeout when possible.', ge=1, ), + NoExplicitNull, ] = None errors: Annotated[ - list[error.Error] | None, Field(description='Errors during session initiation') + list[error.Error] | None, + Field(description='Errors during session initiation'), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_send_message_request.py b/src/adcp/types/domains/sponsored_intelligence/si_send_message_request.py index 2dc9b8eba..947c3302a 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_send_message_request.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_send_message_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_send_message_request.json -# timestamp: 2026-10-07T15:06:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, model_validator from ..core import context as context_1 @@ -24,9 +24,11 @@ class ActionResponse(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - action: Annotated[str | None, Field(description='The action that was triggered')] = None + action: Annotated[ + str | None, Field(description='The action that was triggered'), NoExplicitNull + ] = None payload: Annotated[ - dict[str, Any] | None, Field(description='Action-specific response data') + dict[str, Any] | None, Field(description='Action-specific response data'), NoExplicitNull ] = None @@ -44,19 +46,23 @@ class SiSendMessageRequest(AdcpRequest, AdcpVersionEnvelope): ), ] session_id: Annotated[str, Field(description='Active session identifier')] - message: Annotated[str | None, Field(description="User's message to the brand agent")] = None + message: Annotated[ + str | None, Field(description="User's message to the brand agent"), NoExplicitNull + ] = None action_response: Annotated[ ActionResponse | None, Field(description='Response to a previous action_button (e.g., user clicked checkout)'), + NoExplicitNull, ] = None sponsored_context_receipt: Annotated[ si_sponsored_context_receipt.SiSponsoredContextReceipt | None, Field( description="Host receipt for sponsored context accepted from a prior SI response in this session. This gives the brand/seller an audit-visible record of the host's accepted use mode and disclosure commitment for that context." ), + NoExplicitNull, ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None @model_validator(mode='after') def _require_schema_required_group(self) -> SiSendMessageRequest: diff --git a/src/adcp/types/domains/sponsored_intelligence/si_send_message_response.py b/src/adcp/types/domains/sponsored_intelligence/si_send_message_response.py index 809393763..8da333d59 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_send_message_response.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_send_message_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_send_message_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat from ..a2ui import surface as surface_1 @@ -28,21 +28,21 @@ class Type(StrEnum): class Price(AdCPBaseModel): - amount: StrictFloat | None = None - currency: str | None = None + amount: Annotated[StrictFloat | None, NoExplicitNull] = None + currency: Annotated[str | None, NoExplicitNull] = None class Intent(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - action: Annotated[str | None, Field(description="The commerce action (e.g., 'purchase')")] = ( - None - ) - product: Annotated[dict[str, Any] | None, Field(description='Product details for checkout')] = ( - None - ) - price: Annotated[Price | None, Field(description='Price information')] = None + action: Annotated[ + str | None, Field(description="The commerce action (e.g., 'purchase')"), NoExplicitNull + ] = None + product: Annotated[ + dict[str, Any] | None, Field(description='Product details for checkout'), NoExplicitNull + ] = None + price: Annotated[Price | None, Field(description='Price information'), NoExplicitNull] = None class ContextForCheckout(AdCPBaseModel): @@ -50,10 +50,14 @@ class ContextForCheckout(AdCPBaseModel): extra='allow', ) conversation_summary: Annotated[ - str | None, Field(description='Summary of the conversation leading to purchase') + str | None, + Field(description='Summary of the conversation leading to purchase'), + NoExplicitNull, ] = None applied_offers: Annotated[ - list[str] | None, Field(description='Offer IDs that were applied during the conversation') + list[str] | None, + Field(description='Offer IDs that were applied during the conversation'), + NoExplicitNull, ] = None @@ -66,13 +70,17 @@ class Handoff(AdCPBaseModel): Field( description='Type of handoff: transaction (ready for ACP checkout) or complete (conversation done)' ), + NoExplicitNull, ] = None intent: Annotated[ Intent | None, Field(description='For transaction handoffs: what the user wants to purchase'), + NoExplicitNull, ] = None context_for_checkout: Annotated[ - ContextForCheckout | None, Field(description='Context to pass to ACP for seamless checkout') + ContextForCheckout | None, + Field(description='Context to pass to ACP for seamless checkout'), + NoExplicitNull, ] = None @@ -80,11 +88,13 @@ class Response(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - message: Annotated[str | None, Field(description='Conversational message from brand agent')] = ( - None - ) + message: Annotated[ + str | None, Field(description='Conversational message from brand agent'), NoExplicitNull + ] = None surface: Annotated[ - surface_1.A2UiSurface | None, Field(description='A2UI surface with interactive components') + surface_1.A2UiSurface | None, + Field(description='A2UI surface with interactive components'), + NoExplicitNull, ] = None ui_elements: Annotated[ list[si_ui_element.SiUiElement] | None, @@ -92,6 +102,7 @@ class Response(AdCPBaseModel): deprecated=True, description='Visual components to render (DEPRECATED: use surface instead)', ), + NoExplicitNull, ] = None @@ -100,18 +111,22 @@ class SiSendMessageResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) extra='allow', ) session_id: Annotated[str, Field(description='Session identifier')] - response: Annotated[Response | None, Field(description="Brand agent's response")] = None + response: Annotated[ + Response | None, Field(description="Brand agent's response"), NoExplicitNull + ] = None mcp_resource_uri: Annotated[ str | None, Field( description='MCP resource URI for hosts with MCP Apps support (e.g., ui://si/session-abc123)' ), + NoExplicitNull, ] = None sponsored_context: Annotated[ si_sponsored_context.SiSponsoredContext | None, Field( description='Declaration for sponsored context carried by this brand-agent response. Hosts MUST either honor the declared context_use and disclosure_obligation or reject the context before presenting, comparing, or otherwise using it.' ), + NoExplicitNull, ] = None session_status: Annotated[ si_session_status.SiSessionStatus, @@ -120,8 +135,10 @@ class SiSendMessageResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope) ), ] handoff: Annotated[ - Handoff | None, Field(description='Handoff request when session_status is pending_handoff') + Handoff | None, + Field(description='Handoff request when session_status is pending_handoff'), + NoExplicitNull, ] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context.py b/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context.py index 7264f9df4..724a90e8e 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_sponsored_context.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool from ..core import brand_ref @@ -41,7 +41,9 @@ class Jurisdiction(AdCPBaseModel): extra='allow', ) country: Annotated[str, Field(description='ISO 3166-1 alpha-2 country code.')] - region: Annotated[str | None, Field(description='Optional sub-national region code.')] = None + region: Annotated[ + str | None, Field(description='Optional sub-national region code.'), NoExplicitNull + ] = None regulation: Annotated[str, Field(description='Regulation or policy identifier.')] @@ -60,16 +62,19 @@ class DisclosureObligation(AdCPBaseModel): Field( description='Disclosure label text the host should render when disclosure is required.' ), + NoExplicitNull, ] = None timing: Annotated[ Timing | None, Field( description="When the disclosure must be presented relative to the sponsored context's influence." ), + NoExplicitNull, ] = None proximity: Annotated[ Proximity | None, Field(description='Where the disclosure should appear relative to the affected output.'), + NoExplicitNull, ] = None jurisdictions: Annotated[ list[Jurisdiction] | None, @@ -77,6 +82,7 @@ class DisclosureObligation(AdCPBaseModel): description='Jurisdictions where this declared disclosure obligation applies.', min_length=1, ), + NoExplicitNull, ] = None @@ -92,7 +98,9 @@ class DeclaredBy(AdCPBaseModel): extra='allow', ) agent_url: Annotated[ - AnyUrl | None, Field(description='HTTPS URL of the declaring agent or service.') + AnyUrl | None, + Field(description='HTTPS URL of the declaring agent or service.'), + NoExplicitNull, ] = None role: Annotated[Role, Field(description='Role of the declaring party.')] @@ -110,6 +118,7 @@ class PayingPrincipal(AdCPBaseModel): Field( description='Optional seller-assigned account context for the paying principal. This intentionally carries only an account_id so the canonical economic principal remains paying_principal.brand.' ), + NoExplicitNull, ] = None operator: Annotated[ str | None, @@ -117,12 +126,14 @@ class PayingPrincipal(AdCPBaseModel): description='Domain of the operator acting for the paying principal, when different from the brand domain.', pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', ), + NoExplicitNull, ] = None display_name: Annotated[ str | None, Field( description='Human-readable label for disclosure rendering. The canonical identity remains the brand/account reference.' ), + NoExplicitNull, ] = None @@ -147,9 +158,13 @@ class SiSponsoredContext(AdCPBaseModel): ), ] declared_at: Annotated[ - AwareDatetime | None, Field(description='When this sponsored-context declaration was made.') + AwareDatetime | None, + Field(description='When this sponsored-context declaration was made.'), + NoExplicitNull, ] = None declared_by: Annotated[ - DeclaredBy | None, Field(description='Agent or service that attached the declaration.') + DeclaredBy | None, + Field(description='Agent or service that attached the declaration.'), + NoExplicitNull, ] = None - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context_receipt.py b/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context_receipt.py index 4246a0e5f..10b6bc467 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context_receipt.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_sponsored_context_receipt.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_sponsored_context_receipt.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AwareDatetime, ConfigDict, Field from ..core import ext as ext_1 @@ -39,9 +39,12 @@ class DisclosureCommitment(AdCPBaseModel): Field( description='Disclosure label text the host committed to render, if different from or copied from the declaration.' ), + NoExplicitNull, ] = None notes: Annotated[ - str | None, Field(description='Optional host explanation for the disclosure commitment.') + str | None, + Field(description='Optional host explanation for the disclosure commitment.'), + NoExplicitNull, ] = None @@ -58,6 +61,7 @@ class HostReceipt(AdCPBaseModel): Field( description="Use mode the host accepted. When host_receipt.status is accepted, this MUST match the declaration's context_use; a host that cannot honor the declared use mode rejects the sponsored context instead of down-scoping it." ), + NoExplicitNull, ] = None received_at: Annotated[ AwareDatetime, Field(description='When the host received the sponsored context.') @@ -67,18 +71,21 @@ class HostReceipt(AdCPBaseModel): Field( description='Host-defined surface or placement where the context was accepted, such as an assistant session, search result page, or comparison module.' ), + NoExplicitNull, ] = None disclosure_commitment: Annotated[ DisclosureCommitment | None, Field( description='How the host committed to handle the declared disclosure obligation. Required when host_receipt.status is accepted.' ), + NoExplicitNull, ] = None rejection_reason: Annotated[ str | None, Field( description='Optional explanation when status is rejected, for example unsupported context_use or inability to satisfy the disclosure obligation.' ), + NoExplicitNull, ] = None @@ -98,4 +105,4 @@ class SiSponsoredContextReceipt(AdCPBaseModel): description='Receiving-surface accountability fact: the use mode the host accepted and committed to honor for this sponsored context.' ), ] - ext: ext_1.ExtensionObject | None = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_request.py b/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_request.py index 42e4630d1..20b5b5c6a 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_request.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_terminate_session_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import context as context_1 @@ -34,21 +34,28 @@ class TransactionIntent(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - action: Action | None = None - product: Annotated[dict[str, Any] | None, Field(description='Product/service details')] = None + action: Annotated[Action | None, NoExplicitNull] = None + product: Annotated[ + dict[str, Any] | None, Field(description='Product/service details'), NoExplicitNull + ] = None class TerminationContext(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - summary: Annotated[str | None, Field(description='Summary of the conversation')] = None + summary: Annotated[ + str | None, Field(description='Summary of the conversation'), NoExplicitNull + ] = None transaction_intent: Annotated[ TransactionIntent | None, Field(description='For handoff_transaction - what user wants to buy'), + NoExplicitNull, ] = None cause: Annotated[ - str | None, Field(description='For host_terminated - why host ended session') + str | None, + Field(description='For host_terminated - why host ended session'), + NoExplicitNull, ] = None @@ -59,7 +66,7 @@ class SiTerminateSessionRequest(AdcpRequest, AdcpVersionEnvelope): session_id: Annotated[str, Field(description='Session identifier to terminate')] reason: Annotated[Reason, Field(description='Reason for termination')] termination_context: Annotated[ - TerminationContext | None, Field(description='Context for the termination') + TerminationContext | None, Field(description='Context for the termination'), NoExplicitNull ] = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_response.py b/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_response.py index 5c79336e8..e05c15c20 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_response.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_terminate_session_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_terminate_session_response.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -9,7 +9,7 @@ from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool from ..core import context as context_1 @@ -29,24 +29,28 @@ class AcpHandoff(AdCPBaseModel): Field( description="Brand's ACP checkout endpoint. Hosts MUST validate this is HTTPS before opening." ), + NoExplicitNull, ] = None checkout_token: Annotated[ str | None, Field( description='Opaque token for the checkout flow. The host passes this to the checkout endpoint to correlate the SI session with the transaction.' ), + NoExplicitNull, ] = None payload: Annotated[ dict[str, Any] | None, Field( description='Rich checkout context to pass to the ACP endpoint (product details, applied offers, pricing). Alternative to checkout_token for integrations that need structured data.' ), + NoExplicitNull, ] = None expires_at: Annotated[ AwareDatetime | None, Field( description='When this handoff data expires. Hosts should initiate checkout before this time.' ), + NoExplicitNull, ] = None @@ -61,8 +65,10 @@ class FollowUp(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - action: Action | None = None - data: Annotated[dict[str, Any] | None, Field(description='Data for follow-up action')] = None + action: Annotated[Action | None, NoExplicitNull] = None + data: Annotated[ + dict[str, Any] | None, Field(description='Data for follow-up action'), NoExplicitNull + ] = None class SiTerminateSessionResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnvelope): @@ -78,12 +84,16 @@ class SiTerminateSessionResponse(AdcpResponse, AdcpVersionEnvelope, ProtocolEnve Field( description="Resulting session state. 'complete' for handoff_transaction/handoff_complete, 'terminated' for user_exit/session_timeout/host_terminated." ), + NoExplicitNull, ] = None acp_handoff: Annotated[ AcpHandoff | None, Field(description='ACP checkout handoff data. Present when reason is handoff_transaction.'), + NoExplicitNull, + ] = None + follow_up: Annotated[ + FollowUp | None, Field(description='Suggested follow-up actions'), NoExplicitNull ] = None - follow_up: Annotated[FollowUp | None, Field(description='Suggested follow-up actions')] = None - errors: list[error.Error] | None = None - context: context_1.ContextObject | None = None - ext: ext_1.ExtensionObject | None = None + errors: Annotated[list[error.Error] | None, NoExplicitNull] = None + context: Annotated[context_1.ContextObject | None, NoExplicitNull] = None + ext: Annotated[ext_1.ExtensionObject | None, NoExplicitNull] = None diff --git a/src/adcp/types/domains/sponsored_intelligence/si_ui_element.py b/src/adcp/types/domains/sponsored_intelligence/si_ui_element.py index 92556de6d..e9efbdadd 100644 --- a/src/adcp/types/domains/sponsored_intelligence/si_ui_element.py +++ b/src/adcp/types/domains/sponsored_intelligence/si_ui_element.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_ui_element.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -27,4 +27,6 @@ class SiUiElement(AdCPBaseModel): extra='allow', ) type: Annotated[Type, Field(description='Component type')] - data: Annotated[dict[str, Any] | None, Field(description='Component-specific data')] = None + data: Annotated[ + dict[str, Any] | None, Field(description='Component-specific data'), NoExplicitNull + ] = None diff --git a/src/adcp/types/domains/trusted_match/available_package.py b/src/adcp/types/domains/trusted_match/available_package.py index a7c3c7ab9..e14d46eba 100644 --- a/src/adcp/types/domains/trusted_match/available_package.py +++ b/src/adcp/types/domains/trusted_match/available_package.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: trusted_match/available_package.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import catalog, format_id, package_format_snapshot, seller_agent_ref @@ -30,6 +30,7 @@ class AvailablePackage(AdCPBaseModel): deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format identifiers eligible for this package. Use canonical `format_options`.', ), + NoExplicitNull, ] = None format_options: Annotated[ list[package_format_snapshot.PackageFormatSnapshot] | None, @@ -37,10 +38,12 @@ class AvailablePackage(AdCPBaseModel): description='Immutable seller-resolved PackageFormatSnapshots eligible for this package. A sender projects this field only when the Trusted Match transport profile supports the highest AdCP release version of every field in every snapshot. It MUST NOT strip an unsupported selector or digest field from a snapshot; for an older or version-unknown provider, omit the entire contract-bearing snapshot and do not claim production-path evidence from a legacy projection.', min_length=1, ), + NoExplicitNull, ] = None catalogs: Annotated[ list[catalog.Catalog] | None, Field( description="The buyer's catalogs attached to this package, with selectors (ids, gtins, tags, category, query) scoping which items are in play. References synced catalogs by catalog_id. The provider resolves items from its cached copy." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/context_match_request.py b/src/adcp/types/domains/trusted_match/context_match_request.py index 64205bfca..adadb8079 100644 --- a/src/adcp/types/domains/trusted_match/context_match_request.py +++ b/src/adcp/types/domains/trusted_match/context_match_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: trusted_match/context_match_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal from uuid import UUID -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field @@ -71,21 +71,24 @@ class ContextSignals(AdCPBaseModel): description="Content topic identifiers. Use IAB Content Taxonomy 3.0 IDs (e.g., '632' for Food & Drink) when taxonomy_id is 7, or bounded human-readable category labels (e.g., 'cooking.pasta') for custom taxonomies. For non-public content attributable to a single user or session, publishers MUST use standardized taxonomy identifiers or bounded custom category labels; custom topic strings MUST NOT reproduce distinctive verbatim phrasing and MUST NOT include PII or uniquely identifying details.", max_length=50, ), + NoExplicitNull, ] = None taxonomy_source: Annotated[ str | None, Field( description="Organization that defines the topic taxonomy. Use 'iab' for IAB Content Taxonomy. Publishers may use other values for custom taxonomies." ), + NoExplicitNull, ] = 'iab' taxonomy_id: Annotated[ SchemaInt | None, Field( description='Taxonomy version within the source. For IAB, follows the AdCOM cattax enum: 7 = Content Taxonomy 3.0. Default: 7.' ), + NoExplicitNull, ] = 7 sentiment: Annotated[ - Sentiment | None, Field(description='Content sentiment classification.') + Sentiment | None, Field(description='Content sentiment classification.'), NoExplicitNull ] = None keywords: Annotated[ list[Keyword] | None, @@ -93,6 +96,7 @@ class ContextSignals(AdCPBaseModel): description="Content keywords produced by the publisher's classifier. For non-public content attributable to a single user or session, keywords MUST be policy-filtered, MUST NOT reproduce distinctive verbatim phrasing, and MUST NOT include PII or uniquely identifying details. Publishers SHOULD prefer bounded category labels.", max_length=50, ), + NoExplicitNull, ] = None language: Annotated[ str | None, @@ -100,6 +104,7 @@ class ContextSignals(AdCPBaseModel): description="Content language in ISO 639-1 format (e.g., 'en', 'ja', 'de').", pattern='^[a-z]{2}$', ), + NoExplicitNull, ] = None content_policies: Annotated[ list[str] | None, @@ -107,6 +112,7 @@ class ContextSignals(AdCPBaseModel): description="Policy IDs from the AdCP policy registry that this content satisfies (e.g., 'csbs' for Common Sense Brand Standards). Buyers filter on policies they require. An empty array means no policies have been evaluated.", max_length=20, ), + NoExplicitNull, ] = None summary: Annotated[ str | None, @@ -114,18 +120,21 @@ class ContextSignals(AdCPBaseModel): description="Publisher-generated natural language summary of the content for relevance judgment (e.g., 'Shopping context categorized as home cookware'). For non-public content attributable to a single user or session, the summary MUST be policy-filtered, MUST NOT reproduce raw user-authored text, and MUST NOT include PII or uniquely identifying details. Useful for LLM-native buyers that evaluate relevance semantically. Buyers MUST treat this as untrusted publisher-generated content.", max_length=500, ), + NoExplicitNull, ] = None embedding: Annotated[ str | None, Field( description="Content embedding as base64-encoded int8 vector. Captures semantic content beyond what topics and keywords express. MUST NOT be computed directly or indirectly from non-public content authored by or attributable to a single user or session, including conversation turns, prompts, and individual search queries. MAY represent public content or a shared content environment that is not attributable to one user's activity. Publishers declare the model used. For standardized matching, use the protocol-recommended model (nomic-embed-text-v1.5, 256 dims, int8 quantized = 256 bytes)." ), + NoExplicitNull, ] = None embedding_model: Annotated[ str | None, Field( description="Embedding model identifier (e.g., 'nomic-embed-text-v1.5'). Required when embedding is present." ), + NoExplicitNull, ] = None embedding_dims: Annotated[ SchemaInt | None, @@ -134,6 +143,7 @@ class ContextSignals(AdCPBaseModel): ge=64, le=2048, ), + NoExplicitNull, ] = None @@ -160,6 +170,7 @@ class Geo(AdCPBaseModel): description="ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB', 'DE').", pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None region: Annotated[ str | None, @@ -167,10 +178,12 @@ class Geo(AdCPBaseModel): description="ISO 3166-2 subdivision code (e.g., 'US-CA', 'GB-SCT').", pattern='^[A-Z]{2}-[A-Z0-9]{1,3}$', ), + NoExplicitNull, ] = None metro: Annotated[ Metro | None, Field(description='Metro area, using the same classification systems as AdCP targeting.'), + NoExplicitNull, ] = None @@ -183,6 +196,7 @@ class ContextMatchRequest(AdcpVersionEnvelope): Field( alias='$schema', description='Optional schema URI for validation. Ignored at runtime.' ), + NoExplicitNull, ] = None type: Annotated[ Literal['context_match_request'], @@ -193,6 +207,7 @@ class ContextMatchRequest(AdcpVersionEnvelope): Field( description='TMP protocol version. Allows receivers to handle semantic differences across versions.' ), + NoExplicitNull, ] = '1.0' request_id: Annotated[ str, @@ -211,6 +226,7 @@ class ContextMatchRequest(AdcpVersionEnvelope): Field( description="Publisher's human-readable property slug (e.g., 'cnn_homepage'). Optional when property_rid is present. Useful for logging and debugging." ), + NoExplicitNull, ] = None property_type: Annotated[ property_type_1.PropertyType, Field(description='Type of the publisher property') @@ -232,6 +248,7 @@ class ContextMatchRequest(AdcpVersionEnvelope): Field( description='Full content artifact adjacent to this ad opportunity. Same schema used for content standards evaluation. The publisher sends the artifact when they want the buyer to evaluate the full content. Contractual protections govern buyer use. TEE deployment upgrades contractual trust to cryptographic verification. Because the router fans out to multiple buyer agents, publishers MUST NOT include bearer tokens, service-account credentials, or signed URLs in this artifact. Routers MUST remove every asset `access` object and remove or replace every credential-bearing asset `url` before forwarding; only public asset URLs that recipients can resolve independently may remain.' ), + NoExplicitNull, ] = None artifact_refs: Annotated[ list[ArtifactRef] | None, @@ -240,18 +257,21 @@ class ContextMatchRequest(AdcpVersionEnvelope): max_length=20, min_length=1, ), + NoExplicitNull, ] = None geo: Annotated[ Geo | None, Field( description='Coarse geographic location of the viewer. Publisher controls granularity — country is sufficient for regulatory compliance and volume filtering, region or metro helps with campaign targeting and valuation. Coarsened to prevent user identification: no postcode, no coordinates. All fields optional.' ), + NoExplicitNull, ] = None context_signals: Annotated[ ContextSignals | None, Field( description="Pre-computed classifier outputs for the content environment. Use when the publisher wants to provide privacy-reduced context without sharing content or public references. Can supplement artifact_refs or replace them entirely. Ephemeral content that many users encounter (a trending query, a syndicated segment) is shared content; one user's turn or query is not. For non-public content attributable to a single user or session, only the field-specific privacy-reduced outputs permitted below may be sent. Raw content MUST NOT be included. The publisher is the classifier and privacy boundary." ), + NoExplicitNull, ] = None package_ids: Annotated[ list[str] | None, @@ -260,4 +280,5 @@ class ContextMatchRequest(AdcpVersionEnvelope): max_length=500, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/context_match_response.py b/src/adcp/types/domains/trusted_match/context_match_response.py index f9e878915..73f7a2253 100644 --- a/src/adcp/types/domains/trusted_match/context_match_response.py +++ b/src/adcp/types/domains/trusted_match/context_match_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: trusted_match/context_match_response.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, RootModel, StringConstraints from ..core.protocol_envelope import ProtocolEnvelope @@ -21,6 +21,7 @@ class Signals(AdCPBaseModel): segments: Annotated[ list[str] | None, Field(description='Contextual segment identifiers combined across provider responses.'), + NoExplicitNull, ] = None @@ -75,6 +76,7 @@ class ContextMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope) Field( description='Merged non-keyed response-level signals. Provider-local targeting pairs do not pass through this object; the router emits them only in signals_by_provider.' ), + NoExplicitNull, ] = None signals_by_provider: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Za-z0-9_]+$', min_length=1, max_length=64)], SignalsByProvider] @@ -83,4 +85,5 @@ class ContextMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope) description="Router-authored map of provider targeting pairs, keyed by the publisher-assigned provider_id from provider registration. For every provider response containing a non-empty signals.targeting_kvs list, the router copies the complete list unchanged into that provider's bucket. The router derives the map key from its registration and MUST ignore or reject provider-supplied signals_by_provider data. A provider with no targeting pairs is omitted. Publishers resolve each (provider_id, key) tuple to a local ad-server destination and drop tuples that have no local mapping.", min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/error.py b/src/adcp/types/domains/trusted_match/error.py index 987255417..7ea7b9d53 100644 --- a/src/adcp/types/domains/trusted_match/error.py +++ b/src/adcp/types/domains/trusted_match/error.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: trusted_match/error.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field @@ -38,5 +38,7 @@ class TmpError(AdCPBaseModel): ), ] message: Annotated[ - str | None, Field(description='Human-readable error description for debugging') + str | None, + Field(description='Human-readable error description for debugging'), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/identity_match_request.py b/src/adcp/types/domains/trusted_match/identity_match_request.py index 7b6c559e1..ed7540657 100644 --- a/src/adcp/types/domains/trusted_match/identity_match_request.py +++ b/src/adcp/types/domains/trusted_match/identity_match_request.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: trusted_match/identity_match_request.json -# timestamp: 2026-10-04T15:01:24+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated, Any, Literal -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, StrictBool from ..core import brand_ref @@ -27,18 +27,25 @@ class Consent(AdCPBaseModel): extra='forbid', ) gdpr: Annotated[ - StrictBool | None, Field(description='Whether GDPR applies to this request.') + StrictBool | None, + Field(description='Whether GDPR applies to this request.'), + NoExplicitNull, ] = None tcf_consent: Annotated[ - str | None, Field(description='IAB TCF v2.2 consent string. Present when gdpr is true.') + str | None, + Field(description='IAB TCF v2.2 consent string. Present when gdpr is true.'), + NoExplicitNull, + ] = None + gpp: Annotated[ + str | None, Field(description='IAB Global Privacy Platform string.'), NoExplicitNull ] = None - gpp: Annotated[str | None, Field(description='IAB Global Privacy Platform string.')] = None us_privacy: Annotated[ str | None, Field( deprecated=True, description='US Privacy string (CCPA). Deprecated in favor of GPP but still widely used.', ), + NoExplicitNull, ] = None @@ -83,12 +90,14 @@ class Attestation(AdCPBaseModel): Field( description="Relying-party id the proof was minted for. The receiver checks this against the relying_party_id's published owner (brand.json `identity_relying_parties[]`) so a forwarded proof cannot be replayed under a different owner." ), + NoExplicitNull, ] = None action: Annotated[ str | None, Field( description='Issuer action/scope the proof was bound to (e.g. "humanity-check-for-ads").' ), + NoExplicitNull, ] = None claims: Annotated[ list[attestation_claim.AttestationClaim], @@ -103,12 +112,14 @@ class Attestation(AdCPBaseModel): Field( description='Credential strength (e.g. World ID Orb = biometric unique-human; Device = weaker; Document = passport/NFC).' ), + NoExplicitNull, ] = None signal_binding: Annotated[ str | None, Field( description='Hash of the signal the proof commits to. The receiver MUST verify it matches the context the receiver expects (e.g. a buyer-issued nonce or the request_id) and that the attestation is within its freshness window; this, plus nullifier-reuse tracking, is the replay defense.' ), + NoExplicitNull, ] = None proof: Annotated[ dict[str, Any], @@ -119,6 +130,7 @@ class Attestation(AdCPBaseModel): expires_at: Annotated[ AwareDatetime | None, Field(description='Attestation validity horizon. Receivers MUST reject when past.'), + NoExplicitNull, ] = None @@ -143,6 +155,7 @@ class Identity(AdCPBaseModel): Field( description="Optional verifiable proof ABOUT this identity (e.g. World ID proof-of-personhood and/or age). The receiver MUST verify it (see conformance) and MUST treat an absent-or-unverifiable attestation as 'no attestation' — never as an asserted-true claim. Issuer-agnostic: World ID is the first scheme; mDL / VC-style issuers use the same shape. Receivers MUST bound attestation size to prevent DoS." ), + NoExplicitNull, ] = None @@ -155,6 +168,7 @@ class IdentityMatchRequest(AdcpVersionEnvelope): Field( alias='$schema', description='Optional schema URI for validation. Ignored at runtime.' ), + NoExplicitNull, ] = None type: Annotated[ Literal['identity_match_request'], @@ -165,6 +179,7 @@ class IdentityMatchRequest(AdcpVersionEnvelope): Field( description='TMP protocol version. Allows receivers to handle semantic differences across versions.' ), + NoExplicitNull, ] = '1.0' request_id: Annotated[ str, @@ -191,6 +206,7 @@ class IdentityMatchRequest(AdcpVersionEnvelope): Field( description='Privacy consent signals. Buyers in regulated jurisdictions MUST NOT process the user token without consent information.' ), + NoExplicitNull, ] = None package_ids: Annotated[ list[str] | None, @@ -198,6 +214,7 @@ class IdentityMatchRequest(AdcpVersionEnvelope): description="Optional. When omitted, the buyer evaluates eligibility against the full set of active packages it has registered for `seller_agent_url`. When provided, the composition of `package_ids` MUST be statistically independent of the current placement — sending only the page-specific subset would let the buyer correlate Identity Match with Context Match by comparing package sets. Two acceptable modes: (a) **all-active** — include every active package this buyer has at this publisher; (b) **fuzzed** — include a random sample of active packages, optionally padded with synthetic non-existent IDs, drawn from a distribution that does not depend on the current placement. The buyer's silent-drop behavior on unknown IDs (specified below) is what makes synthetic-ID padding safe — they do not affect the response shape and cannot leak registry membership. When both `seller_agent_url` and `package_ids` are present, the buyer evaluates against the intersection of its registered active set and `package_ids`; IDs in `package_ids` that the buyer has not registered for this seller MUST be silently ignored (not surfaced as errors) to avoid leaking registry membership back to the publisher.", min_length=1, ), + NoExplicitNull, ] = None country: Annotated[ str | None, @@ -205,6 +222,7 @@ class IdentityMatchRequest(AdcpVersionEnvelope): description='ISO 3166-1 alpha-2 country code. Routing directive for the TMP Router — used to select the correct regional provider. The router MUST strip this field before forwarding the request to the buyer agent. Not an identity signal.', pattern='^[A-Z]{2}$', ), + NoExplicitNull, ] = None sealed_credentials: Annotated[ list[SealedCredential] | None, @@ -212,4 +230,5 @@ class IdentityMatchRequest(AdcpVersionEnvelope): description='Optional HPKE-sealed credentials addressed to specific audiences — the network-as-RP ("issuer-as-RP"/Mechanism B) carrier. Each payload is opaque to the publisher, who relays it untouched; the inner plaintext is an `attestation` (see identities[].attestation) scoped to the audience\'s relying party. Reuses the TMPX envelope format. Router handling (normative — see docs/trusted-match/specification.mdx): the router forwards each entry only to the provider that owns its `audience_kid` (not broadcast), folds `sealed_credentials` into the per-provider re-signature canonical bytes so an injected/swapped blob breaks the signature, and includes a `sealed_credentials_hash` in the dedup cache key. Receivers decrypt only entries whose `audience_kid` they hold a key for and ignore the rest. Receivers MUST bound count and size to prevent DoS amplification.', max_length=8, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/identity_match_response.py b/src/adcp/types/domains/trusted_match/identity_match_response.py index bd5eb5e1a..3d6c5610e 100644 --- a/src/adcp/types/domains/trusted_match/identity_match_response.py +++ b/src/adcp/types/domains/trusted_match/identity_match_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: trusted_match/identity_match_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, StringConstraints, model_validator from ..core.protocol_envelope import ProtocolEnvelope @@ -77,12 +77,14 @@ class IdentityMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope deprecated=True, description='DEPRECATED in favor of tmpx_providers. Routers MAY continue to populate this field for back-compat with consumers that only know the single-token shape; when both fields are present, tmpx_providers is authoritative. Single HPKE-encrypted exposure token containing the resolved user identity tokens. Wire format: kid.base64url_nopad(ciphertext) — unpadded base64url per RFC 4648 section 5 (no = characters). Publishers MUST treat this value as opaque pass-through data. Removed in 4.0.', ), + NoExplicitNull, ] = None tmpx_providers: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Za-z0-9_]+$', min_length=1, max_length=64)], TmpxProviders] | None, Field( description="Router-populated: ordered TMPX chunk/value pairs grouped by the originating identity provider's `provider_id`. Each entry's `chunks[]` is a copy of the provider's emitted `tmpx_chunks` list, in the same order. Each chunk carries a provider-local `slot_id` (from the provider's registered `tmpx_slots`) and an opaque URL-safe `value`; the publisher's deployment configuration (see publisher-tmpx-config.json) resolves each `(provider_id, slot_id)` pair to the ad-server macro name, targeting key, VAST substitution, or play-log field for that surface. The protocol carries values and attribution only. Required by router conformance when any identity provider emitted TMPX in this request; collapsing per-provider tokens into a single string loses attribution and breaks per-provider impression accounting. Map keys MUST match the provider_id charset registered in provider-registration.json (enforced by `propertyNames`). Publishers MUST NOT parse, decode, or transform any chunk's `value` — each is an opaque URL-safe wire string substituted verbatim into the mapped destination." ), + NoExplicitNull, ] = None @model_validator(mode='after') diff --git a/src/adcp/types/domains/trusted_match/offer.py b/src/adcp/types/domains/trusted_match/offer.py index 0cd6257cb..499d17f6e 100644 --- a/src/adcp/types/domains/trusted_match/offer.py +++ b/src/adcp/types/domains/trusted_match/offer.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: trusted_match/offer.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field from ..core import brand_ref @@ -25,34 +25,40 @@ class Offer(AdCPBaseModel): Field( description="Optional echo of the package's seller agent from sync-time metadata. Provided for publisher-side observability so log pipelines can attribute offers to sellers without round-tripping to the media-buy store. Non-authoritative: the binding on the cached AvailablePackage is source of truth. When omitted, the router MAY stamp this field from its cached package→seller map." ), + NoExplicitNull, ] = None brand: Annotated[ brand_ref.BrandReference | None, Field( description='Brand for this offer. Required when the product allows dynamic brands (brand selected at match time rather than fixed on the package). For single-brand packages, the brand is already known from the media buy.' ), + NoExplicitNull, ] = None price: Annotated[ offer_price.OfferPrice | None, Field( description='Price for this offer. Only present when the product supports variable pricing. For fixed-price packages, price is already set on the media buy.' ), + NoExplicitNull, ] = None summary: Annotated[ str | None, Field( description="Buyer-generated description of the offer, for the publisher to judge relevance. E.g., '50% off Goldenfield mayo — recipe integration'. The publisher (or their AI assistant) uses this to decide whether the offer fits the context." ), + NoExplicitNull, ] = None creative_manifest: Annotated[ creative_manifest_1.CreativeManifest | None, Field( description='Full creative details, inline. When present, the publisher has everything needed to render. Inline for small creatives (markdown, product card). For large creatives (VAST, video), the manifest references external assets via URLs.' ), + NoExplicitNull, ] = None creative_data: Annotated[ dict[str, str] | None, Field( description="Optional free-form string enhancements for dynamic creative rendering, such as sponsor labels and promotion codes. The map has no protocol-level key registry or required keys: meanings are package-local, receivers MUST ignore unknown keys, and a missing key MUST NOT make an otherwise renderable offer fail. Required renderable content belongs in creative_manifest assets, so creative_data may complement but never replace a manifest's required assets. This is not ad-server macro substitution or attribution tracking; tracker URLs belong in creative_manifest assets and per-user exposure tracking uses TMPX." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/offer_price.py b/src/adcp/types/domains/trusted_match/offer_price.py index 174fc0e17..3496bb89f 100644 --- a/src/adcp/types/domains/trusted_match/offer_price.py +++ b/src/adcp/types/domains/trusted_match/offer_price.py @@ -1,13 +1,13 @@ # generated by datamodel-codegen: # filename: trusted_match/offer_price.json -# timestamp: 2026-10-03T15:15:43+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum from typing import Annotated -from adcp.types.base import AdCPBaseModel +from adcp.types.base import AdCPBaseModel, NoExplicitNull from pydantic import ConfigDict, Field, StrictFloat @@ -27,6 +27,8 @@ class OfferPrice(AdCPBaseModel): StrictFloat, Field(description='Price amount in the specified currency', ge=0.0) ] currency: Annotated[ - str | None, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$') + str | None, + Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$'), + NoExplicitNull, ] = 'USD' model: Annotated[Model, Field(description='Pricing model for this offer')] diff --git a/src/adcp/types/domains/trusted_match/provider_context_match_response.py b/src/adcp/types/domains/trusted_match/provider_context_match_response.py index b120d6b25..c15bae21e 100644 --- a/src/adcp/types/domains/trusted_match/provider_context_match_response.py +++ b/src/adcp/types/domains/trusted_match/provider_context_match_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: trusted_match/provider_context_match_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field, RootModel from ..core.protocol_envelope import ProtocolEnvelope @@ -40,10 +40,10 @@ class Signals(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) - segments: Annotated[list[str] | None, Field(description='Contextual segment identifiers.')] = ( - None - ) - targeting_kvs: TargetingKvs | None = None + segments: Annotated[ + list[str] | None, Field(description='Contextual segment identifiers.'), NoExplicitNull + ] = None + targeting_kvs: Annotated[TargetingKvs | None, NoExplicitNull] = None class ContextMatchResponseProviderRouter(AdcpVersionEnvelope, ProtocolEnvelope): @@ -72,10 +72,12 @@ class ContextMatchResponseProviderRouter(AdcpVersionEnvelope, ProtocolEnvelope): ge=0, le=86400, ), + NoExplicitNull, ] = None signals: Annotated[ Signals | None, Field( description="Provider-origin response-level signals. Targeting pairs use the provider's local key vocabulary; the router preserves and attributes them rather than passing them through as flattened publisher targeting." ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/provider_identity_match_response.py b/src/adcp/types/domains/trusted_match/provider_identity_match_response.py index 91d88bb28..9e5e06ea7 100644 --- a/src/adcp/types/domains/trusted_match/provider_identity_match_response.py +++ b/src/adcp/types/domains/trusted_match/provider_identity_match_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: trusted_match/provider_identity_match_response.json -# timestamp: 2026-10-03T16:00:16+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations from typing import Annotated, Literal -from adcp.types.base import SchemaInt +from adcp.types.base import NoExplicitNull, SchemaInt from pydantic import ConfigDict, Field from ..core.protocol_envelope import ProtocolEnvelope @@ -48,4 +48,5 @@ class IdentityMatchResponseProviderRouter(AdcpVersionEnvelope, ProtocolEnvelope) max_length=2, min_length=1, ), + NoExplicitNull, ] = None diff --git a/src/adcp/types/domains/trusted_match/provider_registration.py b/src/adcp/types/domains/trusted_match/provider_registration.py index 706732e31..bc1fad6be 100644 --- a/src/adcp/types/domains/trusted_match/provider_registration.py +++ b/src/adcp/types/domains/trusted_match/provider_registration.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: trusted_match/provider_registration.json -# timestamp: 2026-10-11T15:55:26+00:00 +# timestamp: 2026-10-21T11:05:54+00:00 from __future__ import annotations @@ -8,7 +8,7 @@ from typing import Annotated, Literal from uuid import UUID -from adcp.types.base import AdCPBaseModel, SchemaInt +from adcp.types.base import AdCPBaseModel, NoExplicitNull, SchemaInt from adcp.types._scalar import ScalarStr from pydantic import AnyUrl, ConfigDict, Field, StrictBool, field_validator, model_validator @@ -64,6 +64,7 @@ class TmpProviderRegistration1(AdCPBaseModel): identity_match: Annotated[ StrictBool | None, Field(description='Provider handles Identity Match requests (POST /identity).'), + NoExplicitNull, ] = None countries: Annotated[ list[Country] | None, @@ -71,6 +72,7 @@ class TmpProviderRegistration1(AdCPBaseModel): description="ISO 3166-1 alpha-2 country codes this provider serves. The router filters Identity Match providers by the request's country field. MUST be present and non-empty when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None uid_types: Annotated[ list[uid_type.UidType] | None, @@ -78,6 +80,7 @@ class TmpProviderRegistration1(AdCPBaseModel): description="Identity types this provider can resolve. The router selects Identity Match providers whose uid_types overlaps with any uid_type in the request's identities array. MUST be present and non-empty when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None properties: Annotated[ list[UUID] | None, @@ -85,6 +88,7 @@ class TmpProviderRegistration1(AdCPBaseModel): description='Property RIDs (UUID v7) this provider serves. When present, the router only sends requests from these properties to this provider. When absent, the provider serves all properties.', min_length=1, ), + NoExplicitNull, ] = None timeout_ms: Annotated[ SchemaInt | None, @@ -93,6 +97,7 @@ class TmpProviderRegistration1(AdCPBaseModel): ge=5, le=5000, ), + NoExplicitNull, ] = 50 priority: Annotated[ SchemaInt | None, @@ -100,6 +105,7 @@ class TmpProviderRegistration1(AdCPBaseModel): description='Provider ordering for Context Match offer conflict resolution. Lower values have higher priority. When two providers return offers for the same package_id (a configuration error), the router keeps the offer from the higher-priority provider; equal priorities are broken by first response received. Identity Match eligibility remains a responder-scoped union because silent omission is not a negative vote. Also used for adaptive timeout allocation — higher-priority providers receive a larger share of the latency budget.', ge=0, ), + NoExplicitNull, ] = 0 tmpx_slots: Annotated[ list[TmpxSlot] | None, @@ -108,12 +114,14 @@ class TmpProviderRegistration1(AdCPBaseModel): max_length=2, min_length=1, ), + NoExplicitNull, ] = None status: Annotated[ Status | None, Field( description='Provider lifecycle status. Active providers receive requests. Inactive providers are skipped entirely. Draining providers stop receiving new requests but in-flight requests complete normally.' ), + NoExplicitNull, ] = Status.active @@ -156,6 +164,7 @@ class TmpProviderRegistration2(AdCPBaseModel): context_match: Annotated[ StrictBool | None, Field(description='Provider handles Context Match requests (POST /context).'), + NoExplicitNull, ] = None identity_match: Annotated[ Literal[True], @@ -167,6 +176,7 @@ class TmpProviderRegistration2(AdCPBaseModel): description="ISO 3166-1 alpha-2 country codes this provider serves. The router filters Identity Match providers by the request's country field. MUST be present and non-empty when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None uid_types: Annotated[ list[uid_type.UidType] | None, @@ -174,6 +184,7 @@ class TmpProviderRegistration2(AdCPBaseModel): description="Identity types this provider can resolve. The router selects Identity Match providers whose uid_types overlaps with any uid_type in the request's identities array. MUST be present and non-empty when identity_match is true.", min_length=1, ), + NoExplicitNull, ] = None properties: Annotated[ list[UUID] | None, @@ -181,6 +192,7 @@ class TmpProviderRegistration2(AdCPBaseModel): description='Property RIDs (UUID v7) this provider serves. When present, the router only sends requests from these properties to this provider. When absent, the provider serves all properties.', min_length=1, ), + NoExplicitNull, ] = None timeout_ms: Annotated[ SchemaInt | None, @@ -189,6 +201,7 @@ class TmpProviderRegistration2(AdCPBaseModel): ge=5, le=5000, ), + NoExplicitNull, ] = 50 priority: Annotated[ SchemaInt | None, @@ -196,6 +209,7 @@ class TmpProviderRegistration2(AdCPBaseModel): description='Provider ordering for Context Match offer conflict resolution. Lower values have higher priority. When two providers return offers for the same package_id (a configuration error), the router keeps the offer from the higher-priority provider; equal priorities are broken by first response received. Identity Match eligibility remains a responder-scoped union because silent omission is not a negative vote. Also used for adaptive timeout allocation — higher-priority providers receive a larger share of the latency budget.', ge=0, ), + NoExplicitNull, ] = 0 tmpx_slots: Annotated[ list[TmpxSlot] | None, @@ -204,12 +218,14 @@ class TmpProviderRegistration2(AdCPBaseModel): max_length=2, min_length=1, ), + NoExplicitNull, ] = None status: Annotated[ Status | None, Field( description='Provider lifecycle status. Active providers receive requests. Inactive providers are skipped entirely. Draining providers stop receiving new requests but in-flight requests complete normally.' ), + NoExplicitNull, ] = Status.active diff --git a/tests/conformance/_schema_parity.py b/tests/conformance/_schema_parity.py index 02f142c26..8b5a09bd4 100644 --- a/tests/conformance/_schema_parity.py +++ b/tests/conformance/_schema_parity.py @@ -565,15 +565,6 @@ def _is_numeric_string(value: Any) -> bool: #: on it. A predicate that matches nothing has been fixed, and the suite says #: to delete it — this set only shrinks. DECLARED: dict[str, Callable[[Divergence], bool]] = { - # The generator renders an optional property as ``X | None``, which - # conflates "may be absent" with "may be null". JSON Schema separates - # them: a property outside ``required`` may be absent, and ``null`` is - # valid only when the declared type admits it. Closing this needs a - # decision about whether AdCP intends optional properties to accept an - # explicit null; if it does, the schemas declare ``["string", "null"]``. - "optional_field_accepts_explicit_null": lambda d: ( - d.direction == MODEL_ACCEPTS and d.mutation is None - ), # ``format: date-time`` and ``format: date`` become ``AwareDatetime`` and # ``date``, whose parsers read a number as a Unix timestamp and a numeric # string as one too. The schema requires an RFC 3339 string. diff --git a/tests/conformance/test_schema_model_parity.py b/tests/conformance/test_schema_model_parity.py index b68847f0f..5bb3facf3 100644 --- a/tests/conformance/test_schema_model_parity.py +++ b/tests/conformance/test_schema_model_parity.py @@ -18,6 +18,7 @@ from __future__ import annotations import copy +import json from functools import cache from typing import Any @@ -76,8 +77,11 @@ def _model_for(tool: str) -> tuple[str, Any] | tuple[None, None]: def _model_verdict(model: Any, document: Any) -> tuple[bool, frozenset[str]]: + # The document under test is a JSON document, so the model grades it in + # JSON mode: that is where a ``null`` is a value rather than the SDK's + # Python spelling of absence (``adcp.types.base.NoExplicitNull``). try: - model.model_validate(document) + model.model_validate_json(json.dumps(document)) except ValidationError as exc: return False, frozenset(error["type"] for error in exc.errors()) return True, frozenset() diff --git a/tests/test_canceled_literal_default.py b/tests/test_canceled_literal_default.py index b1b851d3d..6617f11a8 100644 --- a/tests/test_canceled_literal_default.py +++ b/tests/test_canceled_literal_default.py @@ -101,7 +101,7 @@ class TestFixIdempotency: """The post-gen fix must be idempotent — running it twice produces the same output.""" def test_fix_is_idempotent_on_synthesized_source(self) -> None: - from scripts.post_generate_fixes import _CANCELED_FIELD_RE + from scripts.post_generate_fixes import _CANCELED_FIELD_RE, _WIDENED_LITERAL_TRUE already_fixed = ( " canceled: Annotated[\n" @@ -115,14 +115,14 @@ def test_fix_is_idempotent_on_synthesized_source(self) -> None: ) result, count = _CANCELED_FIELD_RE.subn( - r"\1Literal[True] | None\2 = None", + _WIDENED_LITERAL_TRUE, already_fixed, ) assert count == 0, "fix must not re-apply to already-widened source" assert result == already_fixed def test_fix_matches_destructive_source(self) -> None: - from scripts.post_generate_fixes import _CANCELED_FIELD_RE + from scripts.post_generate_fixes import _CANCELED_FIELD_RE, _WIDENED_LITERAL_TRUE destructive = ( " canceled: Annotated[\n" @@ -136,7 +136,7 @@ def test_fix_matches_destructive_source(self) -> None: ) result, count = _CANCELED_FIELD_RE.subn( - r"\1Literal[True] | None\2 = None", + _WIDENED_LITERAL_TRUE, destructive, ) assert count == 1 diff --git a/tests/test_codegen_invariants.py b/tests/test_codegen_invariants.py index 36fe98aee..bae0673f7 100644 --- a/tests/test_codegen_invariants.py +++ b/tests/test_codegen_invariants.py @@ -176,17 +176,34 @@ def test_proposal_refinement_does_not_remove_frequency_cap_by_default() -> None: @pytest.mark.parametrize("prefix", ["", "bundled"]) @pytest.mark.parametrize( - "annotation,default", + "annotation,default,widened", [ - ("Literal[True]", "True"), - ("Annotated[Literal[True], Field(deprecated=True)]", "True"), - ("Annotated[Literal[True], Field(description='Explicit choice — True')]", "True"), + ("Literal[True]", "True", "Annotated[Literal[True] | None, NoExplicitNull]"), + ( + "Annotated[Literal[True], Field(deprecated=True)]", + "True", + "Annotated[Literal[True] | None, Field(deprecated=True), NoExplicitNull]", + ), + ( + "Annotated[Literal[True], Field(description='Explicit choice — True')]", + "True", + "Annotated[Literal[True] | None, Field(description='Explicit choice — True'), " + "NoExplicitNull]", + ), ( "Annotated[\n Literal[True],\n" " Field(description='Explicit choice — preserve metadata'),\n ]", "True", + "Annotated[\n Literal[True] | None,\n" + " Field(description='Explicit choice — preserve metadata'),\n" + " NoExplicitNull,\n ]", + ), + ( + "Annotated[Literal[False], Field(description='False-only flag') ]", + "False", + "Annotated[Literal[False] | None, Field(description='False-only flag') , " + "NoExplicitNull]", ), - ("Annotated[Literal[False], Field(description='False-only flag') ]", "False"), ], ) def test_optional_const_fixes_are_idempotent( @@ -195,7 +212,10 @@ def test_optional_const_fixes_are_idempotent( prefix: str, annotation: str, default: str, + widened: str, ) -> None: + """Widening keeps the constant's metadata and adds ``NoExplicitNull`` (#1547): + the schema admits absence, never ``null``.""" from scripts import post_generate_fixes target = tmp_path / prefix / "core/model.py" @@ -212,10 +232,13 @@ def test_optional_const_fixes_are_idempotent( fix = post_generate_fixes.fix_optional_boolean_literal_defaults fix() updated = target.read_text() - optional_annotation = annotation.replace(f"Literal[{default}]", f"Literal[{default}] | None") - assert updated == original.replace( - f"optional_flag: {annotation} = {default}", - f"optional_flag: {optional_annotation} = None", + assert updated == ( + "from typing import Annotated\n" + "from adcp.types.base import NoExplicitNull\n" + + original.replace( + f"optional_flag: {annotation} = {default}", + f"optional_flag: {widened} = None", + ) ) ast.parse(updated) fix() diff --git a/tests/test_manifest_response_readback.py b/tests/test_manifest_response_readback.py index 8947987dd..366ad4b3d 100644 --- a/tests/test_manifest_response_readback.py +++ b/tests/test_manifest_response_readback.py @@ -44,7 +44,8 @@ def response_case(case, kind=FUTURE_KIND): manifest = { - "format_kind": kind, + # ``None`` means the property is absent: the schema admits no null. + **({"format_kind": kind} if kind is not None else {}), "assets": {}, "vendor_annotation": {"notes": ["preserved"]}, } @@ -175,7 +176,9 @@ def test_async_response_union_and_webhook_round_trip(case): result = adapter.validate_json(json.dumps(payload)) assert isinstance(result, model) assert read_manifest(result, case).format_kind == FUTURE_KIND - reparsed = adapter.validate_json(adapter.dump_json(result)) + # A ``TypeAdapter`` dump has no ``exclude_none`` default of its own, and + # the schema admits no ``null`` for an omitted property. + reparsed = adapter.validate_json(adapter.dump_json(result, exclude_none=True)) assert isinstance(reparsed, model) assert read_manifest(reparsed, case).format_kind == FUTURE_KIND @@ -209,7 +212,12 @@ async def test_public_completed_task_parser_keeps_manifest_readback(case): # A successful untyped fallback would silently hide strict-union rejection. assert isinstance(result.data, model) assert read_manifest(result.data, case).format_kind == FUTURE_KIND - reparsed = TaskResult[AdcpAsyncResponseData].model_validate_json(result.model_dump_json()) + # ``TaskResult`` is a plain pydantic model: its dump carries every ``None`` + # as ``null`` unless told otherwise, and a JSON ``null`` is not an AdCP + # document (#1547). Dump the way the CLI does, with ``exclude_none``. + reparsed = TaskResult[AdcpAsyncResponseData].model_validate_json( + result.model_dump_json(exclude_none=True) + ) assert isinstance(reparsed.data, model) assert read_manifest(reparsed.data, case).format_kind == FUTURE_KIND diff --git a/tests/test_no_explicit_null.py b/tests/test_no_explicit_null.py new file mode 100644 index 000000000..63ca04fef --- /dev/null +++ b/tests/test_no_explicit_null.py @@ -0,0 +1,289 @@ +"""An optional property refuses an explicit ``null`` unless its schema admits one. + +The generator renders every optional property as ``T | None = None``. That is +the right Python surface — omitted reads as ``None`` and dumps as absent — but +it also admitted an explicit JSON ``null`` that the schema, and so the SDK's +own JSON-Schema arm, rejects (#1547). The generator now marks each optional +property the bundled validator says cannot be ``null`` and the post-generation +pass points the mark at :data:`adcp.types.base.NoExplicitNull`. + +The rule is about the wire: a JSON document carrying ``null`` is refused, and +the published JSON Schema has no null arm. A Python ``None`` — keyword +construction, ``model_validate`` on a dict — stays the SDK's spelling of +"not supplied", symmetric with ``model_dump``'s ``exclude_none`` default, unless +``ADCP_STRICT_VALIDATION`` asks for it to be refused too. + +``tests/conformance/test_schema_model_parity.py`` is the sweep: it mutates every +leaf of every graded tool's request to ``None`` and requires the bundled schema +and the model (in JSON mode) to agree, with the +``optional_field_accepts_explicit_null`` divergence no longer declared. These +tests pin the rule's shape on the reported case, a genuinely nullable property, +and a sample of generated models. +""" + +from __future__ import annotations + +import json +import random +from pathlib import Path +from typing import Annotated, Any + +import pytest +from pydantic import BaseModel, ValidationError + +import adcp.types as public_types +from adcp.types import base as types_base +from adcp.types.base import NoExplicitNull +from adcp.types.domains.creative.sync_creatives_request import SyncCreativesRequest +from adcp.types.domains.media_buy.update_media_buy_request import UpdateMediaBuyRequest +from adcp.validation import get_schema, validate_request + +#: The #1547 reproduction: a legal assignment-only sync, plus ``creatives: null``. +_SYNC_WITH_NULL_CREATIVES: dict[str, Any] = { + "adcp_version": "3.2", + "account": {"account_id": "acct_1"}, + "idempotency_key": "repro-null-creatives-0001", + "creatives": None, + "assignment_operations": [ + {"operation": "unassign", "creative_id": "cr_1", "package_id": "pkg_1"}, + ], +} + + +def _null_errors(exc: ValidationError) -> list[tuple[tuple[Any, ...], str]]: + return [(error["loc"], error["type"]) for error in exc.errors()] + + +def test_explicit_null_creatives_is_refused_like_the_bundled_schema() -> None: + """The reported case: both arms now reject the JSON document at the same place.""" + schema_node = get_schema("sync_creatives", "request", version="3.2")["properties"]["creatives"] + assert schema_node["type"] == "array" + + outcome = validate_request("sync_creatives", _SYNC_WITH_NULL_CREATIVES, version="3.2") + assert [(issue.pointer, issue.keyword) for issue in outcome.issues] == [("/creatives", "type")] + + with pytest.raises(ValidationError) as excinfo: + SyncCreativesRequest.model_validate_json(json.dumps(_SYNC_WITH_NULL_CREATIVES)) + # The type's own error, exactly as a required array given null would report. + assert _null_errors(excinfo.value) == [(("creatives",), "list_type")] + + +def test_a_python_none_still_spells_absence() -> None: + """``model_validate`` on a dict and keyword construction keep the SDK's convention. + + ``None`` on the Python side means "not supplied": it reads back as ``None`` + and ``model_dump`` leaves it off the wire, exactly as an omitted property + would. Refusing it here would break every caller who passes an optional + value through as ``field=maybe_none``. + """ + parsed = SyncCreativesRequest.model_validate(_SYNC_WITH_NULL_CREATIVES) + assert parsed.creatives is None + assert "creatives" not in parsed.model_dump() + + base = {k: v for k, v in _SYNC_WITH_NULL_CREATIVES.items() if k != "creatives"} + constructed = SyncCreativesRequest(**base, creatives=None) + assert constructed.creatives is None + assert constructed.model_dump() == SyncCreativesRequest(**base).model_dump() + + +def test_strict_validation_refuses_a_python_none_too(monkeypatch: pytest.MonkeyPatch) -> None: + """``ADCP_STRICT_VALIDATION`` extends the refusal to Python-mode input. + + The policy is read when a model's core schema is built, like the + ``extra`` policy it travels with, so the check uses a model built under it. + """ + monkeypatch.setattr(types_base, "_EXTRA_POLICY", "forbid") + + class Strict(BaseModel): + items: Annotated[list[str] | None, NoExplicitNull] = None + + assert Strict().items is None + with pytest.raises(ValidationError) as excinfo: + Strict.model_validate({"items": None}) + assert _null_errors(excinfo.value) == [(("items",), "list_type")] + with pytest.raises(ValidationError): + Strict(items=None) + assert Strict.model_validate_json('{"items": ["a"]}').items == ["a"] + + +def test_canonical_subclass_inherits_the_rule_through_inherit() -> None: + """The canonical redeclaration (``_inherit``, #1548) keeps the generated marker.""" + field = public_types.SyncCreativesRequest.model_fields["creatives"] + assert NoExplicitNull in field.metadata + canonical = public_types.SyncCreativesRequest + with pytest.raises(ValidationError) as excinfo: + canonical.model_validate_json(json.dumps(_SYNC_WITH_NULL_CREATIVES)) + assert (("creatives",), "list_type") in _null_errors(excinfo.value) + assert canonical.model_validate(_SYNC_WITH_NULL_CREATIVES).creatives is None + + +def test_a_redeclaration_that_keeps_the_field_info_keeps_the_marker() -> None: + """What ``_inherit`` relies on: assigning the parent's ``FieldInfo`` carries the rule.""" + parent_field = SyncCreativesRequest.model_fields["creatives"] + + class Narrowed(SyncCreativesRequest): + creatives: list[dict[str, object]] | None = parent_field # type: ignore[assignment] + + assert NoExplicitNull in Narrowed.model_fields["creatives"].metadata + with pytest.raises(ValidationError) as excinfo: + Narrowed.model_validate_json(json.dumps(_SYNC_WITH_NULL_CREATIVES)) + assert _null_errors(excinfo.value) == [(("creatives",), "list_type")] + + +def test_omitted_creatives_still_reads_as_none() -> None: + """Absence is the one permitted way of not supplying the property, and reads as ``None``.""" + payload = {k: v for k, v in _SYNC_WITH_NULL_CREATIVES.items() if k != "creatives"} + assert validate_request("sync_creatives", payload, version="3.2").valid + + request = SyncCreativesRequest.model_validate(payload) + assert request.creatives is None + assert "creatives" not in request.model_fields_set + assert "creatives" not in request.model_dump() + + # Keyword construction without the property behaves the same way. + constructed = SyncCreativesRequest(**payload) + assert constructed.creatives is None + assert constructed.model_dump() == request.model_dump() + + +def test_constraints_and_description_survive_beside_the_marker() -> None: + """The marker is appended to the field's metadata, not substituted for it.""" + field = SyncCreativesRequest.model_fields["creatives"] + assert NoExplicitNull in field.metadata + assert field.description == "Array of creative assets to sync (create or update)" + too_many = { + **{k: v for k, v in _SYNC_WITH_NULL_CREATIVES.items() if k != "creatives"}, + "creatives": [ + {"creative_id": f"cr_{i}", "name": f"n{i}", "format_kind": "image"} for i in range(101) + ], + } + with pytest.raises(ValidationError) as excinfo: + SyncCreativesRequest.model_validate(too_many) + assert any(error["type"] == "too_long" for error in excinfo.value.errors()) + + +def test_published_schema_states_what_the_bundle_states() -> None: + """``model_json_schema`` no longer advertises a null arm for a non-nullable property.""" + node = SyncCreativesRequest.model_json_schema()["properties"]["creatives"] + assert "anyOf" not in node + assert node["type"] == "array" + assert node["minItems"] == 1 + assert node["maxItems"] == 100 + assert node["description"] == "Array of creative assets to sync (create or update)" + assert "creatives" not in SyncCreativesRequest.model_json_schema().get("required", []) + + +def test_genuinely_nullable_property_keeps_accepting_null() -> None: + """A property whose schema lists ``null`` is generated without the marker.""" + schema = get_schema("update_media_buy", "request", version="3.2") + assert "null" in schema["properties"]["budget_cap_timezone"]["type"] + assert "null" in schema["properties"]["daily_budget_cap"]["type"] + + payload = { + "account": {"account_id": "acct_1"}, + "media_buy_id": "mb_1", + "idempotency_key": "nullable-timezone-0001", + "budget_cap_timezone": None, + "daily_budget_cap": None, + } + assert validate_request("update_media_buy", payload, version="3.2").valid + + request = UpdateMediaBuyRequest.model_validate_json(json.dumps(payload)) + assert request.budget_cap_timezone is None + assert "budget_cap_timezone" in request.model_fields_set + for name in ("budget_cap_timezone", "daily_budget_cap"): + assert NoExplicitNull not in UpdateMediaBuyRequest.model_fields[name].metadata + node = UpdateMediaBuyRequest.model_json_schema()["properties"]["budget_cap_timezone"] + assert {"type": "null"} in node["anyOf"] + + +def test_json_round_trip_of_an_omitted_property_stays_omitted() -> None: + """Dump then parse as JSON: an omitted property never becomes an explicit null.""" + base = {k: v for k, v in _SYNC_WITH_NULL_CREATIVES.items() if k != "creatives"} + request = SyncCreativesRequest(**base, creatives=None) + wire = request.model_dump_json() + assert '"creatives"' not in wire + assert SyncCreativesRequest.model_validate_json(wire).creatives is None + + +# --- a sample of generated models, graded against their own schema ----------------- + + +def _generated_models() -> list[tuple[str, type[BaseModel]]]: + names = sorted( + name + for name in dir(public_types) + if not name.startswith("_") and not name.startswith("Legacy") + ) + models = [] + for name in names: + obj = getattr(public_types, name, None) + if isinstance(obj, type) and issubclass(obj, BaseModel) and obj.model_fields: + models.append((name, obj)) + return models + + +@pytest.mark.parametrize( + "name,model", + random.Random(1547).sample(_generated_models(), 60), + ids=lambda value: value if isinstance(value, str) else "", +) +def test_every_optional_field_either_carries_the_marker_or_admits_null( + name: str, model: type[BaseModel] +) -> None: + """For a sample of models: the marker is the only thing that refuses ``None``, and + pydantic's verdict on ``null`` follows the marker. + + Each optional field is probed in isolation with ``model_construct``-free input: + a document carrying every required field's current default-less sentinel would + need per-model synthesis, so the probe reads the field's own validator through + the published schema instead — a marked field has no null arm, an unmarked + optional field has one (or is a bare ``Any``). + """ + schema = model.model_json_schema() + required = set(schema.get("required", [])) + for field_name, field in model.model_fields.items(): + if field.is_required(): + continue + wire_name = field.alias or field_name + node = schema["properties"].get(wire_name) + if node is None: + continue + marked = NoExplicitNull in field.metadata + annotation = repr(field.annotation) + if marked: + assert wire_name not in required + assert node.get("type") != "null" + assert {"type": "null"} not in node.get("anyOf", []), (name, field_name, node) + elif "None" in annotation and "Any" not in annotation and node.get("anyOf") is None: + # An unmarked optional ``T | None`` must publish a null arm, unless the + # type itself is a reference the schema expresses without ``anyOf``. + assert "$ref" in node or node.get("type") == "null" or "allOf" in node, ( + name, + field_name, + node, + ) + + +def test_no_generated_module_still_carries_the_transient_mark() -> None: + """The ``x-adcp-no-explicit-null`` key is a generator-internal carrier and never ships.""" + domains = Path(public_types.__file__).parent / "domains" + offenders = [ + path.relative_to(domains) + for path in domains.rglob("*.py") + if "x-adcp-no-explicit-null" in path.read_text() + ] + assert offenders == [] + bundled = Path(public_types.__file__).parent.parent / "_schemas" + if bundled.exists(): + for path in bundled.rglob("*.json"): + assert "x-adcp-no-explicit-null" not in path.read_text(), path + + +def test_marker_is_widespread_in_the_generated_tree() -> None: + """The rule is generator-level: it reaches thousands of fields across hundreds of modules.""" + domains = Path(public_types.__file__).parent / "domains" + modules = [path for path in domains.rglob("*.py") if "NoExplicitNull" in path.read_text()] + fields = sum(path.read_text().count("NoExplicitNull") - 1 for path in modules) + assert len(modules) > 500 + assert fields > 8000 diff --git a/tests/test_post_generate_no_explicit_null.py b/tests/test_post_generate_no_explicit_null.py new file mode 100644 index 000000000..e10c8d014 --- /dev/null +++ b/tests/test_post_generate_no_explicit_null.py @@ -0,0 +1,179 @@ +"""The ``NoExplicitNull`` post-generation rewrite reproduces the generator's layout. + +``point_omittable_fields_at_no_explicit_null`` removes the +``json_schema_extra={'x-adcp-no-explicit-null': True}`` keyword the generator +emitted for a marked property and appends ``NoExplicitNull`` to the field's +``Annotated`` metadata, re-rendering the statement the way black laid it out. +Every later fix reads that text, so the layout has to be the generator's own: +with the marker suppressed the rewrite must give back exactly what the +generator writes for the same schema without the mark. +""" + +from __future__ import annotations + +import ast +import textwrap + +import pytest + +from scripts import post_generate_fixes as fixes + +MARK = fixes._NO_EXPLICIT_NULL_EXTRA + + +def _rewrite(source: str, marker: str | None = fixes._NO_EXPLICIT_NULL_MARKER) -> tuple[str, int]: + rewritten, count = fixes._rewrite_no_explicit_null_marks(textwrap.dedent(source), marker) + ast.parse(rewritten) + return rewritten, count + + +def test_width_follows_the_repository_black_configuration() -> None: + assert fixes._GENERATED_LINE_WIDTH == 100 + + +def test_mark_alone_becomes_the_marker_on_one_line() -> None: + source = f"""\ + class X(AdCPBaseModel): + ref: Annotated[R | None, Field({MARK})] = None + """ + rewritten, count = _rewrite(source) + assert count == 1 + assert " ref: Annotated[R | None, NoExplicitNull] = None\n" in rewritten + + +def test_mark_beside_other_arguments_is_removed_and_the_marker_appended() -> None: + source = f"""\ + class X(AdCPBaseModel): + creatives: Annotated[ + list[Creative] | None, + Field( + description='Array of creative assets to sync (create or update)', + {MARK}, + max_length=100, + min_length=1, + ), + ] = None + """ + rewritten, count = _rewrite(source) + assert count == 1 + assert rewritten == textwrap.dedent("""\ + class X(AdCPBaseModel): + creatives: Annotated[ + list[Creative] | None, + Field( + description='Array of creative assets to sync (create or update)', + max_length=100, + min_length=1, + ), + NoExplicitNull, + ] = None + """) + + +def test_an_annotation_that_already_refuses_none_only_loses_the_keyword() -> None: + description = "Set to true to cancel the media buy. " * 3 + source = f"""\ + class X(AdCPBaseModel): + status: Annotated[Literal['removed'], Field({MARK})] = 'removed' + canceled: Annotated[ + Literal[True], + Field( + description='{description}', + {MARK}, + ), + ] = True + """ + rewritten, count = _rewrite(source) + assert count == 0 + assert "NoExplicitNull" not in rewritten + assert " status: Literal['removed'] = 'removed'\n" in rewritten + # A single long argument is kept on one inner line with no trailing comma, + # which is the form ``_CANCELED_FIELD_RE`` matches. + assert fixes._CANCELED_FIELD_RE.search(rewritten) is not None + + +def test_a_parenthesised_default_is_read_back_and_re_split_only_when_needed() -> None: + source = f"""\ + class X(AdCPBaseModel): + layout: Annotated[Layout | None, Field({MARK})] = ( + Layout.vertical + ) + """ + rewritten, _count = _rewrite(source, marker=None) + assert rewritten == textwrap.dedent("""\ + class X(AdCPBaseModel): + layout: Layout | None = Layout.vertical + """) + + +def test_black_forms_are_chosen_by_width() -> None: + long_description = "x" * 30 + source = f"""\ + class X(AdCPBaseModel): + short: Annotated[StrictBool | None, Field(description='Short one.', {MARK})] = False + longer_name_here: Annotated[ + StrictBool | None, Field(description='{long_description}', {MARK}) + ] = False + """ + rewritten, count = _rewrite(source) + assert count == 2 + lines = rewritten.splitlines() + short_field = "Annotated[StrictBool | None, Field(description='Short one.'), NoExplicitNull]" + assert f" short: {short_field} = False" in lines + # Too wide for one line and the head does not fit either: the subscript is + # split, with its content hugged on one inner line. + hugged = f"StrictBool | None, Field(description='{long_description}'), NoExplicitNull" + assert " longer_name_here: Annotated[" in lines + assert f" {hugged}" in lines + assert " ] = False" in lines + assert all(len(line) <= fixes._GENERATED_LINE_WIDTH for line in lines) + + +def test_marker_suppressed_gives_back_the_unmarked_rendering() -> None: + source = f"""\ + class X(AdCPBaseModel): + a: Annotated[str | None, Field({MARK})] = None + b: Annotated[str | None, Field(description='kept', {MARK})] = None + c: Annotated[str | None, Field({MARK}, max_length=3)] = None + """ + rewritten, count = _rewrite(source, marker=None) + assert count == 0 + assert rewritten == textwrap.dedent("""\ + class X(AdCPBaseModel): + a: str | None = None + b: Annotated[str | None, Field(description='kept')] = None + c: Annotated[str | None, Field(max_length=3)] = None + """) + + +@pytest.mark.parametrize( + "form", ["Field(json_schema_extra={'x-adcp-no-explicit-null': True, 'other': 1})"] +) +def test_a_json_schema_extra_that_is_not_only_the_mark_is_left_alone(form: str) -> None: + source = f"""\ + class X(AdCPBaseModel): + a: Annotated[str | None, {form}] = None + """ + rewritten, count = _rewrite(source) + assert count == 0 + assert form in rewritten + + +def test_non_ascii_text_before_the_mark_does_not_shift_the_edit() -> None: + """``ast`` columns are UTF-8 byte offsets; an em dash earlier on the line must not + corrupt the rewrite (a whole-tree run caught this once).""" + source = f"""\ + class X(AdCPBaseModel): + a: Annotated[str | None, Field(description='Explicit choice — True', {MARK})] = None + b: Annotated[ + str | None, + Field(description='Irreversible — canceled buys stop. Ünïcode.', {MARK}), + ] = None + """ + rewritten, count = _rewrite(source) + assert count == 2 + assert "Field(description='Explicit choice — True'), NoExplicitNull" in rewritten + assert ( + "Ünïcode.'), NoExplicitNull" in rewritten + or "Ünïcode.'),\n NoExplicitNull" in rewritten + ) diff --git a/tests/test_targeting_overlay_compat.py b/tests/test_targeting_overlay_compat.py index 2e6e668c2..5aa892c65 100644 --- a/tests/test_targeting_overlay_compat.py +++ b/tests/test_targeting_overlay_compat.py @@ -315,28 +315,28 @@ def test_bridge_serialization_keeps_objects_on_the_wire( @pytest.mark.parametrize("container,required", _CONTAINERS) -def test_container_json_schema_keeps_only_mutation_input_and_null( +def test_container_json_schema_keeps_only_mutation_input( container: type[BaseModel], required: dict[str, Any] ) -> None: - schema = container.model_json_schema() - assert schema["properties"]["targeting_overlay"]["anyOf"] == [ - {"$ref": "#/$defs/TargetingOverlayInput"}, - {"type": "null"}, - ] + # The bundled schema declares the overlay as the input shape with no null + # arm (the field is optional, not nullable), and the published schema + # states the same since ``NoExplicitNull`` (#1547). + node = container.model_json_schema()["properties"]["targeting_overlay"] + assert node["$ref"] == "#/$defs/TargetingOverlayInput" + assert "anyOf" not in node @pytest.mark.parametrize("container,required", _CONTAINERS) -def test_field_serialization_schema_keeps_only_mutation_input_and_null( +def test_field_serialization_schema_keeps_only_mutation_input( container: type[BaseModel], required: dict[str, Any] ) -> None: # Canonical parents have their own opaque wrap serializer. Check the field # contract directly rather than expecting properties in that parent schema. annotation = container.model_fields["targeting_overlay"].rebuild_annotation() schema = TypeAdapter(annotation).json_schema(mode="serialization") - assert schema["anyOf"] == [ - {"$ref": "#/$defs/TargetingOverlayInput"}, - {"type": "null"}, - ] + assert "anyOf" not in schema + assert schema["title"] == "TargetingOverlayInput" + assert schema["type"] == "object" @pytest.mark.parametrize( diff --git a/tests/test_targeting_overlay_schema.py b/tests/test_targeting_overlay_schema.py index d3495a3b9..e735ca680 100644 --- a/tests/test_targeting_overlay_schema.py +++ b/tests/test_targeting_overlay_schema.py @@ -23,7 +23,10 @@ _PACKAGE_PATH, ("properties", "new_packages", "anyOf", 0, "items", "properties", "targeting_overlay"), }, - "control_media_buy": {_PACKAGE_PATH}, + # ``packages`` is optional but not nullable on this request, so its + # advertised node is the array itself rather than an ``anyOf`` with a null + # arm (#1547). + "control_media_buy": {("properties", "packages", "items", "properties", "targeting_overlay")}, "buy_products": {("properties", "purchases", "items", "properties", "targeting_overlay")}, } @@ -52,21 +55,31 @@ def test_advertised_targeting_mutations_keep_input_only_schema(tool: str) -> Non # Independent mutation-only reference. Canonical create/update schemas # intentionally sanitize nested creative identities across their graph. - expected = _inline_refs(TypeAdapter(TargetingOverlayInput | None).json_schema()) + # The overlay property is optional, not nullable: the bundled schema + # declares the input shape with no null arm, and the advertised node now + # states the same (#1547), so the reference is the input shape itself. + expected = _inline_refs(TypeAdapter(TargetingOverlayInput).json_schema()) if tool in {"create_media_buy", "update_media_buy"}: expected = sanitize_canonical_schema(expected) - assert len(expected["anyOf"]) == 2 - assert expected["anyOf"][0]["title"] == "TargetingOverlayInput" - assert expected["anyOf"][1] == {"type": "null"} + assert expected["title"] == "TargetingOverlayInput" + assert expected["type"] == "object" for path, node in nodes.items(): - assert set(node) == {"anyOf", "default", "description", "title"}, (tool, path) - # The runtime bridge makes this field non-ref-shaped, so Pydantic adds - # this annotation (30 bytes/site). Pin it rather than permitting an - # arbitrary amount of metadata outside the mutation-only anyOf. + assert set(node) == { + "additionalProperties", + "default", + "description", + "properties", + "title", + "type", + }, (tool, path) + # Inlining the reference merges the input object into the field node; + # the field keeps its own title and description. Pin the keys rather + # than permitting an arbitrary amount of metadata around the object. assert node["title"] == "Targeting Overlay", (tool, path) - assert len(node["anyOf"]) == 2, (tool, path) - assert node["anyOf"] == expected["anyOf"], (tool, path) + assert node["type"] == "object", (tool, path) + assert node["properties"] == expected["properties"], (tool, path) + assert node["additionalProperties"] == expected["additionalProperties"], (tool, path) assert node["default"] is None assert isinstance(node["description"], str) actual_bytes = json.dumps(node, sort_keys=True).encode() @@ -96,7 +109,7 @@ def test_advertised_targeting_fields_carry_the_description_the_schema_declares() t["inputSchema"] for t in ADCP_TOOL_DEFINITIONS if t["name"] == "create_media_buy" ) overlay = next(iter(dict(_targeting_nodes(_inline_refs(advertised))).values())) - fields = next(arm for arm in overlay["anyOf"] if "properties" in arm)["properties"] + fields = overlay["properties"] assert set(fields) == set( TargetingOverlayInput.model_json_schema()["properties"]