Summary
The codegen aspect of the SDK is a really good idea to keep up with the spec details. It works well with the files inside of ucp_sdk.models.schemas.shopping. However, the results in ucp, capability, payment_handler, and service end up being difficult to work with as a developer using and building the validated models.
While there are some different reasons for this, the main one appears to be an overuse of RootModel (and a reliance on redundant BaseModels). This could be improved by limiting RootModel usage with specific Pydantic features.
Motivation
As the SDK exists in v0.3.0 (2026-01-23), the models work with UcpMetadata.model_validate(data) and .model_dump when working with text io. However, the validated models end up constantly referring to my_model.root when used in the code. RootModel was not intended to be used as a JSON/BaseModel replacement. It’s specifically meant to model non-JSON data types when used as input/output models in applications like FastAPI. This isn't necessarily an issue on its own, but it get's combined with the constant type unions used to define the RootModels that Pydantic interprets in a first-come-first-served fashion.
If the codebase (like a FastAPI server) uses UcpMetadata as an input or output model, the dev is required to use isinstance checks on the validated model to see which internal model Pydantic assigned. Because these sub-models have identical key-value fields in the BaseModel spec (like payment_handler.BusinessSchema and business_handler.ResponseSchema), the dev could easily be in a position where a Response-based flow checks for isinstance(model.root.capabilities[0], ResponseSchema) and fail because Pydantic assigned it to the identical BusinessSchema instead.
There are smaller cases of RootModel usage that are less impactful but can still cause issues. For example, using RootModel for Version and ReverseDomainName can cause a FastAPI server to export:
{
"version": "root='2026-01-23'",
"services": {
"root='dev.ucp.shopping'": [
{
"version": "root='2026-01-23'",
"spec": "https://ucp.dev/2026-01-23/specification/service",
This is an example where RootModel would be the correct decision if Version and ReverseDomainName were meant to be used as FastAPI input/output models, but instead the SDK uses them as field validators causing this issue.
Goals
- Prevent Pydantic validation from assigning to identical model definitions
- Fix
root= prefixes appearing in FastAPI output
- Provide usage example(s) in library README
- Initial integration test suite to validate across codegen runs
Non-Goals
No response
Detailed Design
First, RootModel should not be used in the context of this SDK. Since the models contained in the SDK are all JSON objects according to the spec, BaseModel is sufficient for modeling purposes. This on its own removes the .root and root= handling.
It could be as simple as replacing the RootModel.jinja2 template with one for BaseModel or to fold the definition into the existing BaseModel creation in the Python script.
Second, for items like Version and ReverseDomainName used as field validators, this can be implemented using Annotated fields instead.
from typing import Annotated
from pydantic import BaseModel, Field
# Can be defined once as annotated types and reused / imported elsewhere
Version = Annotated[str, Field(pattern="^\\d{4}-\\d{2}-\\d{2}$")]
ReverseDomainName = Annotated[str, Field(pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$")]
# Can be used just like the RootModel versions are now
class Metadata(BaseModel):
version: Version
services: dict[ReverseDomainName, list[dict]]
These type definitions could exist in their own file for import elsewhere. This is most notable for a field like Version which has multiple copies in each file the validator is found in. This redundant definition makes it harder to work with when client libraries create a high-level ...ucp.Version instance and have to keep redefining it for each submode’s unique definition of Version.
Third, the high level models like UcpMetadata (models which are much more likely to be accessed by end users developing UCP servers) should have explicit model definitions. For example, UCP v2026-01-11 had a model for UcpDiscoveryProfile. This approach made it much easier for a developer to implement with libraries like FastAPI because the UcpDiscoveryProfile only referenced the .capabilities model that the profile actually used instead of acting as a UCP meta catch-all for the other types like Checkout and Order responses.
This particular approach may benefit from explicit model definitions of templates to guarantee a solid and expected end-user API while letting the rest of the BaseModel generation handle submodes like all of the shopping stubs. The existing duplication of Base/Platform/Business models would be replaced by Base and child classes with a traditional class hierarchy.
One-hot model definitions like service.PlatformSchema (models 1 through 7 ???) would also improve from this touch as well. This is a much clearer model for development/implementation:
from typing import Literal
from pydantic import BaseModel, HttpUrl
from cup_sdk.models.schemas.types import SchemaUrl, Version
class ServiceBase(BaseModel):
version: Version
spec: HttpUrl
schema_: SchemaUrl
…
class McpService(BaseService):
transport: Literal[“mcp”]
endpoint: HttpUrl
class EmbeddedService(BaseService):
transport: Literal[“embedded”]
Risks and Mitigations
Since this change is focussed on providing a more consistent runtime model, it should be closing more potential security holes than it opens. In theory, there should be a slight improvement to performance by limiting the number of potential submodels that Pydantic needs to validate against before a potential match or throwing a ValidationError.
Backward compatibility should be straightforward since the 0.3.0 version of the library really only supports the .model_validate approach. That should be the initial test case to validate backwards compatibility (will list in test below).
However, this feature request does add some additional complexity to the current codegen-only approach. However, I think it’s best to add this kind of complexity (for a consistent API boundary) earlier on while the UCP spec is being developed.
Test Plan
Since there currently isn’t a test suite for the Python SDK (at least as shown in the code repo), starting with an e2e test against a known JSON payload and UcpMetadata.model_validate should be sufficient to start with for initial development of this feature.
I’d highly recommend adding additional e2e tests for other common dev patterns like FastAPI request/response models and native model construction. For example:
from fastapi import FastAPI
From ucp_sdk
from tests.util import make_ucp_manifest
app = FastAPI()
@app.get(“/.well-known/ucp”)
def ucp_manifest() -> UcpMetadata: # UCP SDK type used in FastAPI type definition
return make_ucp_manifest()
@pytest_asyncio.fixture()
async def client() -> AsyncIterator[AsyncClient]:
"""Async server client that handles lifespan and teardown."""
async with LifespanManager(app): # noqa SIM117
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as _client:
try:
yield _client
finally:
await clear(app) # any necessary clean up between test cases
def test_ucp_manifest(client: httpx.AsyncClient):
app_manifest: dict = await client.get("/.well-known/ucp”).json()
test_manifest = make_ucp_manifest()
# Test validated models
assert UcpMetadata.model_validate(app_manifest) == test_manifest
# Test JSON outputs
assert app_manifest = test_manifest.model_dump()
Graduation Criteria
Working Draft → Candidate:
Candidate → Stable:
Implementation History
- [YYYY-MM-DD]: Proposal submitted.
- [YYYY-MM-DD]: TC approved "Provisional"; capability enters "Working Draft".
- [YYYY-MM-DD]: TC approved advancement to "Candidate".
- [YYYY-MM-DD]: TC approved "Implemented"; capability enters "Stable".
Code of Conduct
Summary
The codegen aspect of the SDK is a really good idea to keep up with the spec details. It works well with the files inside of
ucp_sdk.models.schemas.shopping. However, the results inucp,capability,payment_handler, andserviceend up being difficult to work with as a developer using and building the validated models.While there are some different reasons for this, the main one appears to be an overuse of RootModel (and a reliance on redundant BaseModels). This could be improved by limiting RootModel usage with specific Pydantic features.
Motivation
As the SDK exists in v0.3.0 (2026-01-23), the models work with
UcpMetadata.model_validate(data)and.model_dumpwhen working with text io. However, the validated models end up constantly referring tomy_model.rootwhen used in the code. RootModel was not intended to be used as a JSON/BaseModel replacement. It’s specifically meant to model non-JSON data types when used as input/output models in applications like FastAPI. This isn't necessarily an issue on its own, but it get's combined with the constant type unions used to define the RootModels that Pydantic interprets in a first-come-first-served fashion.If the codebase (like a FastAPI server) uses UcpMetadata as an input or output model, the dev is required to use
isinstancechecks on the validated model to see which internal model Pydantic assigned. Because these sub-models have identical key-value fields in the BaseModel spec (like payment_handler.BusinessSchema and business_handler.ResponseSchema), the dev could easily be in a position where a Response-based flow checks forisinstance(model.root.capabilities[0], ResponseSchema)and fail because Pydantic assigned it to the identicalBusinessSchemainstead.There are smaller cases of RootModel usage that are less impactful but can still cause issues. For example, using RootModel for
VersionandReverseDomainNamecan cause a FastAPI server to export:{ "version": "root='2026-01-23'", "services": { "root='dev.ucp.shopping'": [ { "version": "root='2026-01-23'", "spec": "https://ucp.dev/2026-01-23/specification/service",This is an example where RootModel would be the correct decision if
VersionandReverseDomainNamewere meant to be used as FastAPI input/output models, but instead the SDK uses them as field validators causing this issue.Goals
root=prefixes appearing in FastAPI outputNon-Goals
No response
Detailed Design
First, RootModel should not be used in the context of this SDK. Since the models contained in the SDK are all JSON objects according to the spec, BaseModel is sufficient for modeling purposes. This on its own removes the
.rootandroot=handling.It could be as simple as replacing the
RootModel.jinja2template with one forBaseModelor to fold the definition into the existingBaseModelcreation in the Python script.Second, for items like
VersionandReverseDomainNameused as field validators, this can be implemented usingAnnotatedfields instead.These type definitions could exist in their own file for import elsewhere. This is most notable for a field like Version which has multiple copies in each file the validator is found in. This redundant definition makes it harder to work with when client libraries create a high-level
...ucp.Versioninstance and have to keep redefining it for each submode’s unique definition ofVersion.Third, the high level models like
UcpMetadata(models which are much more likely to be accessed by end users developing UCP servers) should have explicit model definitions. For example, UCP v2026-01-11 had a model for UcpDiscoveryProfile. This approach made it much easier for a developer to implement with libraries like FastAPI because theUcpDiscoveryProfileonly referenced the.capabilitiesmodel that the profile actually used instead of acting as a UCP meta catch-all for the other types like Checkout and Order responses.This particular approach may benefit from explicit model definitions of templates to guarantee a solid and expected end-user API while letting the rest of the BaseModel generation handle submodes like all of the shopping stubs. The existing duplication of Base/Platform/Business models would be replaced by Base and child classes with a traditional class hierarchy.
One-hot model definitions like
service.PlatformSchema(models 1 through 7 ???) would also improve from this touch as well. This is a much clearer model for development/implementation:Risks and Mitigations
Since this change is focussed on providing a more consistent runtime model, it should be closing more potential security holes than it opens. In theory, there should be a slight improvement to performance by limiting the number of potential submodels that Pydantic needs to validate against before a potential match or throwing a ValidationError.
Backward compatibility should be straightforward since the 0.3.0 version of the library really only supports the
.model_validateapproach. That should be the initial test case to validate backwards compatibility (will list in test below).However, this feature request does add some additional complexity to the current codegen-only approach. However, I think it’s best to add this kind of complexity (for a consistent API boundary) earlier on while the UCP spec is being developed.
Test Plan
Since there currently isn’t a test suite for the Python SDK (at least as shown in the code repo), starting with an e2e test against a known JSON payload and
UcpMetadata.model_validateshould be sufficient to start with for initial development of this feature.I’d highly recommend adding additional e2e tests for other common dev patterns like FastAPI request/response models and native model construction. For example:
Graduation Criteria
Working Draft → Candidate:
Candidate → Stable:
Implementation History
Code of Conduct