Erlang External Term Format encoding for Discord Gateway payloads.
Binary format decoded natively by the BEAM via :erlang.binary_to_term/1,
significantly faster than JSON parsing. Payloads are ~15-30% smaller.
Normalization
Discord's ETF payloads differ from JSON in two ways:
- Atom keys — Map keys arrive as atoms (e.g.
:op) instead of strings. - Integer snowflakes — Large IDs arrive as integers instead of strings.
normalize/1 deep-converts the decoded term so the result is identical to
what Jason.decode!/1 would produce. This means the rest of EDA sees no
difference between ETF and JSON payloads.
Security
We use :erlang.binary_to_term/1 without the :safe option because Discord
may introduce new fields (= new atoms) at any time. With :safe, unknown atoms
would crash the decoder. Since normalize/1 immediately converts all atoms to
strings, there is no long-term pollution of the atom table beyond the decode call.
Summary
Functions
Decodes an ETF binary and normalizes atom keys and snowflake integers to strings.
Encodes a map as an ETF binary frame.
Deep-converts an ETF-decoded term to match Jason.decode!/1 output.
Returns "etf" for the gateway URL query parameter.
Functions
Decodes an ETF binary and normalizes atom keys and snowflake integers to strings.
Encodes a map as an ETF binary frame.
Atom keys are converted to strings before encoding because Discord's ETF
parser requires string (binary) keys — atom keys cause a 4002 close code.
This mirrors what Jason.encode!/1 does implicitly for JSON.
Deep-converts an ETF-decoded term to match Jason.decode!/1 output.
- Atom map keys become strings
- Atom values become strings (e.g. event type atoms)
- Integers above
4194304(2^22) become strings (snowflakes, permissions) - Booleans and nil are preserved
- Lists and nested maps are recursively normalized
@spec url_encoding() :: String.t()
Returns "etf" for the gateway URL query parameter.