Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/core/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ def model_dump(self, **kwargs):
kwargs["exclude_none"] = True
return super().model_dump(**kwargs)

def model_dump_json(self, **kwargs):
"""Dump model to JSON string with AdCP-compliant defaults.

By default, excludes None values to match AdCP spec where optional fields
should be omitted rather than set to null. This prevents JSON validation
errors from AdCP consumers that use "additionalProperties": false and don't
allow null for optional fields.

Examples:
response = ListAuthorizedPropertiesResponse(publisher_domains=["example.com"])
# Only includes publisher_domains, omits all None-valued optional fields
json_str = response.model_dump_json() # exclude_none=True by default
"""
if "exclude_none" not in kwargs:
kwargs["exclude_none"] = True
return super().model_dump_json(**kwargs)


class TaskStatus(str, Enum):
"""Standardized task status enum per AdCP MCP Status specification.
Expand Down
88 changes: 88 additions & 0 deletions tests/unit/test_adcp_json_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Test that AdCP responses correctly exclude None values in JSON serialization.

This is critical for client compatibility - the AdCP client (adcp/client npm package)
validates responses against JSON schemas that don't allow null for optional fields.

Issue: PR #xxx - list_authorized_properties returned null for optional fields
Fix: AdCPBaseModel.model_dump_json() now defaults to exclude_none=True
"""

import json

from src.core.schema_adapters import (
GetProductsResponse,
ListAuthorizedPropertiesResponse,
ListCreativeFormatsResponse,
)


def test_list_authorized_properties_excludes_none_in_json():
"""Test that model_dump_json() excludes None values by default.

This prevents schema validation errors in the AdCP client which expects
optional fields to be omitted (not set to null).
"""
# Create response with only required field (all optional fields will be None)
response = ListAuthorizedPropertiesResponse(publisher_domains=["example.com"])

# Serialize to JSON
json_str = response.model_dump_json()
parsed = json.loads(json_str)

# Verify None fields are not present in JSON
assert "publisher_domains" in parsed
assert "primary_channels" not in parsed # Should be excluded (None)
assert "primary_countries" not in parsed # Should be excluded (None)
assert "portfolio_description" not in parsed # Should be excluded (None)
assert "advertising_policies" not in parsed # Should be excluded (None)
assert "last_updated" not in parsed # Should be excluded (None)
assert "errors" not in parsed # Should be excluded (None)


def test_adcp_response_includes_explicit_values():
"""Test that explicitly set values are included in JSON."""
response = ListAuthorizedPropertiesResponse(
publisher_domains=["example.com"],
primary_channels=["display", "video"],
advertising_policies="No tobacco or alcohol",
)

json_str = response.model_dump_json()
parsed = json.loads(json_str)

# Verify explicitly set fields are included
assert parsed["publisher_domains"] == ["example.com"]
assert parsed["primary_channels"] == ["display", "video"]
assert parsed["advertising_policies"] == "No tobacco or alcohol"

# Verify unset fields are still excluded
assert "primary_countries" not in parsed
assert "portfolio_description" not in parsed


def test_model_dump_also_excludes_none():
"""Test that model_dump() (dict) also excludes None by default."""
response = ListAuthorizedPropertiesResponse(publisher_domains=["example.com"])

dump = response.model_dump()

# Verify None fields are not present
assert "publisher_domains" in dump
assert "primary_channels" not in dump
assert "primary_countries" not in dump
assert "portfolio_description" not in dump


def test_other_responses_also_exclude_none():
"""Verify all AdCP response types exclude None values."""
# GetProductsResponse
products_resp = GetProductsResponse(products=[])
products_json = json.loads(products_resp.model_dump_json())
assert "products" in products_json
# Should not have None-valued optional fields

# ListCreativeFormatsResponse
formats_resp = ListCreativeFormatsResponse(formats=[])
formats_json = json.loads(formats_resp.model_dump_json())
assert "formats" in formats_json
# Should not have None-valued optional fields