diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f92b7..5d30926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Generated `State` structs now derive `Jason.Encoder`, fixing `Protocol.UndefinedError` when encoding state over Phoenix channels or other JSON serialization paths + ## [0.3.0] - 2026-02-26 ### Fixed diff --git a/lib/durable_object/dsl/transformers/build_introspection.ex b/lib/durable_object/dsl/transformers/build_introspection.ex index de46e1f..76316eb 100644 --- a/lib/durable_object/dsl/transformers/build_introspection.ex +++ b/lib/durable_object/dsl/transformers/build_introspection.ex @@ -59,6 +59,7 @@ defmodule DurableObject.Dsl.Transformers.BuildIntrospection do quote do defmodule State do @moduledoc false + @derive Jason.Encoder defstruct unquote(Macro.escape(struct_fields)) end end diff --git a/test/durable_object/dsl/transformers_test.exs b/test/durable_object/dsl/transformers_test.exs index fd494ff..b03c5ae 100644 --- a/test/durable_object/dsl/transformers_test.exs +++ b/test/durable_object/dsl/transformers_test.exs @@ -78,5 +78,25 @@ defmodule DurableObject.Dsl.TransformersTest do get_participants = Enum.find(handlers, &(&1.name == :get_participants)) assert get_participants.args == [] end + + test "State struct is JSON-encodable via Jason" do + state = %BasicCounter.State{id: "test-123", count: 42} + assert {:ok, json} = Jason.encode(state) + assert %{"id" => "test-123", "count" => 42} = Jason.decode!(json) + end + + test "State struct with complex fields is JSON-encodable" do + state = %ChatRoom.State{ + id: "room-1", + messages: [%{user: "alice", text: "hello"}], + participants: ["alice", "bob"], + created_at: nil + } + + assert {:ok, json} = Jason.encode(state) + decoded = Jason.decode!(json) + assert decoded["id"] == "room-1" + assert length(decoded["participants"]) == 2 + end end end diff --git a/test/durable_object/server_persistence_test.exs b/test/durable_object/server_persistence_test.exs index 51d5ba6..dec844b 100644 --- a/test/durable_object/server_persistence_test.exs +++ b/test/durable_object/server_persistence_test.exs @@ -100,6 +100,42 @@ defmodule DurableObject.ServerPersistenceTest do end end + defmodule SerdeCounter do + use DurableObject + + state do + field(:count, :integer, default: 0) + end + + handlers do + handler(:increment) + handler(:get_state) + end + + def handle_increment(state) do + {:reply, :ok, %{state | count: state.count + 1}} + end + + def handle_get_state(state) do + {:reply, state, state} + end + end + + describe "state serde through sqlite" do + test "state persists and rehydrates correctly" do + id = unique_id("serde") + + {:ok, :ok} = SerdeCounter.increment(id, repo: TestRepo) + {:ok, :ok} = SerdeCounter.increment(id, repo: TestRepo) + {:ok, :ok} = SerdeCounter.increment(id, repo: TestRepo) + DurableObject.stop(SerdeCounter, id) + + {:ok, state} = SerdeCounter.get_state(id, repo: TestRepo) + assert state.id == id + assert state.count == 3 + end + end + describe "without :repo option" do test "works without persistence" do id = unique_id("no-repo")