Skip to content

feat: encode structs via optional Torque.Encoder protocol - #51

Open
gilbertwong96 wants to merge 1 commit into
lpgauth:mainfrom
gilbertwong96:feat/encoder-protocol
Open

feat: encode structs via optional Torque.Encoder protocol#51
gilbertwong96 wants to merge 1 commit into
lpgauth:mainfrom
gilbertwong96:feat/encoder-protocol

Conversation

@gilbertwong96

@gilbertwong96 gilbertwong96 commented Aug 12, 2026

Copy link
Copy Markdown

Structs currently serialize as raw maps ({"__struct__":"Elixir.Decimal",...}), so there's no way to customize how a struct encodes — e.g. Decimal values lose precision as JSON floats past 2^53.

This adds an optional Torque.Encoder protocol:

  • The NIF rejects structs (maps with an atom __struct__ key) with {:error, :unhandled_struct} — a single map lookup, so the no-struct fast path stays at zero cost
  • The Elixir layer normalizes the term through the protocol and retries once
  • A struct without an implementation still fails with :unhandled_struct — never silently dropped
defimpl Torque.Encoder, for: Decimal do
  def encode(decimal), do: Decimal.to_string(decimal)
end

Torque.encode!(%{price: Decimal.new("37.50")})
#=> ~s({"price":"37.50"})

Structs can also derive the implementation with :only / :except, matching Jason and Elixir's built-in JSON.Encoder:

@derive {Torque.Encoder, only: [:id, :name]}
defstruct [:id, :name, :secret]

@derive {Torque.Encoder, except: [:secret]}
defstruct [:id, :name, :secret]

Consolidation is deferred to the host project (consolidate_protocols: Mix.env() != :test), so apps implement the protocol for their own structs at build time.

Breaking change: structs previously encoded as raw maps now error. Happy to add a structs: :raw opt-out if that's a concern.

Nested structs are handled recursively: a protocol implementation may return a term containing other structs, which are normalized in turn. A struct without an implementation fails the whole encode — implement the protocol for each struct type you want to serialize. Structs nested inside jiffy-style {proplist} tuples are normalized too.

@gilbertwong96
gilbertwong96 force-pushed the feat/encoder-protocol branch 2 times, most recently from 752b4f3 to 297cb84 Compare August 12, 2026 10:19
Structs (maps with an atom __struct__ key) are rejected by the NIF
encoder with {:error, :unhandled_struct}; the Elixir layer then runs
the term through the Torque.Encoder protocol and retries once.

- NIF detects structs while encoding maps (zero cost when absent)
- Torque.Encoder protocol with Any fallback (opt-in per struct type)
- normalize/1 recursively encodes protocol output
- consolidation deferred to the host project so it can implement
  the protocol for its own structs (e.g. Decimal)

A struct without an implementation still fails with
:unhandled_struct — never silently dropped.
@gilbertwong96
gilbertwong96 force-pushed the feat/encoder-protocol branch from 297cb84 to 0b9a19f Compare August 12, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant