Skip to content

feat: update models to UCP release 2026-04-08#28

Open
pjordan wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
pjordan:worktree-feat-update-spec-2026-04-08
Open

feat: update models to UCP release 2026-04-08#28
pjordan wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
pjordan:worktree-feat-update-spec-2026-04-08

Conversation

@pjordan
Copy link
Copy Markdown

@pjordan pjordan commented Apr 16, 2026

Summary

  • Regenerate Pydantic models from UCP specification release 2026-04-08 (previously 2026-01-23)
  • No changes to generate_models.sh or preprocess_schemas.py — purely mechanical regeneration via ./generate_models.sh 2026-04-08

New models

  • Cart capability (cart.py, cart_create_request.py, cart_update_request.py)
  • Catalog operations (catalog_lookup.py, catalog_search.py)
  • Order request variants (order_create_request.py, order_update_request.py)
  • New types: amount, signed_amount, variant, product, product_option, category, media, rating, pagination, error_code, error_response, signals, totals, and more

Modified models

  • capability.py — expanded with cart, catalog, and order capabilities
  • order.py — currency now required, label field added
  • total.py — uses signed_amount type
  • checkout.py / request variants — updated fields
  • embedded_config.py — new transport options
  • Extension schemas (ap2_mandate, buyer_consent, discount) restructured as packages

Verification

  • All models import successfully
  • No changes to build tooling or preprocessing logic

Test plan

  • ./generate_models.sh 2026-04-08 completes without errors
  • All new models import successfully (Cart, CatalogLookup, CatalogSearch, etc.)
  • Existing models (Checkout, Order) still import correctly
  • Reviewer verifies generated output matches expectations for 2026-04-08 spec

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
@pjordan pjordan requested review from a team as code owners April 16, 2026 18:05
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +143
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.
"""


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 on

Consider adjusting the code generation process, perhaps in preprocess_schemas.py, to consolidate these redundant definitions. This would make the generated code much cleaner.

Comment on lines +52 to +59
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",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment on lines +52 to +59
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",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

@ptiper ptiper requested a review from cusell-google April 16, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant