Map with string keys to Absinthe object type

I have an API call returning something in the shape of:

%{"items" => %{ "some-key" => ... } }

I’m trying to build the payload in Absinthe to fit this shape:

  object :email_events_failed do
    field(:items, list_of(:email_event_failed))
  end

where I’m building nested objects as such:

  object :email_event_failed do
    field(:delivery_status, :delivery_status)
    field(:envelope, :envelope)
    field(:event, :string)
    field(:headers, :headers)
    field(:recipient, :string)
    field(:recipient_domain, :string)
    field(:timestamp, :integer)
  end

However I keep getting a response of

{"data":{"emailEvents":{"items":null}}}

So how to I properly create the payload?

1 Like

Hi @alxvallejo this is handled with Absinthe middleware. You can replace the default middleware, which expects atom keys, with middleware that looks for string keys. Here are the relevant docs: https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-default-middleware

4 Likes

Thanks. I encountered the same problem and solved like this;

# schema.ex

query do
  field :tag, :tag do
    resolve ...
    middleware :keys_to_atoms
  end

  def keys_to_atoms(res, _config) do
    new_ft1 = res.value.fields_title |> Map.new(fn {k,v} -> {String.to_atom(k), v} end)
    new_ft2 = res.value.fields_type |> Map.new(fn {k,v} -> {String.to_atom(k), v} end)
    new_value = Map.merge(res.value, %{fields_title: new_ft1, fields_type: new_ft2})

    %{res | value: new_value}
  end
end

It is very important to avoid String.to_atom on data that comes from users. If the user can set fields_title or fields_type then they have an ability to crash your server. Atoms are not garbage collected, so as more users send different fields you will eventually run out of memory.

1 Like

Hello @benwilson512, the suggested middleware works great, but unfortunately it is global.
What would be the recommended to do this just for a few objects?

You can obviously define a custom middleware for each field in the object, but for that you can just write a resolver for each field and it would look cleaner.

Is there any way to replace the Absinthe.Middleware.MapGet for all fields, or even add a middleware before it that would resolve each field differently? (defined in a per object basis)

Nope! The first bit does talk about globally replacing middleware, but you can scope it to just individual fields or objects. There is an example here with the query object Absinthe.Middleware — absinthe v1.7.6

Basically, the def middleware callback is given both the field and the object one at a time, so you can apply any middleware you like at whatever granularity you like.

1 Like

Rather than specifying for each field which type of parent object to expect, something I’ve done is just clone the default MapGet middleware but have it check for both atom and string keys.

defmodule MyAppSchema.Middleware.MapGet do
  @behaviour Absinthe.Middleware

  @impl Absinthe.Middleware
  def call(resolution, key) do
    with %{state: :unresolved, source: source} <- resolution do
      value = Map.get(source, key, Map.get(source, Atom.to_string(key)))
      %{resolution | state: :resolved, value: value}
    end
  end
end

@benwilson512
Thank you, makes sense.

@brettbeatty
My question was precisely how would I avoid doing the exact same code as you posted, as it modifies the behaviour for all schema. I should have mentioned that in my post, it might be helpful for viewers finding it. So, thank you, it might not be a bad solution at the end for some use cases :wink: