Skip to content

Improve TypeScript type generation for package definitions in create_media_buy #90

Description

@bokelley

Summary

The create_media_buy request schema uses anyOf with inline object properties, which prevents TypeScript code generators from creating useful type definitions. This affects all TypeScript implementations of the AdCP client.

Problem

Current Schema Structure

The package definition in static/schemas/v1/media-buy/create-media-buy-request.json (lines 18-63):

{
  "packages": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "buyer_ref": { "type": "string" },
        "products": { "type": "array", "items": { "type": "string" } },
        "format_ids": { "type": "array", "items": { "type": "string" } },
        "format_selection": { "type": "object" },
        "budget": { "$ref": "/schemas/v1/core/budget.json" },
        "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" },
        "creative_ids": { "type": "array", "items": { "type": "string" } }
      },
      "anyOf": [
        {"required": ["buyer_ref", "products", "format_ids"]},
        {"required": ["buyer_ref", "products", "format_selection"]}
      ],
      "additionalProperties": false
    }
  }
}

Impact on TypeScript Generation

When using standard TypeScript code generators like json-schema-to-typescript, this pattern produces:

packages: (
  | { [k: string]: unknown }
  | { [k: string]: unknown }
)[]

This provides zero type safety. TypeScript won't catch:

  • Missing required fields
  • Wrong field types
  • Invalid field names
  • Wrong data structures

Root Cause

The issue occurs when anyOf is used ONLY to vary the required fields, while all properties are defined inline above the anyOf. This pattern confuses code generators, which see conflicting type constraints and fall back to generic unknown types.

Impact

This affects:

  1. TypeScript client libraries - No compile-time validation
  2. IDE support - No autocomplete or inline documentation
  3. Developer experience - Runtime errors that should be caught at build time
  4. API adoption - Harder to use without proper types

Proposed Solutions

Solution 1: Extract Package Definition (Recommended)

Create a separate schema file for package definitions:

New file: static/schemas/v1/media-buy/package-request.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/v1/media-buy/package-request.json",
  "title": "Package Request",
  "description": "Package configuration for media buy creation",
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "buyer_ref": {
          "type": "string",
          "description": "Buyer's reference identifier for this package"
        },
        "products": {
          "type": "array",
          "description": "Array of product IDs to include in this package",
          "items": { "type": "string" }
        },
        "format_ids": {
          "type": "array",
          "description": "Array of format IDs that will be used for this package",
          "items": { "type": "string" }
        },
        "budget": { "$ref": "/schemas/v1/core/budget.json" },
        "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" },
        "creative_ids": {
          "type": "array",
          "description": "Creative IDs to assign to this package at creation time",
          "items": { "type": "string" }
        }
      },
      "required": ["buyer_ref", "products", "format_ids"],
      "not": { "required": ["format_selection"] },
      "additionalProperties": false
    },
    {
      "type": "object",
      "properties": {
        "buyer_ref": {
          "type": "string",
          "description": "Buyer's reference identifier for this package"
        },
        "products": {
          "type": "array",
          "description": "Array of product IDs to include in this package",
          "items": { "type": "string" }
        },
        "format_selection": {
          "type": "object",
          "description": "Dynamic format selection criteria"
        },
        "budget": { "$ref": "/schemas/v1/core/budget.json" },
        "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" },
        "creative_ids": {
          "type": "array",
          "description": "Creative IDs to assign to this package at creation time",
          "items": { "type": "string" }
        }
      },
      "required": ["buyer_ref", "products", "format_selection"],
      "not": { "required": ["format_ids"] },
      "additionalProperties": false
    }
  ]
}

Updated: static/schemas/v1/media-buy/create-media-buy-request.json

{
  "packages": {
    "type": "array",
    "description": "Array of package configurations",
    "items": {
      "$ref": "/schemas/v1/media-buy/package-request.json"
    }
  }
}

Benefits:

  • ✅ TypeScript generators produce proper union types
  • ✅ Package schema is reusable in other contexts (e.g., update_media_buy)
  • ✅ Cleaner separation of concerns
  • ✅ Better documentation structure
  • ✅ Used oneOf instead of anyOf to enforce mutual exclusivity

Why oneOf instead of anyOf?

Since format_ids and format_selection appear to be mutually exclusive (you specify formats either by explicit IDs or by selection criteria), oneOf is more semantically correct. It ensures that exactly ONE of the variants matches, not "any combination of them."

Solution 2: Use allOf with Conditionals (Alternative)

Keep the inline definition but restructure using allOf and if/then/else:

