Wire formats
Copy MarkdownChannelClient ships a pluggable format layer: a single ChannelClient.Format
behaviour owns both the structure of frames and their encoding. The
default is Phoenix Channels JSON, but ETF (Erlang terms) works out of the
box, and MessagePack or anything else is a few lines away.
Built-in formats
| Format | Option | Notes |
|---|---|---|
| JSON v2 | format: :json (default) | JSON arrays; protocol "2.0.0" |
| JSON v1 | vsn: "1.0.0" or format opts | JSON objects; legacy servers |
| ETF | format: :etf | Erlang External Term Format, v2 array shape |
| TOON | format: :toon | Token-Oriented Object Notation (text) |
| BTOON | format: :btoon | Binary variant of TOON |
ETF is a zero-dependency binary format — ideal for Elixir/Erlang service
meshes. Decoding runs in binary_to_term/2 safe mode, so hostile peers
cannot create atoms on your client.
# Default JSON
{:ok, socket} = ChannelClient.Socket.start_link(url: "ws://...")
# ETF
{:ok, socket} =
ChannelClient.Socket.start_link(url: "ws://...", format: :etf)
# JSON v1 with an alternate library
{:ok, socket} =
ChannelClient.Socket.start_link(
url: "ws://...",
format: {ChannelClient.Formats.JSON,
version: "1.0.0", json_library: MyApp.JsonLib}
)TOON & BTOON
TOON (Token-Oriented Object Notation) is a compact text encoding of
JSON-shaped data; BTOON is its binary counterpart. Both are built in, and
they delegate the actual codec to the toon_ex
package — an optional dependency of channel_client:
{:toon_ex, "~> 1.5"}# Uses ToonEx / ToonEx.Btoon from toon_ex by default:
ChannelClient.Socket.start_link(url: "ws://...", format: :toon)
ChannelClient.Socket.start_link(url: "ws://...", format: :btoon)
# Or point at any equivalent codec explicitly:
ChannelClient.Socket.start_link(
url: "ws://...",
format: {ChannelClient.Formats.TOON, library: MyApp.MyToonCodec}
)The codec contract is minimal — encode!/1 and decode!/1. Frames are
maps with :join_ref, :ref, :topic, :event and :payload keys;
codecs may return atom- or string-keyed maps on decode. Payloads must be
JSON-compatible data (that is what TOON represents). If no usable codec is
loaded at startup you get an ArgumentError naming the missing library —
fail fast instead of failing mid-connection.
The legacy options keep working exactly as before: :vsn selects the JSON
protocol version and :json_library (alias :serializer) swaps the JSON
codec. An explicit :format takes precedence.
Server-side note
Your Phoenix endpoint decides which formats it accepts. JSON v2 is the
default on both sides. For ETF you must register an ETF serializer on the
server socket before clients can connect with format: :etf.
Writing a custom format
Implement ChannelClient.Format:
defmodule MyApp.MsgPack do
@behaviour ChannelClient.Format
@impl true
def init(opts), do: opts
@impl true
def encode!(%ChannelClient.Message{} = msg, _opts) do
Msgpax.pack!([msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload])
end
@impl true
def decode!(payload, _opts) when is_binary(payload) do
[join_ref, ref, topic, event, payload | _] = Msgpax.unpack!(payload)
%ChannelClient.Message{
join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload
}
end
endChannelClient.Socket.start_link(
url: "ws://localhost:4000/socket/websocket",
format: {MyApp.MsgPack, compress: true}
)Contract details:
- Return the encoded frame as iodata/binary from
encode!/2. - Raise in
encode!/2for payloads you cannot serialize — synchronous pushes surface this as{:error, reason}, async pushes log and drop. - Raise in
decode!/2for malformed input — those frames are logged and dropped instead of crashing the socket. init/1validates options once at socket startup; prefer failing there.- The socket stamps
refandjoin_refafter outbound plugs run and before encoding, so protocol framing stays correct regardless of format.
Format × plugs × telemetry
Formats compose with everything else in the pipeline: plugs operate on
decoded %ChannelClient.Message{} structs (before encoding / after
decoding), and telemetry measurements like payload_bytes reflect whatever
bytes your format produces.