JSON
JSON has six types: object, array, string, number, boolean and null. That smallness is why it is everywhere and where its two production traps come from. There is one number type and it is not an integer, so identifiers belong in strings; and re-serialising a document does not reproduce its bytes, so anything you verify must be verified against what actually arrived.
Trap one: there is only one number#
JSON does not distinguish an integer from a decimal. Most parsers read every number into a double-precision float, which is exact for integers up to about 2^53 and silently approximate beyond it. A 19-digit identifier survives the network perfectly, is parsed, and comes out ending in a different digit — with no error anywhere in the chain.
{
"id": 9007199254740993, // parsed, this may become ...992
"id_safe": "9007199254740993", // a string is exact
"amount": 0.1 // 0.1 + 0.2 is not 0.3
}- Identifiers are strings. Always, even when every character in them is a digit. An identifier is a label, and no arithmetic is ever done on it.
- Money is not a float. Use minor units as an integer, or a decimal string with the currency beside it. A rounding error in a total is a support case, not a rounding error.
- Timestamps are strings too. JSON has no date type. Use RFC 3339 with an explicit offset, and never a bare local time — a follow-up dated without a zone is a promise nobody knows the hour of.
Trap two: parse, re-emit, and the bytes have changed#
JSON is a text format, but nothing preserves its text. Parsing to a structure and serialising back changes key order, whitespace, number formatting and string escaping. The document means the same thing; it is not the same bytes. This bites hardest in a system where the same payload passes through more than one store.
| Stage | What changes | What it breaks |
|---|---|---|
| A framework parsing a request body | Order, spacing, escaping | Any signature computed over the re-serialised body |
| A binary JSON column in the database | Keys reordered, duplicates collapsed, whitespace dropped | Byte-comparison of 'the same' record between two systems |
| A client library re-emitting for logging | Number formatting | A large identifier, quietly |
Null, absent, and the third state#
{"note": null} and {} are different documents and, in an update, usually mean different things: one says *clear this*, the other says *leave it alone*. A partial update that treats them alike will wipe fields nobody asked it to touch. Decide which convention an endpoint uses and say so in its description rather than leaving a client to discover it.
The related mistake is treating an empty collection as missing data. Connect's memory tiers return an empty tier rather than dropping it, precisely so that "nothing is set at this level" is a visible answer instead of an absence a reader has to interpret.
Does Connect use JSON?#
Used, as the default representation everywhere. API request and response bodies, MCP messages (JSON-RPC 2.0), the A2A agent card at /.well-known/agent-card.json, the documentation manifests, and the arguments the Assistant's 66 tools are called with are all JSON. It is also one of the file formats a workspace can upload and have read, alongside PDF, DOCX, XLSX, CSV, PPTX, images, TXT and Markdown.
The machine descriptions themselves are YAML documents describing JSON payloads — OpenAPI 3.1.0, Arazzo 1.1.0 and AsyncAPI 3.1.0 — which is a presentation choice for editability, not a different data model.
Questions#
Is JSON safe to accept from an untrusted source?
The format has no macros, includes or entity expansion, so it avoids a whole class of XML problems — Connect's file intake refuses DOCTYPE and ENTITY declarations and zip bombs for that reason. What JSON does not protect you from is depth and size, or from the text inside it being an instruction aimed at a model. Bound both; treat content as data.
Can I rely on the order of keys in a response?
No. Objects are unordered by definition and any layer may reorder them. If order carries meaning, use an array, where it is guaranteed.
Why do some numeric-looking fields come back as strings?
Because they are identifiers or amounts. A string is the only JSON type that survives every parser unchanged, which matters most for exactly the fields a mistake in would be hardest to notice.