diff --git a/lib/open_api_spex/cast/utils.ex b/lib/open_api_spex/cast/utils.ex index 6c3e3923..084a9ca9 100644 --- a/lib/open_api_spex/cast/utils.ex +++ b/lib/open_api_spex/cast/utils.ex @@ -5,8 +5,11 @@ defmodule OpenApiSpex.Cast.Utils do # Merge 2 maps considering as equal keys that are atom or string representation # of that atom. Atom keys takes precedence over string ones. + # Struct arguments are demoted to plain maps first: composite casts + # (`allOf`/`anyOf`) can receive struct results from `oneOf`/discriminator + # members, and reducing over a struct raises `Protocol.UndefinedError`. def merge_maps(map1, map2) do - result = Map.merge(map1, map2) + result = Map.merge(plain_map(map1), plain_map(map2)) Enum.reduce(result, result, fn {k, _v}, result when is_atom(k) -> Map.delete(result, to_string(k)) @@ -14,6 +17,9 @@ defmodule OpenApiSpex.Cast.Utils do end) end + defp plain_map(%_{} = struct), do: Map.from_struct(struct) + defp plain_map(map), do: map + def check_required_fields(%{value: input_map} = ctx), do: check_required_fields(ctx, input_map) def check_required_fields(ctx, %{} = input_map) do diff --git a/test/cast/all_of_test.exs b/test/cast/all_of_test.exs index 9f2fbabe..ce6cb7aa 100644 --- a/test/cast/all_of_test.exs +++ b/test/cast/all_of_test.exs @@ -34,6 +34,14 @@ defmodule OpenApiSpex.CastAllOfTest do "Failed to cast value as Age. Value must be castable using `allOf` schemas listed." end + test "allOf wrapper around a oneOf member whose branch casts to a struct" do + union = %Schema{oneOf: [OpenApiSpexTest.Schemas.User.schema()]} + schema = %Schema{allOf: [union]} + value = %{"name" => "Joe", "email" => "joe@example.com", "password" => "12345678"} + + assert {:ok, %{name: "Joe"}} = cast(value: value, schema: schema) + end + test "a more sophisticated example" do dog = %{"bark" => "woof", "pet_type" => "Dog"} TestAssertions.assert_schema(dog, "Dog", OpenApiSpexTest.ApiSpec.spec())