# Schema 3.7.0

Payloads may nest — free-form JSON instead of a map of primitives

Version coverage: /docs/reference/versions/


{{< callout context="note" title="Wire format: unchanged" icon="info-circle" >}}
Both codecs' golden fixtures are **byte-identical to 3.6.0** apart from the recorded version string.
A 3.6.0 decoder reads a 3.7.0 stream and vice versa; only the TypeScript type moved.
{{< /callout >}}

## Changed — payloads may nest

`ClientEvent.payload`, `ClientIssue.payload`, `ClientMetaData.payload` and `ExtensionStat.payload`
changed from a map of primitives to a **free-form JSON object** — `Record<string, unknown>` in the
generated TypeScript, the same type `attachments` has had since it stopped being a string.

A payload value may now be an object, an array, or any nesting of the two:

```typescript
monitor.addEvent({
    type: 'DEVICE_SNAPSHOT',
    payload: {
        device: { os: { name: 'macOS', version: '15.2' }, cpu: { cores: 10 } },
        permissions: ['camera', 'microphone'],
    },
});
```

Before this, a caller with structured context had to flatten it into dotted keys
(`'device.os.name'`) or stringify it into one field and parse it back on the far side.

### Why the wire did not have to move

- **On the protobuf wire a payload was already a JSON `string`** — proto3 can express neither the
  old union-valued map nor the new one — so nesting costs it nothing.
- **In the JSON codec a payload is an opaque object**, written whole whenever it changes and never
  carried forward. That is what made nesting free there too: diffing *into* a payload would need a
  way to say "this key was removed", which the format deliberately does not have.

### The Avro description

In Avro the map's values became `AnyValue`, a record that refers to itself through an `objectValue`
map and an `arrayValue` array — the shape protobuf's `Struct` / `Value` uses, and the only way Avro
can describe unbounded nesting, since a union cannot contain another union and only a *named* type
can recurse.

**Treat it as documentation of what may appear rather than as an encoding.** Avro tags its union
branches in JSON, so the `.avsc` has not described the codecs' actual bytes since payloads stopped
being strings in 3.5.0.

## Upgrading

**Reading a payload value now needs narrowing.** `payload.role` was `boolean | string | number` and
is now `unknown`:

```typescript
// before
const role = payload.role;

// after
const role = typeof payload.role === 'string' ? payload.role : String(payload.role);
```

**Writers need no change** — everything that was valid before still is.

## Fixed — a non-finite number no longer becomes null in silence

Both codecs copied opaque values with `JSON.parse(JSON.stringify(v))`, which turns `NaN` and
`Infinity` into `null` without complaint — exactly the corruption the top-level finite check has
always rejected, but it only ever saw the top level of a record.

With payloads able to nest, a `NaN` several levels down was newly easy to reach, so the encode path
now **walks an opaque value before copying it** and reports the path that was wrong
(`INVALID_VALUE`). A `bigint`, which previously escaped as a bare `TypeError` from inside the codec,
is rejected the same way, and a cycle is reported as `MALFORMED_INPUT` rather than by exhausting the
stack.

Both codecs use the same codes for the same inputs. Values `JSON.stringify` drops harmlessly —
functions, symbols, `undefined` — are still dropped, and a value with its own `toJSON`, such as a
`Date`, still serialises itself.

## Internal — scoped field-type overrides

TypeScript field-type overrides in the generator may now be scoped to one record, as `Record.field`,
and a scoped key beats a bare one.

The bare `payload` entry that gives the four client payloads their opaque type would otherwise also
have caught `SfuExtensionStats.payload`, which is a genuine JSON string on the SFU wire; it keeps
its `string` type through `['SfuExtensionStats.payload', 'string']`. Field names are not unique
across the schema set, so the mechanism now says which one it means.

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

