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 save | Shopify stores |
|---|---|
<p>café</p> | <p>café</p> |
<p>café</p> | <p>café</p> |
<p>x y</p> | a real non-breaking space, not the entity |
<p>x&y</p> | unchanged |
The invisible ones are the reason this matters more than it looks. becomes a real non-breaking space, ­ becomes a soft hyphen, and ‌ and ‍ 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 save | Shopify stores |
|---|---|
<p>Ben & Jerry</p> | <p>Ben & Jerry</p> |
<p>AT&T</p> | <p>AT&T</p> |
<p>5 < 6</p> | <p>5 < 6</p> |
Two related cases worth knowing. An entity with no semicolon, such as   written without one, is treated as a bare ampersand followed by text, so it is stored as &nbsp and the letters stay visible on your page. And a numeric entity that is well formed but names no valid character, such as �, 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.
- Tag and attribute names are lowercased:
<P>x</P>is stored as<p>x</p>, and<a HREF="/x">as<a href="/x">. - Attribute values are normalised to double quotes and given them if they had none:
<a href=/x>is stored as<a href="/x">. A duplicated attribute keeps the first occurrence. - Void elements lose a self-closing slash:
<br/>and<br />are both stored as<br>. - Unclosed and misnested tags are repaired.
<p>ais stored as<p>a</p>, and a stray closing tag on its own is deleted. - Newlines are inserted into lists. A one-line
<ul><li>a</li><li>b</li></ul>comes back with a line break before each<li>. Nothing you wrote asked for that.
What descriptions do not do
Equally useful, because it rules out explanations you might otherwise reach for:
- Line endings are not touched. Windows CRLF, lone carriage returns, tabs, runs of spaces, and leading or trailing whitespace all survive exactly. The only invisible characters removed are the control characters form feed and vertical tab.
- There is no Unicode normalization. An accented character written as one codepoint and the same character written as a letter plus a combining mark are both preserved, and stay different from each other. Emoji, right-to-left text and combining marks all survive.
- Sending null does nothing at all. Writing null to the description returns success and leaves the old description in place. The empty string is what clears it. This one catches out anyone whose import treats an empty cell as a deletion.
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.
| Unit | Decimal places kept | Example |
|---|---|---|
| Grams | 4 | 3.14159265 is stored as 3.1416 |
| Kilograms | 3 | 3.14159265 is stored as 3.142 |
| Ounces | 1 | 2.25 is stored as 2.3 |
| Pounds | 2 | 123.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 save | Stored on a USD shop | Stored on a JPY shop |
|---|---|---|
12.499 | 12.50 | 13 |
1000.50 | 1000.50 | 1001 |
1000 | 1000.00 | 1000 |
0.5 | 0.50 | 1 |
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 save | Shopify stores |
|---|---|
zeta, alpha | alpha, zeta (sorted) |
Sale, sale, SALE | Sale (first spelling wins) |
a single tag written a,b | two 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 save | Title | Vendor and product type |
|---|---|---|
padded | padded | padded |
a b (two spaces) | unchanged | a b (one space) |
a, newline, b | unchanged | a 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-& stays SKU-& 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 & 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.
- Four measurement scripts, several hundred distinct cases, and roughly 1,500 write-then-read pairs in the description and weight sweeps alone.
- Each corpus was run three times and the runs compared case by case, with one exception: the sweep covering titles, vendors, tags, identifiers and metafields ran twice, after a transient network failure killed its first run. Its two runs agree with each other case for case.
- Every case carried a fixed-point check: write the input, read the stored value, write that stored value back, read again.
- Every read was guarded against replica lag, so a slow read could not be recorded as a rule.
- Weights and prices only were measured on two store configurations differing in unit system, default weight unit, currency and plan. The weight table was byte-identical across both. The money results differed on all 24 cases, which is what proved money follows the currency.
- Everything else, including all of the description behaviour, was measured on one store: descriptions, tags, titles, vendors, product types, SKUs, barcodes and metafields. Whether the description parser behaves identically on a store with different settings was not tested. It is stated as a rule here because nothing about parsing HTML plausibly depends on a shop’s currency or unit system, but that is a judgement rather than a measurement, and the vendor caveat above is where the same judgement is explicitly not being made.
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.
WRITE → READ → MATCH ✓
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 ShopifyFAQ
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 é becomes an accented e and becomes a real non-breaking space, and it escapes a bare ampersand, so “Ben & Jerry” is stored as “Ben & 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.