Summary
Add an optional attributes array on Product and Variant for measurable facts like battery life, display size, weight, and storage. Each entry has a required name and value, plus optional numeric_value and unit. Same inline shape as the existing Variant.barcodes. About 30 lines of schema across two files. Additive: no new endpoints, transports, or request parameters.
The shape matches schema.org PropertyValue, which is what most catalog teams already publish via JSON-LD on their product pages. For them, adoption is mostly piping existing data into UCP catalog responses.
Motivation
An agent gets the query "phone with at least 20 hours of battery, under $800." The catalog response usually contains the answer but it's stuck in prose: "all-day battery", "lightweight at 1.2 kg".
tags doesn't carry units. metadata accepts anything, but every merchant picks a different shape, which is the same fragmentation problem as parsing prose.
attributes gives a structured place for measurable facts. numeric_value lets agents filter without parsing display strings.
This isn't a vendor-specific use case. Every catalog has the same gap, and a vendor-namespaced version would be the same field with a different prefix, which doesn't help the cross-merchant comparison problem.
Goals
- Get measurable attributes out of prose into structured form on
Product and Variant.
- Expose a typed numeric field so agents can filter without LLM extraction.
- Stay additive. No breaking changes, no new endpoints, no new transports.
Non-Goals
- Defining a UCP-canonical vocabulary of attribute names. Names follow merchant convention and schema.org practice. Vertical extensions can standardize identifiers later.
- Server-side filtering. Filtering happens agent-side over the catalog response.
- Localization of attribute names. Display strings come from the rendering layer.
- Range expressions. v1 is single values. Range siblings can land later.
- Categorical attributes (color, fit, material). Those go in
tags or product options.
Detailed Design
Schema
"attributes": {
"type": "array",
"description": "Structured product attributes for measurable facts (battery life, display size, weight). Use `tags` for categorical labels and `metadata` for business-specific data. Same shape as schema.org `PropertyValue`.",
"items": {
"type": "object",
"required": ["name", "value"],
"properties": {
"name": {
"type": "string",
"description": "Human-readable attribute name (e.g. `Battery life`, `Display`, `Weight`). Same as schema.org `PropertyValue.name`."
},
"value": {
"type": "string",
"description": "Display value. Same as schema.org `PropertyValue.value`. Can be a number, range, or domain notation."
},
"numeric_value": {
"type": "number",
"description": "Optional comparable numeric form of `value`. Businesses SHOULD populate this when the attribute is a single measurement so agents can filter without parsing the display string."
},
"unit": {
"type": "string",
"description": "Optional unit of measurement (e.g. `hours`, `kg`, `inches`, `GB`). Open vocabulary; clients MUST tolerate unknown values."
}
}
}
}
Same field on Variant. Inline structured array, no new schema files. Same pattern as Variant.barcodes.
Placement
Attributes constant across all variants go on Product (display size, refresh rate). Attributes that vary by variant go on Variant (storage, weight by size). Clients read whichever level is populated. If both are populated, variant wins.
Cross-capability impact
attributes lives on the existing Product and Variant schemas. It propagates wherever those types appear today, which includes catalog responses and Variant references in cart and order line items. No transport bindings, request shapes, or other capabilities change.
Worked example
{
"id": "prod_aurora_phone",
"title": "Aurora Phone",
"description": { "plain": "Flagship phone with all-day battery." },
"price_range": {
"min": { "amount": 79900, "currency": "USD" },
"max": { "amount": 89900, "currency": "USD" }
},
"attributes": [
{ "name": "Battery life", "value": "22", "numeric_value": 22, "unit": "hours" },
{ "name": "Display", "value": "6.1", "numeric_value": 6.1, "unit": "inches" }
],
"variants": [
{
"id": "var_aurora_128",
"title": "128 GB",
"description": { "plain": "Aurora Phone, 128 GB storage." },
"price": { "amount": 79900, "currency": "USD" },
"attributes": [
{ "name": "Storage", "value": "128", "numeric_value": 128, "unit": "GB" }
]
},
{
"id": "var_aurora_256",
"title": "256 GB",
"description": { "plain": "Aurora Phone, 256 GB storage." },
"price": { "amount": 89900, "currency": "USD" },
"attributes": [
{ "name": "Storage", "value": "256", "numeric_value": 256, "unit": "GB" }
]
}
]
}
Risks and Mitigations
Backward compatibility. Additive on Product and Variant. Existing producers keep working. Existing consumers ignore the new field. Same profile as adding any optional field.
Vocabulary fragmentation. Open name vocabulary means catalog teams pick inconsistent names early. Cross-merchant filtering should lean on numeric_value plus unit (both bounded enough to compare meaningfully) rather than exact name matching. The schema.org PropertyValue ecosystem has converging conventions for names over time.
Adoption gap. No required population. Useful when populated, harmless when absent, same as tags and barcodes.
Codegen. Same shape as Variant.barcodes. No new pattern.
Security and performance. No new attack surface, no new request paths, payload growth bounded. Negligible.
Test Plan
Schema validation: required name and value enforced, missing fields rejected, entries with and without each optional field validate. Existing fixtures without attributes validate unchanged.
Sample fixture in the conformance suite: a phone product with populated attributes on both product and variant levels.
Pydantic models in python_sdk regenerate cleanly.
Graduation Criteria
Working Draft to Candidate:
Candidate to Stable:
Implementation History
- [2026-04-30]: Proposal submitted.
Summary
Add an optional
attributesarray onProductandVariantfor measurable facts like battery life, display size, weight, and storage. Each entry has a requirednameandvalue, plus optionalnumeric_valueandunit. Same inline shape as the existingVariant.barcodes. About 30 lines of schema across two files. Additive: no new endpoints, transports, or request parameters.The shape matches schema.org
PropertyValue, which is what most catalog teams already publish via JSON-LD on their product pages. For them, adoption is mostly piping existing data into UCP catalog responses.Motivation
An agent gets the query "phone with at least 20 hours of battery, under $800." The catalog response usually contains the answer but it's stuck in prose: "all-day battery", "lightweight at 1.2 kg".
tagsdoesn't carry units.metadataaccepts anything, but every merchant picks a different shape, which is the same fragmentation problem as parsing prose.attributesgives a structured place for measurable facts.numeric_valuelets agents filter without parsing display strings.This isn't a vendor-specific use case. Every catalog has the same gap, and a vendor-namespaced version would be the same field with a different prefix, which doesn't help the cross-merchant comparison problem.
Goals
ProductandVariant.Non-Goals
tagsor productoptions.Detailed Design
Schema
Same field on
Variant. Inline structured array, no new schema files. Same pattern asVariant.barcodes.Placement
Attributes constant across all variants go on
Product(display size, refresh rate). Attributes that vary by variant go onVariant(storage, weight by size). Clients read whichever level is populated. If both are populated, variant wins.Cross-capability impact
attributeslives on the existingProductandVariantschemas. It propagates wherever those types appear today, which includes catalog responses andVariantreferences in cart and order line items. No transport bindings, request shapes, or other capabilities change.Worked example
{ "id": "prod_aurora_phone", "title": "Aurora Phone", "description": { "plain": "Flagship phone with all-day battery." }, "price_range": { "min": { "amount": 79900, "currency": "USD" }, "max": { "amount": 89900, "currency": "USD" } }, "attributes": [ { "name": "Battery life", "value": "22", "numeric_value": 22, "unit": "hours" }, { "name": "Display", "value": "6.1", "numeric_value": 6.1, "unit": "inches" } ], "variants": [ { "id": "var_aurora_128", "title": "128 GB", "description": { "plain": "Aurora Phone, 128 GB storage." }, "price": { "amount": 79900, "currency": "USD" }, "attributes": [ { "name": "Storage", "value": "128", "numeric_value": 128, "unit": "GB" } ] }, { "id": "var_aurora_256", "title": "256 GB", "description": { "plain": "Aurora Phone, 256 GB storage." }, "price": { "amount": 89900, "currency": "USD" }, "attributes": [ { "name": "Storage", "value": "256", "numeric_value": 256, "unit": "GB" } ] } ] }Risks and Mitigations
Backward compatibility. Additive on
ProductandVariant. Existing producers keep working. Existing consumers ignore the new field. Same profile as adding any optional field.Vocabulary fragmentation. Open
namevocabulary means catalog teams pick inconsistent names early. Cross-merchant filtering should lean onnumeric_valueplusunit(both bounded enough to compare meaningfully) rather than exactnamematching. The schema.orgPropertyValueecosystem has converging conventions for names over time.Adoption gap. No required population. Useful when populated, harmless when absent, same as
tagsandbarcodes.Codegen. Same shape as
Variant.barcodes. No new pattern.Security and performance. No new attack surface, no new request paths, payload growth bounded. Negligible.
Test Plan
Schema validation: required
nameandvalueenforced, missing fields rejected, entries with and without each optional field validate. Existing fixtures withoutattributesvalidate unchanged.Sample fixture in the conformance suite: a phone product with populated
attributeson both product and variant levels.Pydantic models in
python_sdkregenerate cleanly.Graduation Criteria
Working Draft to Candidate:
Attributessection with placement guidance and the worked example.python_sdk.attributes.Candidate to Stable:
Implementation History