Using Ecto for accepting key/value pairs in a user form

I have a form that is backed by a table. As part of the form I have a section where I accept a key, value, and data type from the user. The idea is to build up a JSON object but allow the user to add them one by one. On the backend I want to do some validation on these key/value pairs such as ensuring the input can be cast to the data type selected, and making sure they’re not blank. I have all that working.

The problem I’m running into now is that I’d like to do something like this:

defmodule Tree do
  use Ecto.Schema
  
  schema "tree" do
    embeds_one :leaf, Leaf
  end
end

defmodule Leaf do
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    field :leafs, :map, default: %{}
  end
  
  def changeset(schema, %{"leafs" => leafs}) do
    schema
    |> cast(%{"leafs" => leafs}, [:leafs])
    |> validate_and_cast_custom_facts()
  end
  
  @blacklist = ["apple", "orange"]
  
defp validate_and_cast_custom_facts(changeset) do 
    validate_change changeset, :leafs, fn :leafs, {k, v} ->
      if k in @blacklist do
        ["#{k}": "#{k} is a blacklisted key."]
      else
        []
      end
    end
  end
end

The problem is that it involves converting use input (the key from the form) into an atom in order to add it to the changeset as an error. I can’t think of a better way to do this as I’m using Changesets throughout in API responses and forms for rendering errors. Is there another way I could do this?

The correct key for the error would be the field name, which is :leafs. So, I would do:

[leafs: "#{k} is a blacklisted key."]

Thanks to some help from @schrockwell on Slack, I ended up shoving some extra information into the meta data for an error like this:

add_error(changeset, :leafs, "#{k} is a blacklisted key", bad_key: k)

The problem was I needed to be able to tell which key was bad if I had something like this.

%Leaf{leafs: %{"good_key" => :a, "blacklisted_key" => :b}