Tutorial: Checking a JSON Payload Before It Goes Into a Tag

Management Summary
JSON is a common way for programs to exchange labelled information, such as purchase amounts and product details sent to analytics. A neatly formatted message may still contain wrong values or lose information during processing. The article explains the difference between readable data and data that matches the recipient’s expectations. Repeated field names or very long numeric identifiers illustrate why checking content matters before a measurement tool turns the message into a business report.
Jump to the interactive example ↓
Contents
A JSON payload can be syntactically valid and still carry the wrong value. Three separate checks help: parsing the original text, checking interoperability risks, and validating the receiving application’s rules. Formatting alone covers none of these completely.
The examples below run locally. The checker accepts exactly one JSON value, rejects duplicate object members and keeps decimal values without an intermediate binary floating-point conversion. It does not contact a tag manager or analytics service.

The actual JSON text as the starting point
JSON is a data format, not executable JavaScript. Object names and strings need double quotes. Comments, trailing commas, undefined, NaN and Infinity are outside the JSON grammar. The value categories are object, array, string, number, boolean and null; a document can also consist only of a scalar such as false or 42. Date and identifier conventions belong to the application schema.
An apostrophe inside a double-quoted string is valid. An unescaped line break inside a string is not; the escaped sequence \n represents a newline. Different parsers issue different diagnostics, so a fixed count of “loud” and “silent” errors is not useful.
The original response or file should be retained before formatting. A formatter may refuse malformed JSON and can discard duplicate members or change numeric representation. An error at position zero can come from a byte-order mark, HTML instead of JSON, an empty input or another unexpected first character. It does not diagnose a BOM by itself.
Duplicate-name checks before information is lost
RFC 8259 recommends unique names within each object. Receivers handle duplicates differently: common parsers keep the last value, but rejection or preservation of all pairs is also possible. After conversion to an ordinary dictionary, the discarded value may already be unavailable.
For example, {"a":{"x":1},"a":{"y":2}} repeats the name a even though the paths to the final values, a.x and a.y, differ. A list of repeated streaming leaf paths cannot reliably detect this problem. Conversely, the same name in two separate objects is not a duplicate within one object. The checker uses Python’s object_pairs_hook before building each dictionary, including nested objects and escaped names.
Identifiers and the receiving schema
JavaScript Number uses binary floating point. Every integer in the inclusive interval from −(2^53−1) to 2^53−1 is safe; outside that interval some integers are representable, but not every neighboring integer is distinct. The following Node.js example demonstrates a changed final digit.
const parsed = JSON.parse('{"id":9007199254740993}');
console.log(parsed.id); // 9007199254740992
console.log(Number.isSafeInteger(parsed.id)); // false
const exact = JSON.parse('{"id":"9007199254740993"}');
console.log(exact.id); // "9007199254740993" as a string
A string preserves this identifier when the receiving API permits a string. An API that requires a number needs its documented representation and limits; changing every identifier to a string unconditionally can make a valid request invalid. A digit-counting regular expression is not a complete precision check: it misses negative or exponent forms and can confuse numeric strings with numbers.
A local checker with explicit extra rules
The file uses only Python’s standard library. Saving it as check_json.py allows python3 check_json.py payload.json. Exit code 0 means that the local checks passed; exit code 1 means rejection. The file contents are not printed.
import json
import sys
from decimal import Decimal, InvalidOperation
from pathlib import Path
MAX_BYTES = 1_048_576 # Local example policy, not a JSON limit.
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate object member")
result[key] = value
return result
def reject_constant(value):
raise ValueError("non-JSON numeric constant")
def check(raw):
if len(raw) > MAX_BYTES:
raise ValueError("example size limit exceeded")
# Strict UTF-8; this example also rejects a leading BOM.
text = raw.decode("utf-8")
if text.startswith("\ufeff"):
raise ValueError("leading BOM rejected by local policy")
value = json.loads(text, object_pairs_hook=unique_object,
parse_float=Decimal, parse_constant=reject_constant)
# Reject lone UTF-16 surrogates for interoperable Unicode text.
def visit(x):
if isinstance(x, str):
x.encode("utf-8")
elif isinstance(x, dict):
for key, item in x.items():
visit(key)
visit(item)
elif isinstance(x, list):
for item in x:
visit(item)
visit(value)
return value
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Usage: python3 check_json.py payload.json")
try:
with Path(sys.argv[1]).open("rb") as stream:
raw = stream.read(MAX_BYTES + 1)
check(raw)
except json.JSONDecodeError as exc:
print(f"Syntax error: line {exc.lineno}, column {exc.colno}",
file=sys.stderr)
raise SystemExit(1)
except (OSError, ValueError, UnicodeError, RecursionError,
InvalidOperation, OverflowError):
print("Rejected: encoding, duplicate, value or resource policy",
file=sys.stderr)
raise SystemExit(1)
print("OK: JSON plus the example's interoperability checks")
The one-mebibyte limit, rejection of a leading BOM and rejection of lone surrogate characters are explicit interoperability/resource policies. They are not a claim that every rejected input violates the JSON grammar. The parser also has implementation limits. This example is not a complete defense against hostile input.
Python’s default decoder accepts NaN and Infinity as extensions; parse_constant disables that behavior here. Decimal preserves decimal input for checking, while integer input remains a Python integer. Neither choice proves that a later JavaScript consumer or external API can represent the same value.
Diagnostics from the original evidence
Python exposes line and column for a syntax error. JavaScript error messages vary by engine and version; extracting an English “position” phrase is not a portable API. Character offsets, UTF-16 code units and byte offsets must not be mixed. Tabs and line breaks also make a hand-built caret unreliable.
Local inspection around the original location is useful. Production logs should avoid dumping full payloads containing personal data or credentials. A syntax diagnostic locates where parsing failed, which can be after the actual mistake.
What jq does and does not establish
jq -e . is not a pure validity test: valid false and null values produce a nonzero exit status. jq also normally reads a stream of JSON values, so acceptance does not by itself establish exactly one document. Empty input and extensions need explicit treatment. The Python checker above makes the intended policy clear.
Numeric behavior depends on jq version and build options. Literal preservation and arithmetic conversion are separate matters; a blanket guarantee for every build since version 1.7 is incorrect. A pretty-printed copy should not replace the original evidence for precision analysis.
The actual tag or API contract
A successful local check does not validate required fields, event names, currency, dates or business rules. The generated payload needs the receiving platform’s schema and documented diagnostic tools. Responses and processing results must also be observed; an HTTP status alone may not establish correct downstream data.
For JSON-LD, validation applies to the rendered page and the relevant structured-data type. One broken block does not prove that every other block is unreadable. Correct syntax also does not guarantee rich results. For tag templates, import validation and runtime behavior are separate checks; the failure point depends on the actual format and platform.
Questions and answers
How does JSON.parse in the browser handle duplicate names?
It reports no error and keeps the last value. {"a":1,"a":2} becomes an object with a = 2, and nothing in the result shows that there was a first value. A tag that reads the payload in the browser therefore never sees the duplicate, which is why checking the original text before parsing pays off.
Which values does JSON.stringify change silently when generating a payload?
Several, and only one case raises an error. Code that assembles a payload in JavaScript, for example in a custom variable, can run into these cases:
NaN,Infinityand-Infinitybecomenull. A failed conversion thus arrives as valid JSON.- Properties whose value is
undefined, a function or a symbol are dropped from objects; in arrays they becomenull. A required field can go missing without any message. - A
Dateobject becomes a string in ISO format in UTC. If the receiver expects a different date format or local time, the value does not fit. MapandSetbecome empty objects.- A
BigIntthrows a TypeError. That is the only loud case.
The checker from the article only catches what remains visible in the text. A missing field or a null in the wrong place only shows up against the receiver’s schema.
Why does the checker read decimal numbers as Decimal rather than float?
Because float converts every decimal number to a binary value, and many decimal fractions are only approximated in the process. As a float, the amount 19.99 becomes a value just below it, which usually prints as 19.99 again but differs in calculations; the well-known case is 0.1 + 0.2, which yields 0.30000000000000004. From 16 significant digits on, a binary value can no longer reliably reproduce a literal; the identifier 9007199254740993 in the article shows this in JavaScript.
Decimal keeps the literal as written. That allows rules to be checked against the written value, such as at most two decimal places for an amount; Decimal("19.990") has three, although it describes the same numeric value. Whether a later receiver has the same precision remains open, as the article points out.
Can an oversized identifier still be rescued after JSON.parse?
Not from the parsed object. The number in it has already been rounded to the nearest representable value, 9007199254740993 having become 9007199254740992, and no later conversion knows the original digit. The value only survives if it is read from the original text before parsing or if the source delivers it as a string from the start.
Sources
Learn by trying
One minute: can you spot the mistake?
Why is this JSON invalid?
{"event":"page_view","value":42,}JSON does not allow a trailing comma. Remove it. Valid syntax does not tell you whether the application expects these fields.
One problem, a clear way through
My JSON is causing trouble
- Understand: Check a JSON payload
- Try it: Format JSON
- Start with a small example and find syntax errors. Then check whether the values and types fit your application – valid syntax alone cannot tell you that.


