feat: update models to UCP release 2026-04-08#28
feat: update models to UCP release 2026-04-08#28pjordan wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
Conversation
Regenerate Pydantic models from UCP specification release 2026-04-08 using `./generate_models.sh 2026-04-08`. This is a significant update from the previous 2026-01-23 release. Key changes include: - New Cart capability models (cart, cart_create/update_request) - New Catalog models (catalog_lookup, catalog_search) - New Order request variants (order_create/update_request) - Restructured ap2_mandate, buyer_consent, discount as packages - Updated type models: amount, signed_amount, variant, product, etc. - Error handling models (error_code, error_response) - Signals, pagination, media, and other new type models
There was a problem hiding this comment.
Code Review
This pull request significantly expands the UCP SDK models by adding schemas for shopping carts, catalog search and lookup, and order processing. It introduces environment signals for abuse prevention, refines the pricing model to use signed amounts for adjustments, and implements a more detailed totals breakdown system. Review feedback focuses on improving the maintainability of auto-generated code by reducing duplication in capability extension classes and questioning the use of empty model unions in totals request schemas, which may indicate issues in the source schema or generation logic.
| class Extends(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends1Item(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
|
|
||
|
|
||
| class Extends1(RootModel[list[Extends1Item]]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: list[Extends1Item] = Field(..., min_length=1) | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends2(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends3Item(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
|
|
||
|
|
||
| class Extends3(RootModel[list[Extends3Item]]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: list[Extends3Item] = Field(..., min_length=1) | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends4(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends5Item(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
|
|
||
|
|
||
| class Extends5(RootModel[list[Extends5Item]]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: list[Extends5Item] = Field(..., min_length=1) | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends6(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
||
| class Extends7Item(RootModel[str]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") | ||
|
|
||
|
|
||
| class Extends7(RootModel[list[Extends7Item]]): | ||
| model_config = ConfigDict( | ||
| frozen=True, | ||
| ) | ||
| root: list[Extends7Item] = Field(..., min_length=1) | ||
| """ | ||
| Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. | ||
| """ | ||
|
|
||
|
|
There was a problem hiding this comment.
There is significant code duplication in the newly added Extends* classes. For example, Extends, Extends2, Extends4, and Extends6 are identical. Similarly, Extends1, Extends3, Extends5, and Extends7 are identical, and their item types (Extends*Item) are also identical.
While this code is auto-generated, this duplication harms maintainability and readability. It would be better to define base types and reuse them, possibly through aliasing.
For example:
class ExtendsStr(RootModel[str]):
model_config = ConfigDict(frozen=True)
root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$")
class ExtendsListItem(RootModel[str]):
model_config = ConfigDict(frozen=True)
root: str = Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$")
class ExtendsList(RootModel[list[ExtendsListItem]]):
model_config = ConfigDict(frozen=True)
root: list[ExtendsListItem] = Field(..., min_length=1)
# Aliases for other Extends classes
Extends = ExtendsStr
Extends1 = ExtendsList
# ...and so onConsider adjusting the code generation process, perhaps in preprocess_schemas.py, to consolidate these redundant definitions. This would make the generated code much cleaner.
| class TotalsCreateRequest1(BaseModel): | ||
| """ | ||
| Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. | ||
| """ | ||
|
|
||
| model_config = ConfigDict( | ||
| extra="allow", | ||
| ) |
There was a problem hiding this comment.
The TotalsCreateRequest1 class is an empty BaseModel, and TotalsCreateRequest is a RootModel that unions a list with this empty model. This suggests that the totals can be either a list of total items or an empty object, which is an unusual and potentially confusing pattern.
If the intention is to allow an empty set of totals, it would be more idiomatic to make the list optional or allow an empty list, rather than unioning with an empty object type.
If this is an artifact of the code generation, it might be worth investigating if the source schema or the generation process can be adjusted to produce a clearer model.
| class TotalsUpdateRequest1(BaseModel): | ||
| """ | ||
| Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. | ||
| """ | ||
|
|
||
| model_config = ConfigDict( | ||
| extra="allow", | ||
| ) |
There was a problem hiding this comment.
Similar to totals_create_request.py, the TotalsUpdateRequest1 class is an empty BaseModel, and TotalsUpdateRequest is a RootModel that unions a list with this empty model. This is an unusual pattern.
If an empty set of totals is permissible, it would be clearer to allow an empty list. The current structure with a union to an empty object is confusing and could be a sign of an issue in the source schema or the code generation process.
Summary
2026-04-08(previously2026-01-23)generate_models.shorpreprocess_schemas.py— purely mechanical regeneration via./generate_models.sh 2026-04-08New models
cart.py,cart_create_request.py,cart_update_request.py)catalog_lookup.py,catalog_search.py)order_create_request.py,order_update_request.py)amount,signed_amount,variant,product,product_option,category,media,rating,pagination,error_code,error_response,signals,totals, and moreModified models
capability.py— expanded with cart, catalog, and order capabilitiesorder.py— currency now required, label field addedtotal.py— usessigned_amounttypecheckout.py/ request variants — updated fieldsembedded_config.py— new transport optionsap2_mandate,buyer_consent,discount) restructured as packagesVerification
Test plan
./generate_models.sh 2026-04-08completes without errorsCart,CatalogLookup,CatalogSearch, etc.)Checkout,Order) still import correctly2026-04-08spec