diff --git a/lib/ash_json_api/serializer.ex b/lib/ash_json_api/serializer.ex index 69fffb3..dd325e5 100644 --- a/lib/ash_json_api/serializer.ex +++ b/lib/ash_json_api/serializer.ex @@ -1258,11 +1258,23 @@ defmodule AshJsonApi.Serializer do req = %{fields: %{}, route: %{}, domain: domain} serialize_attributes(req, value, opts) else - value + dump_struct_value(value, type, constraints) end end end + # A struct whose type is a non-resource Ash type (e.g. a TypedStruct or an + # embedded type) isn't JSON-encodable as-is. Dump it through the type into a + # native map; non-struct values pass through unchanged. + defp dump_struct_value(value, type, constraints) when is_struct(value) do + case Ash.Type.dump_to_embedded(type, value, constraints || []) do + {:ok, dumped} -> dumped + _ -> value + end + end + + defp dump_struct_value(value, _type, _constraints), do: value + defp flatten_new_type(type, constraints) do if Ash.Type.NewType.new_type?(type) do new_constraints = Ash.Type.NewType.constraints(type, constraints) diff --git a/test/acceptance/route_test.exs b/test/acceptance/route_test.exs index 8e035bc..8f74428 100644 --- a/test/acceptance/route_test.exs +++ b/test/acceptance/route_test.exs @@ -5,6 +5,15 @@ defmodule Test.Acceptance.RouteTest do use ExUnit.Case, async: true + defmodule Greeting do + use Ash.TypedStruct + + typed_struct do + field(:message, :string, allow_nil?: false) + field(:count, :integer) + end + end + defmodule Actions do use Ash.Resource, domain: Test.Acceptance.RouteTest.Domain, @@ -16,6 +25,7 @@ defmodule Test.Acceptance.RouteTest do route(:get, "/say_hello", :say_hello, query_params: [:to]) route(:post, "/required_say_hello/:to", :with_required) route(:post, "/trigger_job", :trigger_job) + route(:get, "/say_hello_struct/:to", :say_hello_struct) end end @@ -42,6 +52,15 @@ defmodule Test.Acceptance.RouteTest do :ok end) end + + action :say_hello_struct, Test.Acceptance.RouteTest.Greeting do + argument(:to, :string, allow_nil?: false) + + run(fn input, _ -> + {:ok, + %Test.Acceptance.RouteTest.Greeting{message: "Hello, #{input.arguments.to}!", count: 1}} + end) + end end end @@ -94,6 +113,13 @@ defmodule Test.Acceptance.RouteTest do |> Kernel.==(%{"success" => true}) end + test "generic actions returning a TypedStruct are serialized without a Jason encoder" do + assert Domain + |> get("/say_hello_struct/fred", status: 200) + |> Map.get(:resp_body) + |> Kernel.==(%{"message" => "Hello, fred!", "count" => 1}) + end + test "generic actions with required inputs" do assert Domain |> post(