Validating GTIN-14 and SSCC Identifiers in Python
The GTIN-14 and the SSCC look deceptively similar on a supplier feed: both are long strings of digits with a trailing modulo-10 check digit, and it is tempting to hand them to one loose validator that shrugs at “some numeric identifier of roughly the right size.” That shortcut is where the bug lives. A GTIN-14 that identifies a case of product and an SSCC that identifies a specific pallet are not interchangeable, they are not the same length, and a validator that accepts either whenever the string falls in a 14-to-18-digit range will happily wave through a truncated SSCC or a GTIN whose check digit never closed. This guide validates each key on its own terms — exact length, numeric composition, the shared modulo-10 check digit, and the leading indicator or extension digit — as part of the GS1 Identifier Validation gate.
The two keys populate different Key Data Elements. The GTIN-14 anchors product identity in the crosswalk that resolves a KDE Field Mapping Guide product description, while the SSCC ties a physical logistics unit to a shipping event. Conflating them, or accepting a malformed one, corrupts a Critical Tracking Event exactly as silently as a bad GLN does — the difference is only which KDE breaks.
Root Cause Analysis
The failure begins with treating length as a range rather than an exact constraint. A GTIN-14 is fourteen digits, no more and no fewer; an SSCC is eighteen. When a validator checks 14 <= len(value) <= 18, it collapses two distinct keys into one fuzzy category and loses the ability to catch a truncation. An SSCC that lost a digit in transit — seventeen characters instead of eighteen — still falls inside the permissive range, so the loose check passes it, and downstream code that assumes eighteen digits either misreads the extension digit or slices the serial reference incorrectly.
The second half of the failure is skipping the check digit entirely. Length and numeric-composition checks are blind to a transposed or mistyped digit, which is precisely the class of error the modulo-10 check digit exists to catch. Both keys share that algorithm: reading the payload from the right, digits are weighted alternately by 3 and 1, summed, and the check digit closes the sum to the next multiple of ten. Because the arithmetic is identical to the GLN case detailed in the Validate GLN Check Digits guide, the only per-key differences are the exact length and the meaning of the leading digit. Skipping the check digit means a corrupted GTIN or SSCC is trusted into a KDE the same way a naive length check trusts a transposed GLN, and the parent Schema Validation Rules gate never sees the defect because it inspects the field’s shape, not its arithmetic.
Figure — GTIN-14 versus SSCC-18 structure:
Minimal Reproducible Example
The snippet reproduces the loose validator and the two keys it mishandles: a truncated SSCC that lost one digit, and a GTIN-14 with a mistyped check digit. Both slip through because the check inspects only a length range and numeric composition.
# Valid GTIN-14 (indicator 1): 10012345678902
# Valid SSCC (extension 1): 106141411234567897
good_gtin = "10012345678902"
truncated_sscc = "10614141123456789" # 17 digits: one digit dropped in transit
bad_gtin = "10012345678903" # check digit mistyped 2 -> 3
def loose_is_gs1(value: str) -> bool:
# The bug: a range instead of an exact length, and no check-digit test.
return 14 <= len(value) <= 18 and value.isdigit()
print(loose_is_gs1(good_gtin)) # True (happens to be correct)
print(loose_is_gs1(truncated_sscc)) # True (WRONG - SSCC is one digit short)
print(loose_is_gs1(bad_gtin)) # True (WRONG - check digit does not close)
The truncated SSCC is seventeen digits, still inside the 14..18 window, so it passes; downstream, code that reads value[0] as the extension digit and slices a sixteen-digit serial reference now operates on a misaligned string. The GTIN with the flipped check digit passes for the same reason the naive GLN check failed — the range check never recomputes the digit.
Fix Implementation
The fix dispatches on the exact length, recomputes the check digit through one shared modulo-10 helper, and validates the leading digit’s range. Framing the helper to take the full value and its expected length keeps the two key types reading from a single source of arithmetic truth, while each key enforces its own structural rules.
from __future__ import annotations
import logging
gs1_logger = logging.getLogger("fsma204.gs1.gtin_sscc")
def _mod10_ok(value: str) -> bool:
"""True if the trailing digit closes the mod-10 weighted sum of the rest."""
body = value[:-1]
total = 0
for position, char in enumerate(reversed(body)):
weight = 3 if position % 2 == 0 else 1
total += int(char) * weight
return (10 - (total % 10)) % 10 == int(value[-1])
def is_valid_gtin14(value: str) -> bool:
"""GTIN-14: exactly 14 numeric digits, valid check digit, indicator 0-9."""
if len(value) != 14 or not value.isdigit():
return False
if value[0] not in "0123456789": # indicator digit
return False
return _mod10_ok(value)
def is_valid_sscc(value: str) -> bool:
"""SSCC: exactly 18 numeric digits, valid check digit, extension 0-9."""
if len(value) != 18 or not value.isdigit():
return False
if value[0] not in "0123456789": # extension digit
return False
return _mod10_ok(value)
def classify_and_validate(value: str) -> str:
"""Route a numeric identifier to the validator its length implies.
Raises ValueError for quarantine when the key is malformed or of no
recognised GS1 length, so a truncated SSCC never masquerades as valid.
"""
if len(value) == 14 and is_valid_gtin14(value):
gs1_logger.info("GTIN14_ACCEPTED | value=%s", value)
return "GTIN-14"
if len(value) == 18 and is_valid_sscc(value):
gs1_logger.info("SSCC_ACCEPTED | value=%s", value)
return "SSCC"
gs1_logger.warning("GS1_REJECTED | value=%s | len=%d", value, len(value))
raise ValueError(f"Not a valid GTIN-14 or SSCC: {value}")
Where the Validate GLN Check Digits guide isolated the check-digit function to return the digit for unit-testing, here _mod10_ok returns a boolean and folds the comparison inline, because the calling code only ever asks “does this close?” The classify_and_validate dispatcher is the ingestion entry point: it uses exact length to pick the validator, so a seventeen-digit SSCC matches neither branch and is quarantined rather than silently reinterpreted.
Verification Steps
The tests assert each key against a known-good value and against the two defects from the reproduction — a truncated SSCC and a check-digit typo — plus the leading-digit and cross-length cases.
import pytest
def test_valid_gtin14_passes():
assert is_valid_gtin14("10012345678902") is True
def test_valid_sscc_passes():
assert is_valid_sscc("106141411234567897") is True
def test_truncated_sscc_fails():
# 17 digits: the MRE truncation must not pass as an SSCC.
assert is_valid_sscc("10614141123456789") is False
def test_gtin_bad_check_digit_fails():
assert is_valid_gtin14("10012345678903") is False
def test_transposed_sscc_fails():
# Swap two adjacent payload digits; length stays 18, check digit breaks.
assert is_valid_sscc("106141411234567987") is False
def test_dispatch_rejects_unknown_length():
with pytest.raises(ValueError):
classify_and_validate("10614141123456789") # 17 digits, no branch
def test_dispatch_classifies_each_key():
assert classify_and_validate("10012345678902") == "GTIN-14"
assert classify_and_validate("106141411234567897") == "SSCC"
Run pytest and all seven pass. In production the structured log distinguishes the outcomes: GTIN14_ACCEPTED and SSCC_ACCEPTED at INFO, and GS1_REJECTED | value=... | len=... at WARNING with the observed length, so a spike of length-17 rejections immediately points at an upstream truncation. Feeding those counts to the Data Quality Monitoring layer turns the raw rejections into a per-supplier signal.
Related Edge Cases
Three variants deserve their own checks once GTIN-14 and SSCC validation is in place. First, a GTIN-12 or GTIN-13 arriving where a GTIN-14 is expected must be right-justified and zero-padded to fourteen digits before validation, not truncated or left-aligned; the indicator position is only meaningful once the value is a true fourteen-digit GTIN-14. Second, a GTIN-14 carrying a variable-measure indicator of 9 denotes a variable-weight item whose embedded measure must be handled separately from the check-digit test, so pipelines that price by weight should branch on that indicator rather than treat it as a fixed tier. Third, an SSCC embedded in a scanned barcode element string often arrives prefixed with the application identifier 00, which must be stripped to the bare eighteen digits before validation or the length check fails on a structurally sound key. Suppliers whose feeds surface these variants are registered through Supplier Onboarding Automation, and the resulting KDEs flow into the Schema Validation Rules gate.
Frequently Asked Questions
Why not validate GTIN-14 and SSCC with one length-range check?
Because a range check collapses two distinct keys into one fuzzy category and loses the ability to detect a truncation. An SSCC that dropped a digit is seventeen characters, still inside a 14-to-18 window, so a range check passes it and downstream code misaligns the extension digit and serial reference. Dispatching on the exact length — fourteen for a GTIN-14, eighteen for an SSCC — rejects the truncated value instead of reinterpreting it.
Do GTIN-14 and SSCC use the same check-digit algorithm?
Yes. Both use the GS1 modulo-10 algorithm that weights payload digits alternately by 3 and 1 from the right and closes the sum to the next multiple of ten, identical to the GLN. Only the length differs and the meaning of the leading digit differs, which is why a single shared mod-10 helper serves both while each key enforces its own length and leading-digit rules.
What is the leading digit of each key for?
The GTIN-14 leads with an indicator digit that distinguishes packaging tiers of the same base product, with 0 for the base item, 1 through 8 for fixed tiers, and 9 for a variable-measure item. The SSCC leads with an extension digit assigned by the brand owner to increase serial capacity. Neither leading digit changes the check-digit computation, which spans every digit except the last, but both should be present and numeric.
How should a shorter GTIN-12 or GTIN-13 be handled?
Right-justify it and zero-pad it to fourteen digits before validation, since the indicator position is only meaningful once the value is a true fourteen-digit GTIN-14. Never truncate or left-align it, as that would misplace the check digit and corrupt the identity. Once padded, the same GTIN-14 validator applies unchanged.
Where does a rejected GTIN or SSCC go?
The dispatcher raises on any value that matches neither a valid GTIN-14 nor a valid SSCC, so the ingestion caller routes it to the same durable quarantine store the schema validation gate uses, preserving the original value and its observed length. It is never silently dropped or reinterpreted. The observed length in the log makes an upstream truncation immediately diagnosable.
Related
- GS1 Identifier Validation — the parent guide covering all three GS1 keys and the shared mod-10 function.
- Validate GLN Check Digits — the same check-digit arithmetic applied to a 13-digit GLN.
- Schema Validation Rules — the KDE contract that consumes validated GTIN and SSCC values.
- KDE Field Mapping Guide — maps the GTIN to product identity and the SSCC to a logistics unit.
- Supplier Onboarding Automation — registers supplier GTIN and SSCC conventions before the first live batch.
Up: GS1 Identifier Validation — this guide is the GTIN-14 and SSCC implementation of the parent validation gate.