Commit 6b7da2a944d for woocommerce
commit 6b7da2a944de38e2a9f59aad1326038463fc33b2
Author: Francesco <frosso@users.noreply.github.com>
Date: Fri Sep 11 13:17:19 2026 +0200
feat: add raw_key to cart item_data in the Store API (#68388)
diff --git a/docs/apis/store-api/resources-endpoints/cart-items.md b/docs/apis/store-api/resources-endpoints/cart-items.md
index 987219fabf9..bb049916c14 100644
--- a/docs/apis/store-api/resources-endpoints/cart-items.md
+++ b/docs/apis/store-api/resources-endpoints/cart-items.md
@@ -190,6 +190,35 @@ curl "https://example-store.com/wp-json/wc/store/v1/cart/items"
]
```
+## Item data
+
+Each cart item carries display metadata in `item_data`, a list of entries built by callbacks on the `woocommerce_get_item_data` filter:
+
+| Property | Type | Description |
+| --- | --- | --- |
+| `raw_key` | string | Machine-readable name for the entry, set by the extension that added it. Never translated, so clients can match on it. |
+| `name` | string | Name of the metadata, for display. Some extensions send `key` instead. |
+| `value` | string | Value of the metadata. |
+| `display` | string | Optionally, how the value should be displayed. |
+
+`raw_key` was added in WooCommerce 11.2.0. Entries that omit it are unchanged.
+
+Most stores return an empty list. Entries appear when an extension adds them, as Product Add-Ons, Bookings, Deposits and Gift Cards do.
+
+Both `name` and `key` hold a label meant for a shopper to read, so both can be translated and neither is safe to match on. Use `raw_key` to find your own entry.
+
+Every value in an entry, `raw_key` included, is passed through [`wp_kses_post()`](https://developer.wordpress.org/reference/functions/wp_kses_post/). That rewrites some characters — a bare `&` arrives as `&`, so an equality check against the value you set will not match. Keep `raw_key` to lowercase letters, digits, `_`, `-` and `/`.
+
+Extensions can add properties beyond the four above through the same filter. Note that the endpoint checks each entry as a whole: if any one property holds a value that is not a scalar (an array, an object, `null`), the **entire entry** is dropped, not just that property.
+
+The order endpoint has an `item_data` list too, and it uses the opposite convention: there `key` is the raw metadata key and `display_key` is the label. See [Order](./order.md#item-data).
+
+### Data you do not want displayed
+
+`item_data` is for metadata shown next to the cart item. Anything else belongs in your `extensions` namespace on the cart item, which the Store API is built to carry. See [Cart Items](../extending-store-api/available-endpoints-to-extend.md#cart-items).
+
+Setting `hidden` on an entry keeps it out of the cart, the mini-cart and the classic templates, so this is not broken — but it leaves the data identified only by a label field, which is a poor fit for something never shown, and any client written against the schema still receives it.
+
## Single Cart Item
Get a single cart item by its key.
diff --git a/plugins/woocommerce/changelog/add-cart-item-data-raw-key b/plugins/woocommerce/changelog/add-cart-item-data-raw-key
new file mode 100644
index 00000000000..889daf981a2
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-cart-item-data-raw-key
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Store API: add `raw_key` to cart item `item_data` so extensions can identify their own entries without matching a translated label.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
index c8d0bc62484..b677041b1aa 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
@@ -17,6 +17,8 @@
export type ItemData = {
/** Raw (non-display) attribute name, used by variation entries. */
raw_attribute?: string | undefined;
+ /** Raw (non-display) name, used by item_data entries. */
+ raw_key?: string | undefined;
/** Raw (non-display) value. */
value?: string | undefined;
/** Display-ready value; preferred over `value` when present. */
diff --git a/plugins/woocommerce/client/blocks/changelog/add-cart-item-data-raw-key b/plugins/woocommerce/client/blocks/changelog/add-cart-item-data-raw-key
new file mode 100644
index 00000000000..889daf981a2
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/add-cart-item-data-raw-key
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Store API: add `raw_key` to cart item `item_data` so extensions can identify their own entries without matching a translated label.
diff --git a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
index 2b5c2d9ea91..254b3643d37 100644
--- a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
+++ b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
@@ -1096,6 +1096,8 @@ apply_filters( 'woocommerce_get_item_data', array $item_data, array $cart_item )
Allows extensions to attach their own name/value pairs to a cart item, which the Store API returns in the item's `item_data` field.
+Set `raw_key` so clients can find your entry without matching a translated label. Data you do not intend to display belongs in your `extensions` namespace.
+
### Parameters
| Argument | Type | Description |
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/types/type-defs/product-response.ts b/plugins/woocommerce/client/blocks/packages/public-api/types/type-defs/product-response.ts
index b6562c9d871..2847d20d4d8 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/types/type-defs/product-response.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/types/type-defs/product-response.ts
@@ -13,6 +13,12 @@ export interface ProductResponseItemPrices extends CurrencyResponse {
export interface ProductResponseItemBaseData {
value: string;
display?: string;
+ /**
+ * Machine-readable name for the entry, never translated, so it is safe to
+ * match on. Entity-encoded like every other value here, so a bare `&`
+ * arrives as `&`. Added in WooCommerce 11.2.0.
+ */
+ raw_key?: string;
/**
* Truthy marks the entry hidden. Always a string: the Store API runs every
* `item_data` value through `wp_kses_post()`, which string-coerces, so a
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
index 92a82df602a..d01650dd22f 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
@@ -160,6 +160,9 @@ class CartItemSchema extends ItemSchema {
* Allows extensions to attach their own name/value pairs to a cart item, which the Store
* API returns in the item's `item_data` field.
*
+ * Set `raw_key` so clients can find your entry without matching a translated label.
+ * Data you do not intend to display belongs in your `extensions` namespace.
+ *
* @since 4.3.0
*
* @internal Matches filter name in WooCommerce core.
@@ -171,9 +174,7 @@ class CartItemSchema extends ItemSchema {
$item_data = apply_filters( 'woocommerce_get_item_data', array(), $cart_item );
$clean_item_data = [];
foreach ( $item_data as $data ) {
- // We will check each piece of data in the item data element to ensure it is scalar. Extensions could add arrays
- // to this, which would cause a fatal in wp_strip_all_tags. If it is not scalar, we will return an empty array,
- // which will be filtered out in get_item_data (after this function has run).
+ // A non-scalar value would fatal in wp_strip_all_tags, so drop the whole entry.
foreach ( $data as $data_value ) {
if ( ! is_scalar( $data_value ) ) {
continue 2;
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ItemSchema.php
index 97dd79acaa2..aaf253aee5f 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ItemSchema.php
@@ -175,6 +175,12 @@ abstract class ItemSchema extends ProductSchema {
'items' => [
'type' => 'object',
'properties' => [
+ 'raw_key' => [
+ 'description' => __( 'Machine-readable name for the metadata, set by the extension that added it. Never translated, so clients can match on it.', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit' ],
+ 'readonly' => true,
+ ],
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
index 9b26bbc7aa5..46a6931a026 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
@@ -381,6 +381,108 @@ class CartItems extends ControllerTestCase {
$this->assertArrayHasKey( 'catalog_visibility', $data );
}
+ /**
+ * `raw_key` gives extensions a name to match on that is never translated.
+ *
+ * @testdox Cart item_data publishes raw_key and it matches the schema.
+ */
+ public function test_item_data_raw_key_is_published_and_matches_the_schema() {
+ $filter = function ( $item_data ) {
+ $item_data[] = array(
+ 'raw_key' => 'gift_message',
+ 'name' => 'Gift message',
+ 'value' => 'Happy birthday',
+ 'display' => 'Happy birthday!',
+ );
+
+ return $item_data;
+ };
+ add_filter( 'woocommerce_get_item_data', $filter );
+
+ try {
+ $routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
+ $controller = $routes->get( 'cart-items', 'v1' );
+ $cart = WC()->cart->get_cart();
+ $response = $controller->prepare_item_for_response( current( $cart ), new \WP_REST_Request() );
+
+ $entry = $response->get_data()['item_data'][0];
+ $this->assertSame( 'gift_message', $entry['raw_key'] );
+ $this->assertSame( 'Gift message', $entry['name'], 'The display label must be left alone.' );
+
+ $validate = new ValidateSchema( $controller->get_item_schema() );
+ $diff = $validate->get_diff_from_object( $response->get_data() );
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ $this->assertEmpty( $diff, print_r( $diff, true ) );
+ } finally {
+ remove_filter( 'woocommerce_get_item_data', $filter );
+ }
+ }
+
+ /**
+ * Extensions are told to keep `raw_key` to lowercase letters, digits, `_`, `-` and `/`.
+ * This pins what they get if they do not.
+ *
+ * @testdox Cart item_data raw_key is entity-encoded like every other value.
+ */
+ public function test_item_data_raw_key_is_entity_encoded() {
+ $filter = function ( $item_data ) {
+ $item_data[] = array(
+ 'raw_key' => 'acme&co_gift',
+ 'name' => 'Gift message',
+ 'value' => 'Happy birthday',
+ 'display' => 'Happy birthday!',
+ );
+
+ return $item_data;
+ };
+ add_filter( 'woocommerce_get_item_data', $filter );
+
+ try {
+ $routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
+ $controller = $routes->get( 'cart-items', 'v1' );
+ $cart = WC()->cart->get_cart();
+ $response = $controller->prepare_item_for_response( current( $cart ), new \WP_REST_Request() );
+
+ $this->assertSame( 'acme&co_gift', $response->get_data()['item_data'][0]['raw_key'] );
+ } finally {
+ remove_filter( 'woocommerce_get_item_data', $filter );
+ }
+ }
+
+ /**
+ * Entries written before `raw_key` existed must keep working. No schema validation
+ * here on purpose: the validator reports the fields such an entry omits.
+ *
+ * @testdox Cart item_data still passes through entries that set no raw_key.
+ */
+ public function test_item_data_without_raw_key_is_unchanged() {
+ $filter = function ( $item_data ) {
+ $item_data[] = array(
+ 'name' => 'gifting_to_hidden',
+ 'value' => 'recipient@example.com',
+ 'hidden' => true,
+ );
+
+ return $item_data;
+ };
+ add_filter( 'woocommerce_get_item_data', $filter );
+
+ try {
+ $routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
+ $controller = $routes->get( 'cart-items', 'v1' );
+ $cart = WC()->cart->get_cart();
+ $response = $controller->prepare_item_for_response( current( $cart ), new \WP_REST_Request() );
+
+ $entry = $response->get_data()['item_data'][0];
+ $this->assertArrayNotHasKey( 'raw_key', $entry, 'raw_key must not be invented.' );
+ $this->assertSame( 'gifting_to_hidden', $entry['name'] );
+ $this->assertSame( 'recipient@example.com', $entry['value'] );
+ $this->assertSame( '1', $entry['hidden'], 'hidden is string-coerced by wp_kses_post().' );
+ } finally {
+ remove_filter( 'woocommerce_get_item_data', $filter );
+ }
+ }
+
/**
* Test schema matches responses.
*