{
  "items": {
    "allOf": [
      {
        "type": "object",
        "properties": {
          "buyer_ref": { "type": "string" },
          "products": { "type": "array", "items": { "type": "string" } },
          "budget": { "$ref": "/schemas/v1/core/budget.json" },
          "targeting_overlay": { "$ref": "/schemas/v1/core/targeting.json" },
          "creative_ids": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["buyer_ref", "products"]
      },
      {
        "if": {
          "properties": {
            "format_ids": { "type": "array" }
          },
          "required": ["format_ids"]
        },
        "then": {
          "properties": {
            "format_ids": {
              "type": "array",
              "items": { "type": "string" }
            }
          },
          "not": { "required": ["format_selection"] }
        },
        "else": {
          "properties": {
            "format_selection": { "type": "object" }
          },
          "required": ["format_selection"],
          "not": { "required": ["format_ids"] }
        }
      }
    ],
    "additionalProperties": false
  }
}

Benefits:

  • ✅ No additional schema files
  • ✅ May work better with some generators

Drawbacks:

  • ⚠️ More complex to read
  • ⚠️ Not all generators handle if/then/else well

Breaking Change Assessment

Option 1: Extract to Separate File

Breaking: Only if implementations depend on the exact schema structure (unlikely)

Mitigation:

  • Release in next minor version (e.g., v1.7.0)
  • Document in release notes
  • Provide migration guide (though none should be needed)

Reality: This is a refactoring, not a behavioral change. The JSON validation rules remain identical.

Option 2: Use oneOf Instead of anyOf

Breaking: Potentially, if implementations were relying on ambiguous anyOf behavior

Mitigation:

  • Add comprehensive tests showing both variants work
  • Document the mutual exclusivity explicitly
  • Consider this for next major version (v2.0.0) if concerned

Test Cases

To verify the fix works correctly:

// Valid: Package with format_ids
{
  "buyer_ref": "pkg_123",
  "products": ["prod_1"],
  "format_ids": ["display_300x250"]
}

// Valid: Package with format_selection
{
  "buyer_ref": "pkg_456",
  "products": ["prod_1"],
  "format_selection": { "sizes": ["300x250", "728x90"] }
}

// Invalid: Package with both (should fail with oneOf)
{
  "buyer_ref": "pkg_789",
  "products": ["prod_1"],
  "format_ids": ["display_300x250"],
  "format_selection": { "sizes": ["300x250"] }
}

// Invalid: Package with neither (should fail)
{
  "buyer_ref": "pkg_000",
  "products": ["prod_1"]
}

Expected TypeScript Output

With the fix, TypeScript generators should produce:

type Package =
  | {
      buyer_ref: string;
      products: string[];
      format_ids: string[];
      budget?: Budget;
      targeting_overlay?: Targeting;
      creative_ids?: string[];
    }
  | {
      buyer_ref: string;
      products: string[];
      format_selection: object;
      budget?: Budget;
      targeting_overlay?: Targeting;
      creative_ids?: string[];
    };

interface CreateMediaBuyRequest {
  packages: Package[];
  // ... other fields
}

This provides:
✅ Proper union type for the two variants
✅ Type checking for required fields
✅ IDE autocomplete and documentation
✅ Compile-time validation

Implementation Checklist

  • Create package-request.json schema file
  • Update create-media-buy-request.json to use $ref
  • Add test cases for both package variants
  • Add test case for invalid package (both fields present)
  • Update documentation with examples
  • Add to CHANGELOG under "Improved"
  • Consider backporting to maintenance branches

Questions for Maintainers

  1. Is format_selection actually used in practice? If not, we could simplify by removing it entirely.

  2. Are format_ids and format_selection truly mutually exclusive? If so, oneOf is correct. If there's a valid use case for both, we should document it.

  3. Would you prefer Solution 1 (extract) or Solution 2 (allOf)? I recommend Solution 1 for clarity and reusability.

  4. Timeline preference? This could go in the next minor version (v1.7.0) as a non-breaking improvement.

Related Issues

This pattern appears in other parts of the schema as well. Once we establish the preferred approach, we should apply it consistently across:

  • Other tool request schemas
  • Core type definitions
  • Any other uses of inline anyOf with varying required fields

Offer to Help

I'm happy to:

  • Submit a PR implementing the chosen solution
  • Add comprehensive test cases
  • Update documentation
  • Test with multiple TypeScript code generators

References


Current Workaround

For now, TypeScript client libraries must manually define the Package interface to work around this limitation. This works but defeats the purpose of schema-driven type generation.

Conclusion

This is a relatively simple schema refactoring that would significantly improve the developer experience for all TypeScript implementations of AdCP. The change maintains backward compatibility while enabling proper type generation.


Metadata for Issue

Labels:

  • enhancement
  • schema
  • developer-experience
  • typescript
  • good-first-issue (if maintainers approve direction)

Milestone: v1.7.0 or v2.0.0 (depending on breaking change assessment)

Priority: Medium (affects DX but has workaround)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions