Bulk editing guides

Why Shopify changed your product data when you saved it

Updated September 2026

You typed one value, Shopify saved another. A description comes back with an accented letter where you wrote é. A weight of 2.25 oz is sitting in the admin as 2.3. A price you entered as 12.499 reads 12.50, and a tag list you entered in your own order comes back alphabetical and one tag shorter.

None of this is a bug in the tool you used, and none of it is random. Shopify rewrites certain fields as it stores them, the rewrite is different for every field, and each one follows a rule you can predict. This page gives the rule for each field, with one before and after per rule. Every number here comes from writing values into a Shopify store through the Admin API and reading back what was actually stored, not from documentation. The method is at the end.

Descriptions: the field with a parser behind it

The product description is the field that surprises people most, because it is not stored as the text you sent. Shopify parses it as an HTML fragment and serializes it again, and everything below is a consequence of that round trip.

That distinction matters for what this page can promise. The character-level rules (entities, escaping, whitespace, attribute formatting, Unicode) are closed and stated as rules below. What the parser does to arbitrary markup is not a rule anyone can write down from a finite number of examples, so the markup changes further down are labelled as observations and nothing more.

HTML entities are decoded into characters

Every named entity is decoded into the character it stands for, with three exceptions: &, < and > survive unchanged, because those three characters have to stay escaped in HTML text. " and ' are decoded, to a plain double and single quote, because in text content they do not need escaping. Numeric entities are all decoded too, decimal or hex, in any case, zero-padded or not.

You saveShopify stores
<p>caf&eacute;</p><p>café</p>
<p>caf&#233;</p><p>café</p>
<p>x&nbsp;y</p>a real non-breaking space, not the entity
<p>x&amp;y</p>unchanged

The invisible ones are the reason this matters more than it looks. &nbsp; becomes a real non-breaking space, &shy; becomes a soft hyphen, and &zwnj; and &zwj; become a zero-width non-joiner and a zero-width joiner. Your page renders identically and your stored value is different, so a tool that compares what it sent against what is now in the store will report a difference you cannot see anywhere on screen.

A bare ampersand is escaped

The other direction of the same round trip. An & that does not begin a valid entity is escaped on the way out, so a vendor name goes in as one string and is stored as another.

You saveShopify stores
<p>Ben & Jerry</p><p>Ben &amp; Jerry</p>
<p>AT&T</p><p>AT&amp;T</p>
<p>5 < 6</p><p>5 &lt; 6</p>

Two related cases worth knowing. An entity with no semicolon, such as &nbsp written without one, is treated as a bare ampersand followed by text, so it is stored as &amp;nbsp and the letters stay visible on your page. And a numeric entity that is well formed but names no valid character, such as &#xD800;, is deleted outright rather than escaped.

Markup gets reformatted (observed, not a complete rule)

These are cases that were measured directly. They are consistent and repeatable, but the full behaviour of an HTML parser cannot be captured in a list, so treat them as examples of the kind of thing that happens rather than as the whole rule.

What descriptions do not do

Equally useful, because it rules out explanations you might otherwise reach for:

Weights: decimal places depend on the unit

The rule: the value is rounded half-up, away from zero, to a fixed number of decimal places that depends only on the unit. The unit itself is stored exactly as written and is never converted.

UnitDecimal places keptExample
Grams43.14159265 is stored as 3.1416
Kilograms33.14159265 is stored as 3.142
Ounces12.25 is stored as 2.3
Pounds2123.456 is stored as 123.46

Ounces keeping a single decimal place is the one worth remembering, and it is the case that started this investigation: a live edit writing 2.25 oz was stored as 2.3, while 12.5 oz and 0.79 lb in the same batch round-tripped exactly. Two decimal places is a perfectly ordinary way to write an ounce weight, and it is one more than the field keeps. Rounding is half-up rather than banker’s rounding, which was checked directly: in ounces, 0.05 through 0.95 all round up, where banker's rounding would have sent half of them down.

Two more results from the same measurement. A negative weight is refused with an error and nothing is stored. And sending null as the weight, like sending null as the description, returns success and changes nothing, so a weight cannot be cleared that way. Set it to 0 instead.

If you are correcting weights across a catalogue, the unit column matters as much as the number: bulk editing SKUs, barcodes and weights covers the file format side of it.

Prices: decimal places depend on your currency

The rule: money is rounded half-up to the number of decimal places your shop's currency keeps, and padded out to that many. Not to two places, and not to the number of places you typed. To your currency's.

You saveStored on a USD shopStored on a JPY shop
12.49912.5013
1000.501000.501001
10001000.001000
0.50.501

This was measured on two shops, one on USD and one on JPY, and all 24 test values differed between them. Not one agreed, which is what establishes that the precision follows the currency rather than being a fixed platform behaviour.

The practical consequence has nothing to do with which currency you are on: anyone who types more decimal places than their currency keeps gets a different number than the one they typed. On a currency that keeps no decimal places at all, which is the case measured here on JPY, that means every price with a decimal point in it. On USD it means three decimals, which sounds unlikely until you are calculating a price from a margin or a currency conversion and your spreadsheet hands you 12.499.

Leading zeros, a leading plus sign, surrounding spaces and exponent notation are all normalised away as well: 012.50, +12.50 and 1e3 are stored as 12.50, 12.50 and 1000.00 on a USD shop.

Tags: split, deduplicated and sorted

The rule: every tag is trimmed, empty tags are dropped, a tag containing a comma is split into several tags, duplicates are removed case-insensitively with the first occurrence winning, and the surviving list is returned sorted.

You saveShopify stores
zeta, alphaalpha, zeta (sorted)
Sale, sale, SALESale (first spelling wins)
a single tag written a,btwo tags, a and b
  padded  padded

The sorting is harmless and the trimming is usually what you wanted. The two that change your data are the comma split, so one tag you meant literally becomes two, and the case-insensitive dedupe, so a file containing both Sale and sale silently loses one of them. If you are consolidating near-duplicate tags on purpose, bulk tag cleanup covers the add-then-remove order that avoids losing products from automated collections halfway through.

Titles, vendors and product types

The rule: all of them are trimmed at both ends and have zero-width characters stripped. Vendor and product type also collapse runs of internal whitespace, including tabs and line breaks, into a single space. Title, SEO title and SEO description do not.

You saveTitleVendor and product type
  padded  paddedpadded
a  b (two spaces)unchangeda b (one space)
a, newline, bunchangeda b

Emptying these four fields gives four different results, which is worth knowing before an import writes blank cells across your catalogue. An empty title is refused outright with “Title can’t be blank”. Product type accepts an empty string and stores it. SEO title and SEO description accept it and store null.

Vendor is the one to be careful with. Saving an empty vendor does not empty it. On the store this was measured against, the value that came back was that store’s own shop name. Whether the replacement is always the shop name, or a default that happened to equal it there, was not established, so this is the one finding on this page that should be read as a warning rather than a rule: do not expect a blank vendor cell to clear a vendor, and check what landed if you try.

Title, vendor and product type are all capped at 255 characters and reject anything longer. The SEO fields accepted 1000 characters without complaint, so Shopify’s guidance about search-listing lengths is advice rather than validation.

SKUs, barcodes and metafields

SKU and barcode are trimmed, and a value that is empty after trimming is stored as null. Nothing else is touched. Internal spaces, tabs, letter case and entity-looking text all survive exactly, so SKU-&amp; stays SKU-&amp; rather than being decoded the way a description would be. Over 255 characters is rejected. A cell containing only spaces becomes null, because the trim happens first.

Metafields split into two groups. Text, boolean and URL types are stored byte for byte, leading and trailing spaces included, which is the opposite of every other field on this page. The numeric and JSON types are canonicalised: 042 in an integer field is stored as 42, 1.50 in a decimal field is stored as 1.5, 3 becomes 3.0, and a JSON object has its whitespace stripped while its key order is preserved. An empty string in a single-line text metafield is rejected rather than stored.

The contrast with descriptions is the thing to carry away: entity decoding happens in the description and nowhere else. a &amp; b stays exactly that in a title, a vendor, a tag, a SKU and every metafield type.

All of it is deterministic and repeatable

Two properties were checked on every single case, because they are what make the rest of this page useful rather than anecdotal.

The same input always gives the same stored value. Every corpus was run repeatedly and every case agreed across every run. There were no disagreements anywhere.

The transform is stable. Every case was written a second time, using the value Shopify had stored, and came back unchanged. Applying the rule twice is the same as applying it once.

For you that means a save that changed your value is not a transient failure and retrying will not fix it. If 2.25 oz came back as 2.3, sending 2.25 again gives 2.3 again, forever. The only way to store a value is to send one the rules leave alone. That is also why a tool that reports these as errors and retries them is not going to converge, and why a bulk edit that reports success without checking can leave a catalogue in a state nobody has looked at.

How we measured this

Everything above comes from writing values into Shopify development stores through the Admin GraphQL API (version 2026-01) and reading back what was stored, using the same mutations and the same read shape SureEdit itself uses. No part of it is quoted from documentation.

What this does not cover: country and locale were the same on both stores, so neither was varied. The full markup behaviour of the description parser is not enumerable, as noted in that section. And for tags, the rule that the first spelling wins was established for case variants; which spelling survives in one specific Unicode case was not tested.

Seeing the stored value before you write it

Everything on this page is a reason a bulk edit can report a difference that is nobody’s mistake. The useful response is not to loosen the check until it stops complaining. It is to show the value Shopify will actually store, before writing anything.

SureEdit does that for the rules it has measured. In both the wizard preview and the CSV import diff, a value that Shopify will rewrite is shown as the value it will become, with the reason in plain language beside it: “Shopify stores weights in oz to 1 decimal place, so this is saved as 2.3 oz”. That covers weights, tags, trimming on the text fields, SKU and barcode, leading zeros on integer metafields, and the entity and ampersand rules on descriptions that contain no raw HTML tags.

Where a rule is not fully measured, it declines and writes your value exactly as you typed it rather than guessing. That is deliberate, and it is why a blank vendor, a description containing markup, and decimal and JSON metafields carry no prediction. Money is a case of its own: SureEdit does not predict it, but it writes prices at the precision your shop is already using rather than the precision you happened to type, and tells you when that changes the number.

After the write, every value is read back from Shopify and compared to what was asked for. A difference is reported as a mismatch rather than explained away, which is how each of the rules on this page was found in the first place.

WRITEREADMATCH ✓

Run this edit with proof, not hope

SureEdit shows the value Shopify will actually store before it writes, re-reads every write to confirm it, and keeps one-click undo. Free up to 50 products per edit.

Install free on Shopify

FAQ

Why did Shopify change my product description after I saved it?
Shopify parses the description as HTML and writes it back out again. On that round trip it decodes HTML entities into the characters they stand for, so &eacute; becomes an accented e and &nbsp; becomes a real non-breaking space, and it escapes a bare ampersand, so “Ben & Jerry” is stored as “Ben &amp; Jerry”. The text your customers see is the same. The stored characters are not.

Why did Shopify round my weight from 2.25 oz to 2.3 oz?
Weight is rounded half-up to a fixed number of decimal places that depends on the unit: 4 for grams, 3 for kilograms, 1 for ounces, 2 for pounds. Ounces keep one decimal place, so 2.25 rounds away from zero to 2.3. The unit is stored exactly as you wrote it and is never converted.

Why does Shopify change the decimals on my price?
Prices are rounded half-up to the number of decimal places your shop’s currency keeps, and padded out to that many. On USD, 12.499 is stored as 12.50 and 12 is stored as 12.00. On a zero-decimal currency such as JPY, 1500.50 is stored as 1501. Typing more decimal places than your currency keeps always gives you a different number than the one you typed.

Is this random, or will the same value always change the same way?
It is deterministic. Every corpus in this measurement was run repeatedly, most of them three times, and every case agreed across every run with no disagreements anywhere. Each case was also written back a second time and stored unchanged, so the transform is stable: applying it twice gives the same result as applying it once. Retrying a save does not get you a different answer.