# Schema 3.5.0

All payloads become records, not pre-serialised strings

Version coverage: /docs/reference/versions/


{{< callout context="caution" title="Payload representation changes" icon="alert-triangle" >}}
Producers and consumers should move to the 3.5.0 generated schemas together, or use a reader that
accepts both — [`observer-js`](/docs/observer-js/) does.
{{< /callout >}}

## Changed — payloads are records

`ClientEvent.payload`, `ClientIssue.payload`, `ClientMetaData.payload` and `ExtensionStat.payload`
changed from an optional **string** to an optional **map of primitives** —
`Record<string, boolean | string | number>` in the generated TypeScript (Avro:
`["null", {"type": "map", "values": ["boolean", "string", "double"]}]`, keeping the null-first
optional convention).

Producers no longer `JSON.stringify` every payload before buffering it, and consumers read payload
fields directly instead of parsing a nested JSON document.

```jsonc
// before
{ "type": "MEDIA_TRACK_MUTED", "payload": "{\"trackId\":\"t-1\",\"kind\":\"audio\"}" }

// 3.5.0
{ "type": "MEDIA_TRACK_MUTED", "payload": { "trackId": "t-1", "kind": "audio" } }
```

## Why — measured, not argued

Measured on Node 22 with realistic samples carrying 35–40 payloads:

| | Effect |
|---|---|
| Payload bytes on the wire | **~12% smaller** — a pre-stringified payload gets every quote escaped when the whole sample serialises |
| Serialising the sample | **~18% faster** end to end (0.37 ms → 0.30 ms per sample) |
| The events portion alone | **~54% faster** (0.042 ms → 0.020 ms) |
| Server-side decode | ~3% saved — one parse instead of 1 + N |

Each payload was previously walked twice: once by its own `JSON.stringify` at buffering time (30–40
calls per sample, now zero, along with their intermediate-string garbage) and once more
character-by-character for escaping. Payload fields also become **directly queryable without a
second parse**.

## Codec impact

- **Protobuf**: the payload still travels as a JSON `string`, because proto3 cannot express a
  union-valued map — the same convention `attachments` uses. The protobuf codec converts it through
  its JSON-fields mechanism, keyed on the field name, so all four payload fields are covered and
  **protobuf field numbering is unchanged**.
- **JSON**: no change needed. Events, issues, meta items and extension stats are value lists written
  whole, and object values were already handled generically.

The TypeScript generator learned to render union-valued Avro maps for this.

## Upgrading

```typescript
// before
const payload = JSON.parse(event.payload ?? '{}');

// 3.5.0
const payload = event.payload ?? {};
```

A reader that must accept both generations does what the observer does: pass an object through
untouched, parse a string.

{{< callout context="note" title="3.7.0 widened this again" icon="info-circle" >}}
A payload value stayed a *primitive* in 3.5.0 and 3.6.0. [3.7.0](../v3-7-0/) widened it to free-form
JSON, so a payload may nest objects and arrays — again with no change to either wire format.
{{< /callout >}}

[← Back to version history](/docs/schema/versions/) · [3.4.0 →](../v3-4-0/)

