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:
- TypeScript client libraries - No compile-time validation
- IDE support - No autocomplete or inline documentation
- Developer experience - Runtime errors that should be caught at build time
- 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
Questions for Maintainers
-
Is format_selection actually used in practice? If not, we could simplify by removing it entirely.
-
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.
-
Would you prefer Solution 1 (extract) or Solution 2 (allOf)? I recommend Solution 1 for clarity and reusability.
-
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)
Summary
The
create_media_buyrequest schema usesanyOfwith 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:This provides zero type safety. TypeScript won't catch:
Root Cause
The issue occurs when
anyOfis used ONLY to vary therequiredfields, while all properties are defined inline above theanyOf. This pattern confuses code generators, which see conflicting type constraints and fall back to genericunknowntypes.Impact
This affects:
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:
update_media_buy)oneOfinstead ofanyOfto enforce mutual exclusivityWhy
oneOfinstead ofanyOf?Since
format_idsandformat_selectionappear to be mutually exclusive (you specify formats either by explicit IDs or by selection criteria),oneOfis 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
allOfandif/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:
Drawbacks:
if/then/elsewellBreaking Change Assessment
Option 1: Extract to Separate File
Breaking: Only if implementations depend on the exact schema structure (unlikely)
Mitigation:
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
anyOfbehaviorMitigation:
Test Cases
To verify the fix works correctly:
Expected TypeScript Output
With the fix, TypeScript generators should produce:
This provides:
✅ Proper union type for the two variants
✅ Type checking for required fields
✅ IDE autocomplete and documentation
✅ Compile-time validation
Implementation Checklist
package-request.jsonschema filecreate-media-buy-request.jsonto use$refQuestions for Maintainers
Is
format_selectionactually used in practice? If not, we could simplify by removing it entirely.Are
format_idsandformat_selectiontruly mutually exclusive? If so,oneOfis correct. If there's a valid use case for both, we should document it.Would you prefer Solution 1 (extract) or Solution 2 (allOf)? I recommend Solution 1 for clarity and reusability.
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:
anyOfwith varyingrequiredfieldsOffer to Help
I'm happy to:
References
json-schema-to-typescript: https://github.com/bcherny/json-schema-to-typescriptjson-schema-to-ts: https://github.com/ThomasAribart/json-schema-to-tsCurrent 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:
enhancementschemadeveloper-experiencetypescriptgood-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)