The GS separator in GS1 codes: what it is and where it gets lost
GS is an invisible character with code 29 that marks where a variable-length field ends. Without it the code string cannot be split into fields, and a point of sale, a pharmacy verification system or an ERP rejects the code even though the DataMatrix itself reads perfectly.
Why the separator is needed
In a GS1 code the fields follow one another with no spaces: first the two-to-four-digit Application Identifier (AI), then the value. Some AIs have a length fixed by the standard — the GTIN in AI 01 is always 14 digits, the expiry date in AI 17 is always 6 — so a parser knows where they end. Others are variable: a batch number (AI 10) or a serial number (AI 21) can be up to 20 characters, and without a separator there is no way to tell where it ends and the next field begins.
The rule: a separator follows every variable-length field that is followed by another field. The last field needs no separator.
[FNC1] 01 09506000134352 17 271231 10 A1B2C3 [GS] 21 7Kd93mQ2xZ
└ fixed, 14 digits ┘ └fixed 6┘ └ variable ┘ └ last field — no GSThis is the typical layout of a pharmaceutical pack code (GTIN, expiry, batch, serial number). Put the batch last and the separator moves: it then follows the serial number instead. Codes of the Russian Chestny ZNAK system use the same rule with their own fields:
| Structure | Where GS goes |
|---|---|
| 01 + 17 + 10 + 21 (typical pharma pack) | After the batch number (10) |
| 01 + 21 + 91 + 92 (Chestny ZNAK, long code) | After the serial number and after key 91 |
| 01 + 21 + 93 (Chestny ZNAK, short code) | After the serial number only |
FNC1 and GS — the difference
Inside a DataMatrix symbol there is a special codeword, FNC1 (codeword 232). It does two jobs:
- At the start of the code FNC1 declares the symbol a GS1 code. The scanner does not transmit it, but with the symbology identifier prefix enabled it starts the string with
]d2instead of]d1for a plain DataMatrix. - In the middle of the code FNC1 is a field separator, and the scanner transmits it as the GS character, ASCII 29 (Packaging Digest).
Some label software encodes the GS character itself in the middle instead of FNC1. A scanner returns the same string in both cases, but a print quality verifier may flag it as a deviation from GS1. The leading FNC1, however, has no substitute: without it the code is a plain DataMatrix, and a GS1 system will not accept it.
Where GS gets lost
The separator is a control character, and any link in the chain that treats it as garbage silently strips it:
| Link | What happens | What to do |
|---|---|---|
| Scanner in keyboard emulation mode | GS is sent as a key combination the application ignores or interprets its own way | Switch the scanner to virtual COM port mode, or configure it to replace GS with a printable character the application turns back into GS |
| CSV, Excel, clipboard | Control characters are stripped on import and copy | Keep codes in files exactly as received and do not pass them through Excel |
| Label template | The data gets the text “\x1d” or “<GS>” instead of the character | Use the FNC1 input method your label software provides |
| Decoder without GS1 support | The leading FNC1 is lost or arrives as a non-printable character | Check the ]d2 prefix or the GS1 flag in the decoder API |
| Web forms and JSON | The field is cleaned of “invisible” characters | Send it as \u001d in JSON and do not trim control characters |
Decoders make this mistake too. When we checked our own decoder against ISO/IEC 16022, we found exactly such a bug: an FNC1 beyond the second position was dropped instead of being transmitted as GS. On screen such a code looks fine; the loss shows up only in the bytes.
How to check that GS is there
The reliable way is to look at the bytes, not at the text on screen. In hexadecimal, GS is 1d:
code = scanner.read() # string from the scanner or decoder
print(code.encode("latin-1").hex(" "))
# ... 33 1d 32 31 ... — 1d after the batch number, before "21"
assert "\x1d" in code, "separator lost"If there is no 1d anywhere, follow the chain from the scanner to the database and see at which step it disappears. If it is already missing at the scanner output, the problem is in the printed code or the scanner settings.
Try it
The libscanner generator produces GS1 DataMatrix with a leading FNC1 and separators in the right places; try it in the online generator, the sources are on GitHub.
The reader keeps the separator on every output: in Python Code.text is bytes, and GS stays the byte 0x1D; the dmr command-line tool prints it as <GS> so it is not lost when copied. The requireGs1 option rejects codes without GS1 structure. Install with pip install libscanner-dmr, or check a photo in the online scanner.
Chestny ZNAK is a trademark of CRPT LLC (ООО «Оператор-ЦРПТ»); GS1 is a trademark of GS1 AISBL. The product is not affiliated with or certified by their owners.