Why JSON Is a Good Event Format—and Where It Breaks
JSON makes event ingestion portable and flexible. Learn the design conventions, query tradeoffs, performance limits, and schema practices that keep it useful.
JSON is a remarkably effective boundary for event data. Every mainstream language can produce it, HTTP transports it naturally, and an event can gain a new property without coordinating a database migration across every producer.
That flexibility is useful at ingestion. It does not make schema design optional. A stream of inconsistent JSON objects becomes a collection of tiny mysteries, and parsing every field at query time eventually has a performance cost.
The best event systems use JSON as a flexible contract with conventions, then materialize structure where repeated queries justify it.
Why JSON works so well at the boundary#
An event is a fact plus context:
{
"event": "report_generated",
"event_id": "evt_01J2...",
"user_id": "usr_123",
"account_id": "acct_42",
"plan": "pro",
"format": "pdf",
"duration_ms": 1840
}
This shape has several practical advantages.
It is portable#
The producer does not need a vendor SDK or generated client. A function, worker, CLI, device gateway, or legacy service can send the same payload with an ordinary HTTP library.
It evolves additively#
Adding duration_ms does not invalidate older events that lack it. Consumers can adopt the field when ready. This is especially valuable for analytics, where the questions evolve faster than the application’s core relational model.
It preserves context#
A flat metrics system might record only report_generated = 1. JSON keeps the plan, format, account, and duration attached to the fact, enabling analysis nobody specified when the counter was created.
It is inspectable#
A developer can read a payload in a log, request trace, queue, or sample table without a decoding tool. Human readability shortens debugging loops.
“Schemaless” still has a schema#
The schema exists in the producer code and in every query that assumes a field.
These two events are both valid JSON and a poor shared contract:
{ "event": "report_generated", "duration_ms": 1840 }
{ "Event": "Report Generated", "duration": "1.84s" }
Differences in capitalization, naming, type, and unit force every analysis to clean the data again.
Use a few stable rules:
- one event-name field with a consistent naming convention
- ISO language about units in field names, such as
_msand_cents - strings for identifiers, even when a current id looks numeric
- explicit booleans rather than
"yes"and"no" - UTC timestamps at the event envelope
- stable
event_idvalues when retries can occur user_idandaccount_idwhere identity matters
Our event schema guide provides a complete checklist.
Prefer properties over event-name explosion#
These names encode a property in the taxonomy:
report_pdf_generated
report_csv_generated
report_xlsx_generated
This is easier to analyze:
{
"event": "report_generated",
"format": "pdf"
}
The second form lets one query compare formats, and a new format does not create another event definition. Create a new event when the business action changes, not whenever one dimension changes.
Flatten the fields you query often#
Deep nesting can mirror an application object but creates friction:
{
"event": "request_finished",
"request": {
"route": "/v1/reports",
"performance": {
"duration_ms": 1840
}
}
}
For analytics, a focused flat payload is often better:
{
"event": "request_finished",
"route": "/v1/reports",
"duration_ms": 1840
}
Do not serialize an entire ORM model “just in case.” It adds cost, leaks fields unintentionally, and makes the analytical grain unclear.
Query-time JSON has a performance budget#
ClickHouse can extract fields from a JSON string:
SELECT
JSONExtractString(json, 'plan') AS plan,
quantile(0.95)(JSONExtractFloat(json, 'duration_ms')) AS p95
FROM events
WHERE JSONExtractString(json, 'event') = 'report_generated'
GROUP BY plan
This is ideal for exploration and fields whose value is not yet proven. Repeatedly parsing the same hot fields across billions of rows wastes work.
When a query becomes important and frequent, consider:
- extracting the field into a typed column
- adding a materialized column or view
- choosing a low-cardinality type for repeated strings
- validating the type at ingestion
- pre-aggregating a narrowly defined dashboard metric
The right progression is schema-on-read for discovery, then schema-on-write for the stable hot path.
Arrays and objects need a reason#
Arrays are convenient to send and harder to aggregate correctly. If an order contains five items, ask whether the analytical grain is one order event with an item array, five item events, or both.
Use the form that matches the questions:
- order-level conversion and revenue → one order event
- product-level units and attach rate → one event per line item, or a carefully unnested array
Objects are useful for bounded contextual groups, but unbounded arbitrary keys create high cardinality and unpredictable queries.
JSON makes privacy mistakes easy#
Serializing an object can include far more than intended. Never log:
- passwords or reset tokens
- session cookies
- API keys and authorization headers
- payment card details
- private message bodies without a defined need
- full request or user objects by default
Create a safe event object explicitly. Treat the analytics payload as a separate interface with its own review and retention policy.
Where JSON event storage fits#
JSON works especially well for:
- product and business events
- structured application logs
- webhooks and integration payloads
- API request analytics
- job and workflow lifecycle events
- heterogeneous device or service telemetry
It works less well as the only representation of canonical transactional state or as an excuse to avoid ownership of the event vocabulary.
GraphJSON is built around this tradeoff. You can send the JSON you have, explore every field immediately, and use ClickHouse SQL directly. When a field becomes important, your event conventions and query patterns make that structure visible.
Continue with How to Query JSON in ClickHouse, or send a real event through the five-minute quickstart.

Written by JR
Founder and builder of GraphJSON.