Repository: Shopify/hydrogen (2025-08-14T22:23:03Z)

Issue:
parseMetafield works incorrectly with 'money' type metafields
### What is the location of your example repository?

_No response_

### Which package or tool is having this issue?

Hydrogen

### What version of that package or tool are you using?

2025.5.0

### What version of Remix are you using?

react router 7.6.0

### Steps to Reproduce

Query the product metafield like this:

```typescript
const QUERY = `#graphql
    bundle: product(handle: "my-bundle") {
      originalPrice: metafield( namespace: "custom", key: "my_audiobook_bundle_original_price") {
        value
        type
      }
      selectedOrFirstAvailableVariant {
        price {
          amount
          currencyCode
        }
      }
    }
  }
` as const;

const {bundle} = await storefront.query(QUERY)
```

This returns everything as expected:

```json
{
  "bundle": {
    "originalPrice": {
      "value": "{\"amount\":\"30.0\",\"currency_code\":\"USD\"}",
      "type": "money"
    },
    "selectedOrFirstAvailableVariant": {
      "price": {
        "amount": "20.0",
        "currencyCode": "USD"
      }
    }
  }
}
```

Now try parsing the metafield with `parseMetafield` from `'@shopify/hydrogen'`:

```typescript
const parsedOriginalPrice = parseMetafield<ParsedMetafields['money']>({
      ...bundle.originalPrice,
    }).parsedValue;
```
`parsedOriginalPrice` is inferred as `MoneyV2`, but it actually looks like this:

```json
{
  "amount": "20.0",
  "currency_code": "USD"
}
```


### Expected Behavior

`currency_code` is parsed to `currencyCode` on the object if it gets inferred as `MoneyV2`.

### Actual Behavior

`bundle.price` is correctly inferred as `MoneyV2`, and it has the `currencyCode` property.
`parsedOriginalPrice` is **incorrectly** inferred as `MoneyV2`, since `currency_code` is not parsed to `currencyCode` by `parseMetafield`.


Discussion:

Patch:
diff --git a/.changeset/fix-parsemetafield-money-currency.md b/.changeset/fix-parsemetafield-money-currency.md
new file mode 100644
index 0000000000..8db48f2a2b
--- /dev/null
+++ b/.changeset/fix-parsemetafield-money-currency.md
@@ -0,0 +1,11 @@
+---
+"@shopify/hydrogen-react": patch
+---
+
+Fixed parseMetafield to correctly handle money type metafields with currency_code
+
+- Transform currency_code (from Storefront API) to currencyCode (expected by MoneyV2 type)
+- Maintain backward compatibility for metafields already using currencyCode
+- Add tests for both snake_case and camelCase formats
+
+Fixes #3071
\ No newline at end of file
diff --git a/packages/hydrogen-react/src/parse-metafield.ts b/packages/hydrogen-react/src/parse-metafield.ts
index b56070be63..cda03dc1b4 100644
--- a/packages/hydrogen-react/src/parse-metafield.ts
+++ b/packages/hydrogen-react/src/parse-metafield.ts
@@ -1,5 +1,6 @@
 import type {
   Collection,
+  CurrencyCode,
   GenericFile,
   Metafield as MetafieldBaseType,
   MoneyV2,
@@ -63,8 +64,43 @@ export function parseMetafield<ReturnGeneric>(
       } as ReturnGeneric;
 
     // TODO: 'money' should probably be parsed even further to like `useMoney()`, but that logic needs to be extracted first so it's not a hook
+    case 'money': {
+      let parsedValue: MoneyV2 | null = null;
+      try {
+        const parsed = parseJSON(metafield.value ?? '');
+        // Transform currency_code from Storefront API to currencyCode to match MoneyV2 type
+        if (parsed && typeof parsed === 'object' && 'currency_code' in parsed) {
+          const moneyData = parsed as {amount: string; currency_code: string};
+          parsedValue = {
+            amount: moneyData.amount,
+            currencyCode: moneyData.currency_code as CurrencyCode,
+          };
+        } else if (
+          parsed &&
+          typeof parsed === 'object' &&
+          'currencyCode' in parsed
+        ) {
+          // Already in correct format (for backward compatibility)
+          parsedValue = parsed as MoneyV2;
+        } else {
+          parsedValue = parsed as MoneyV2 | null;
+        }
+      } catch (err) {
+        const parseError = `parseMetafield(): attempted to JSON.parse the 'metafield.value' property, but failed.`;
+        if (__HYDROGEN_DEV__) {
+          throw new Error(parseError);
+        } else {
+          console.error(`${parseError} Returning 'null' for 'parsedValue'`);
+        }
+        parsedValue = null;
+      }
+      return {
+        ...metafield,
+        parsedValue,
+      } as ReturnGeneric;
+    }
+
     case 'dimension':
-    case 'money':
     case 'json':
     case 'rating':
     case 'volume':


Test patch:
diff --git a/packages/hydrogen-react/src/parse-metafield.test.ts b/packages/hydrogen-react/src/parse-metafield.test.ts
index 572803436a..6c65bf551c 100644
--- a/packages/hydrogen-react/src/parse-metafield.test.ts
+++ b/packages/hydrogen-react/src/parse-metafield.test.ts
@@ -134,7 +134,7 @@ describe(`parseMetafield`, () => {
       expectType<null | MyJson>(parsedOtherType.parsedValue);
     });
 
-    it(`money`, () => {
+    it(`money with currencyCode (backward compatibility)`, () => {
       const parsed = parseMetafield<ParsedMetafields['money']>({
         type: 'money',
         value: JSON.stringify({amount: '12', currencyCode: 'USD'}),
@@ -144,6 +144,16 @@ describe(`parseMetafield`, () => {
       expectType<null | MoneyV2>(parsed?.parsedValue);
     });
 
+    it(`money with currency_code (Storefront API format)`, () => {
+      const parsed = parseMetafield<ParsedMetafields['money']>({
+        type: 'money',
+        value: JSON.stringify({amount: '30.0', currency_code: 'USD'}),
+      });
+      expect(parsed?.parsedValue?.amount === '30.0').toBe(true);
+      expect(parsed?.parsedValue?.currencyCode === 'USD').toBe(true);
+      expectType<null | MoneyV2>(parsed?.parsedValue);
+    });
+
     it(`rich_text_field`, () => {
       const parsed = parseMetafield<ParsedMetafields['rich_text_field']>({
         type: 'rich_text_field',