Commit 9c1a56bc851 for woocommerce
commit 9c1a56bc851aef092f3b7f5aff7842f8741171ef
Author: Sam Seay <samueljseay@gmail.com>
Date: Fri Mar 6 00:13:50 2026 +1300
Add formatted_weight and formatted_dimensions to Store API ProductSchema (#63405)
* Add formatted_weight and formatted_dimensions to Store API ProductSchema
Adds locale-aware formatted display strings (e.g. "2.5 kg", "10 × 5 × 3 cm")
alongside the raw weight/dimensions fields. These use wc_format_weight() and
wc_format_dimensions() to match the formatting ProductSpecifications currently
gets via wp_interactivity_config, unblocking migration to Store API serialization.
* Add changefile(s) from automation for the following project(s): woocommerce
* Decode HTML entities in formatted_dimensions Store API response
wc_format_dimensions() joins with × (HTML entity). Decode to
Unicode × so JSON consumers don't need to handle entity decoding.
* Fix linting
* Match unit test `formatted_dimensions`
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63405-add-store-api-formatted-weight-dimensions b/plugins/woocommerce/changelog/63405-add-store-api-formatted-weight-dimensions
new file mode 100644
index 00000000000..ca792af514e
--- /dev/null
+++ b/plugins/woocommerce/changelog/63405-add-store-api-formatted-weight-dimensions
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add formatted_weight and formatted_dimensions to Store API ProductSchema
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/product-response.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/product-response.ts
index d1833091f66..55dbe6f2530 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/product-response.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/product-response.ts
@@ -86,6 +86,14 @@ export interface ProductResponseItem {
class: string;
};
sold_individually: boolean;
+ weight: string;
+ dimensions: {
+ length: string;
+ width: string;
+ height: string;
+ };
+ formatted_weight: string;
+ formatted_dimensions: string;
add_to_cart: {
text: string;
description: string;
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index 7160a1668ac..38ef7e1cece 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -51,68 +51,68 @@ class ProductSchema extends AbstractSchema {
*/
public function get_properties() {
return [
- 'id' => [
+ 'id' => [
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'name' => [
+ 'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'slug' => [
+ 'slug' => [
'description' => __( 'Product slug.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'parent' => [
+ 'parent' => [
'description' => __( 'ID of the parent product, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'type' => [
+ 'type' => [
'description' => __( 'Product type.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'variation' => [
+ 'variation' => [
'description' => __( 'Product variation attributes, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'permalink' => [
+ 'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'short_description' => [
+ 'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'description' => [
+ 'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'on_sale' => [
+ 'on_sale' => [
'description' => __( 'Is the product on sale?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'sku' => [
+ 'sku' => [
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
],
- 'prices' => [
+ 'prices' => [
'description' => __( 'Price data provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit', 'embed' ],
@@ -161,25 +161,25 @@ class ProductSchema extends AbstractSchema {
]
),
],
- 'price_html' => array(
+ 'price_html' => array(
'description' => __( 'Price string formatted as HTML.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
- 'average_rating' => [
+ 'average_rating' => [
'description' => __( 'Reviews average rating.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'review_count' => [
+ 'review_count' => [
'description' => __( 'Amount of reviews that the product has.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'images' => [
+ 'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -188,7 +188,7 @@ class ProductSchema extends AbstractSchema {
'properties' => $this->image_attachment_schema->get_properties(),
],
],
- 'categories' => [
+ 'categories' => [
'description' => __( 'List of categories, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -222,7 +222,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'tags' => [
+ 'tags' => [
'description' => __( 'List of tags, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -256,7 +256,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'brands' => [
+ 'brands' => [
'description' => __( 'List of brands, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -290,7 +290,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'attributes' => [
+ 'attributes' => [
'description' => __( 'List of attributes (taxonomy terms) assigned to the product. For variable products, these are mapped to variations (see the `variations` field).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -358,7 +358,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'variations' => [
+ 'variations' => [
'description' => __( 'List of variation IDs, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -396,7 +396,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'grouped_products' => [
+ 'grouped_products' => [
'description' => __( 'List of grouped product IDs, if applicable.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit', 'embed' ],
@@ -407,31 +407,31 @@ class ProductSchema extends AbstractSchema {
'readonly' => true,
],
],
- 'has_options' => [
+ 'has_options' => [
'description' => __( 'Does the product have additional options before it can be added to the cart?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'is_purchasable' => [
+ 'is_purchasable' => [
'description' => __( 'Is the product purchasable?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'is_in_stock' => [
+ 'is_in_stock' => [
'description' => __( 'Is the product in stock?', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'is_on_backorder' => [
+ 'is_on_backorder' => [
'description' => __( 'Is the product stock backordered? This will also return false if backorder notifications are turned off.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'stock_availability' => [
+ 'stock_availability' => [
'description' => __( 'Information about the product\'s availability.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit', 'embed' ],
@@ -451,19 +451,19 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'low_stock_remaining' => [
+ 'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'number', 'null' ],
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'sold_individually' => [
+ 'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
- 'weight' => [
+ 'weight' => [
'description' => sprintf(
/* translators: %s: weight unit */
__( 'Product weight (%s).', 'woocommerce' ),
@@ -473,7 +473,7 @@ class ProductSchema extends AbstractSchema {
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
- 'dimensions' => [
+ 'dimensions' => [
'description' => __( 'Product dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
@@ -511,7 +511,19 @@ class ProductSchema extends AbstractSchema {
],
],
],
- 'add_to_cart' => [
+ 'formatted_weight' => [
+ 'description' => __( 'Product weight formatted for display (e.g. "2.5 kg").', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit' ],
+ 'readonly' => true,
+ ],
+ 'formatted_dimensions' => [
+ 'description' => __( 'Product dimensions formatted for display (e.g. "10 × 5 × 3 cm").', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit' ],
+ 'readonly' => true,
+ ],
+ 'add_to_cart' => [
'description' => __( 'Add to cart button parameters.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit', 'embed' ],
@@ -562,7 +574,7 @@ class ProductSchema extends AbstractSchema {
],
],
],
- self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
+ self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
@@ -575,45 +587,47 @@ class ProductSchema extends AbstractSchema {
public function get_item_response( $product ) {
$availability = ProductAvailabilityUtils::get_product_availability( $product );
return [
- 'id' => $product->get_id(),
- 'name' => $this->prepare_html_response( $product->get_title() ),
- 'slug' => $product->get_slug(),
- 'parent' => $product->get_parent_id(),
- 'type' => $product->get_type(),
- 'variation' => $this->prepare_html_response( $product->is_type( ProductType::VARIATION ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ),
- 'permalink' => $product->get_permalink(),
- 'sku' => $this->prepare_html_response( $product->get_sku() ),
- 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
- 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
- 'on_sale' => $product->is_on_sale(),
- 'prices' => (object) $this->prepare_product_price_response( $product ),
- 'price_html' => $this->prepare_html_response( $product->get_price_html() ),
- 'average_rating' => (string) $product->get_average_rating(),
- 'review_count' => $product->get_review_count(),
- 'images' => $this->get_images( $product ),
- 'categories' => $this->get_term_list( $product, 'product_cat' ),
- 'tags' => $this->get_term_list( $product, 'product_tag' ),
- 'brands' => $this->get_term_list( $product, 'product_brand' ),
- 'attributes' => $this->get_attributes( $product ),
- 'variations' => $this->get_variations( $product ),
- 'grouped_products' => $this->get_grouped_products( $product ),
- 'has_options' => $product->has_options(),
- 'is_purchasable' => $product->is_purchasable(),
- 'is_in_stock' => $product->is_in_stock(),
- 'is_on_backorder' => ProductStockStatus::ON_BACKORDER === $product->get_stock_status(),
- 'low_stock_remaining' => $this->get_low_stock_remaining( $product ),
- 'stock_availability' => (object) array(
+ 'id' => $product->get_id(),
+ 'name' => $this->prepare_html_response( $product->get_title() ),
+ 'slug' => $product->get_slug(),
+ 'parent' => $product->get_parent_id(),
+ 'type' => $product->get_type(),
+ 'variation' => $this->prepare_html_response( $product->is_type( ProductType::VARIATION ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ),
+ 'permalink' => $product->get_permalink(),
+ 'sku' => $this->prepare_html_response( $product->get_sku() ),
+ 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
+ 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
+ 'on_sale' => $product->is_on_sale(),
+ 'prices' => (object) $this->prepare_product_price_response( $product ),
+ 'price_html' => $this->prepare_html_response( $product->get_price_html() ),
+ 'average_rating' => (string) $product->get_average_rating(),
+ 'review_count' => $product->get_review_count(),
+ 'images' => $this->get_images( $product ),
+ 'categories' => $this->get_term_list( $product, 'product_cat' ),
+ 'tags' => $this->get_term_list( $product, 'product_tag' ),
+ 'brands' => $this->get_term_list( $product, 'product_brand' ),
+ 'attributes' => $this->get_attributes( $product ),
+ 'variations' => $this->get_variations( $product ),
+ 'grouped_products' => $this->get_grouped_products( $product ),
+ 'has_options' => $product->has_options(),
+ 'is_purchasable' => $product->is_purchasable(),
+ 'is_in_stock' => $product->is_in_stock(),
+ 'is_on_backorder' => ProductStockStatus::ON_BACKORDER === $product->get_stock_status(),
+ 'low_stock_remaining' => $this->get_low_stock_remaining( $product ),
+ 'stock_availability' => (object) array(
'text' => $availability['availability'] ?? '',
'class' => $availability['class'] ?? '',
),
- 'sold_individually' => $product->is_sold_individually(),
- 'weight' => $product->get_weight(),
- 'dimensions' => (object) [
+ 'sold_individually' => $product->is_sold_individually(),
+ 'weight' => $product->get_weight(),
+ 'dimensions' => (object) [
'length' => $product->get_length(),
'width' => $product->get_width(),
'height' => $product->get_height(),
],
- 'add_to_cart' => (object) array_merge(
+ 'formatted_weight' => wc_format_weight( (float) $product->get_weight() ),
+ 'formatted_dimensions' => html_entity_decode( wc_format_dimensions( (array) $product->get_dimensions( false ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
+ 'add_to_cart' => (object) array_merge(
[
'text' => $this->prepare_html_response( $product->add_to_cart_text() ),
'description' => $this->prepare_html_response( $product->add_to_cart_description() ),
@@ -622,7 +636,7 @@ class ProductSchema extends AbstractSchema {
],
( new QuantityLimits() )->get_add_to_cart_limits( $product )
),
- self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER, $product ),
+ self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER, $product ),
];
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
index d604d3e6a11..49d5d3f2e0c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
@@ -76,6 +76,8 @@ class Products extends ControllerTestCase {
$this->assertEquals( $this->products[0]->get_length(), $data['dimensions']->length );
$this->assertEquals( $this->products[0]->get_width(), $data['dimensions']->width );
$this->assertEquals( $this->products[0]->get_height(), $data['dimensions']->height );
+ $this->assertEquals( wc_format_weight( (float) $this->products[0]->get_weight() ), $data['formatted_weight'] );
+ $this->assertEquals( html_entity_decode( wc_format_dimensions( (array) $this->products[0]->get_dimensions( false ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ), $data['formatted_dimensions'] );
$this->assertCount( 0, $data['grouped_products'] );
$this->assertCount( 1, $data['images'] );