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 &times; (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'] );