-
Notifications
You must be signed in to change notification settings - Fork 431
feat!: Support fractional quantities and unit measurement in lower funnel capabilities #597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b3d866c
a0f08a4
246a79b
f825c24
8d0ea2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "$id": "https://ucp.dev/schemas/common/types/measure.json", | ||
| "title": "Measure", | ||
| "description": "Represents a quantitative value paired with its physical or standardized unit of measurement.", | ||
| "type": "object", | ||
| "additionalProperties": true, | ||
| "required": ["value", "unit"], | ||
| "properties": { | ||
| "value": { | ||
| "type": "number", | ||
| "description": "Precise fractional quantity." | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The description for value is a bit narrow since value also covers whole counts and signed adjustments. Updating it to something like "The quantitative magnitude of the measurement (e.g., count, fractional amount, or signed adjustment quantity)" would be an improvement. |
||
| }, | ||
| "unit": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than a free-form A few reasons this feels like the right fit:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good callout! While it is consistent with other fields like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack that Rec 20's everyday-interpretability is a fair concern and human-readable symbols are probably the right call. What I think is missing is that UCP already has a house style for open strings with a known vocabulary and a long tail, and this field does not follow it yet. availability.status reads "Well-known values: in_stock, backorder, preorder, out_of_stock, discontinued." expectation.method_type reads "Well-known values: shipping, pickup, digital; additional values MAY be used." total.type enumerates subtotal, items_discount, discount, fulfillment, tax, fee, total and adds "Businesses MAY use additional values." policies[].type goes further with "Platforms MUST tolerate unknown values." The pattern is consistent across at least a dozen fields: enumerate the known values inline, then say what to do with the rest. By comparison, "SHOULD prefer using standard symbol (e.g., kg for kilogram, lb for pound)" is illustrative rather than enumerative, and silent on unknowns, so lb and lbs, or floz and fl oz, remain equally compliant. That matters because the unit is not just display text here, it qualifies numbers that get compared and summed. order_line_item.status is derived from quantity.fulfilled == quantity.total, total moves with signed adjustment quantities, and fulfilled accumulates from fulfillment_event quantities, so a consumer has to know whether two quantities are in the same unit before it can do any of that. To a machine, lb and lbs are two different units. The same applies to unit pricing, where measure.unit and reference.unit have to agree for "per 100ml" to render correctly. Left open, every platform writes its own synonym table for every business it integrates, and the protocol could settle it once instead. The drift is not hypothetical. The count token alone is ct at Google, each at Instacart, and ITEM at Shopify. Worth noting Shopify's UnitPriceMeasurement mirrors variant.unit_price almost exactly (quantityValue and quantityUnit, referenceValue and referenceUnit), and it constrains both unit fields to a closed enum of 25 readable tokens like LB, KG, and OZ. So would giving unit the same treatment? Concretely, enumerate the common units inline in the description for weight, volume, length, and area, keep the existing MUST for each, and add the usual clause that businesses MAY use additional values for long tail units like bunch or head. That stays a description change rather than a schema one, so it avoids the enum maintenance you flagged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here’s a potential compromise proposal that bridges the gap between structured standardization and readability: We could anchor our unit vocabulary on UNECE Rec 20 (Codes for Units of Measure), but instead of using the opaque alphanumeric codes (e.g., For example:
Why this works:
|
||
| "type": "string", | ||
| "description": "Unit of measurement. MUST use 'each' to represent countable goods and SHOULD prefer using standard symbol (e.g., kg for kilogram, lb for pound) for other units." | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,13 +84,8 @@ | |
| "description": "ISO 4217 currency code." | ||
| }, | ||
| "measure": { | ||
| "type": "object", | ||
| "description": "Product quantity in packaging (e.g., 750ml bottle).", | ||
| "required": ["value", "unit"], | ||
| "properties": { | ||
| "value": { "type": "number", "description": "Package quantity." }, | ||
| "unit": { "type": "string", "description": "Unit of measurement." } | ||
| } | ||
| "$ref": "../../common/types/measure.json", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to make this an array of saleable units to support the grocery use case? e.g. with a wide variety of units a business may offer multiple units e.g. "bags" and "ounces" of black beans?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good callout! Some thoughts:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For reference, the way we handle this within our business, is that you have 1 unit and unit price per sku (variant). This generally needs to be the case for a couple of reasons, but the two major ones are inventory tracking and analytics. For our grocery customers, they do a lot of buy online pick up in store. If we are computing inventory on two different selling units for a single product, it becomes more complex and risk the change of over selling. |
||
| "description": "Product quantity in packaging (e.g., 750ml bottle)." | ||
| }, | ||
| "reference": { | ||
| "type": "object", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we put the step quantity in here as well? For example, selling in 0.01 increments of grams. When this is used by one of the catalog APIs, it is hint to the UI on what the purchasable quantity is. (i.e a drop down in some cases). It can default to 1 and it can also support client side validation.
Do we need to make units mandatory? Can it just be optional? If we make it optional, then instead of using "anyOf" when referencing this, can we just get rid of the integer version and keep this in a backwards compatible way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good idea! But I think we need to give it a bit more thought on how this would fit in with respect to where measurement is being used in catalog today before introducing the field (adding it in a follow-up should be a non-breaking change).
This PR simply refactors out the construct from
variant.unit_price.measure, based on where it is being used (as part ofunit_price), I don't think it makes sense (at this stage) to introducestep_quantity.I think for any quantity measurement that goes beyond whole number counts,
unitis required to remove any ambiguity on what the quantity is being measured against (and that's also why I slightly prefer theanyOfcomposition more since it cleanly differentiate between the whole number counts from fractional quantities). Note that even if we makeunitoptional and drop the integer version, this still won't be a backwards compatible change as we are changing the semantics/type of the originalquantityfield (and that would be considered a breaking change in UCP per https://ucp.dev/latest/specification/overview/#breaking-changes).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that making
unitoptional is the weaker path, and that dropping the integer branch is breaking regardless.But I think there is a third option that keeps your disambiguation requirement while still collapsing the union: a single
measuretype whereunitstays required, and countable goods carry a count unit rather than a separate integer branch. For example{ "value": 2, "unit": "each" }for eggs and{ "value": 1.5, "unit": "lb" }for weighed goods, instead ofanyOf[integer, measure]. This is exactly how schema.orgQuantitativeValueand GS1 model it, treating count as just another unit.The trade this makes: every consumer reads one shape (
value+unit) with no integer-or-object branching across the five schemas, andorder_line_itemno longer has to carry two parallel object shapes that cannot mix (integertotalwith a measuredfulfilled). Since this PR is alreadyfeat!, the breaking cost is being spent either way.The main downside is verbosity on the common countable case, but a readable count token like
each(rather than a raw code) keeps{ "value": 2, "unit": "each" }close to2in practice. Would it be worth weighing this unified shape against theanyOf, given it preserves the required-unit guarantee you want but leaves consumers with a single parsing path?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I was thinking about this a little differently. While going from an integer to a number might change a code generator or change an SDK, relaxing from an integer to a decimal is backwards compatible from a JSON standpoint.
Also, if we may unit mandatory, and set a default value of "each", wouldn't that also satisfy the backwards compatibility requirement? There appears to be precedence of default values within the schemas already (unless I am misinterpreting something).
Also, if we are not telling the platform what the limits are, how will it know that can't send quantities more granular then we support? i.e. if we are selling in int 0.01 increments, but the platform sends the business a 0.001 quantity then it will get rejected and end up with shopper facing error.
Happy to chat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@amithanda The third option makes sense and fair callout on this PR already
feat!so the breaking cost is already spent! The only worry I have with replacing the definition entirely is that this will introduce one additional layer of nestedness with the data structure for 90% of the eCommerce use cases we have (whenunit=each). The existinganyOfwould keep all current integrations compliant, and only disambiguate for the 10% scenario where a weighted/fractional quantity is needed. I personally feel like introducing another breaking change in the future to collapse the definition is "easier" than having to revert back this design when we realize it won't perform well with agent reasoning.@sdedeo2025 Even using a default value on
unitwon't make this change backwards compatible because we are changing the semantics of the field - instead ofquantity: 1, we will now havequantity: { "value": 1 }.While it's not an optimized experience, an available path here is for business to return a recoverable error outlining what is an acceptable quantity (maybe also the
step_quantityas part of the description) so platform can reason and prompt/surface recovery paths to the shopper. This is somewhat similar to what we have today when platform requests for more quantities than what business has in stock.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I lean toward @amithanda's point here that carrying duplicate
anyOf[integer, Measure]blocks across 5 separate schemas creates unnecessary duplication and risks future pattern inconsistency.Since UCP already uses polymorphic shared types where appropriate (such as
fulfillment_destination.jsonencapsulating shipping vs. retail location), an alternative is encapsulating the union insidecommon/types/measure.jsonitself.What this looks like in practice:
1. Centralized
measure.json:{ "$id": "https://ucp.dev/schemas/common/types/measure.json", ... "oneOf": [ { "type": "integer", "description": "Item quantity with an implied unit of 'each'." }, { "type": "object", "required": ["value", "unit"], "additionalProperties": true, "properties": { "value": { "type": "number", "description": "The quantitative magnitude of the measurement." }, "unit": { "type": "string", "description": "Unit of measurement (e.g. each, kilogram, ...)." } } } ] }2. Clean
$refacross shopping schemas:In
line_item.json,expectation.json,fulfillment_event.json,adjustment.json:3. Payloads:
"quantity": 2(un-nested, lightweight primitive)"quantity": { "value": 1.5, "unit": "lb" }This approach eliminates schema duplication* by using just
$refmeasure.jsonin place of theanyOfblock and preserves simple primitives by still allowing compact representation of typical e-commerce purchases that are implicitly "each" (quantity: 2) .If we prefer avoiding polymorphism, I would bias towards using a single
Measureobject with an optional defaultunit: "each"to limit verbosity.