Ecto.Changeset and data with dynamic schema

I’m trying to build a validation via Ecto.Changeset for dynamic data where the schema of the data is user-defined.

My example schema currently is:

[
      %{
        "label" => "Status of Ticket",
        "id" => "status_of_ticket",
        "type" => "select",
        "options" => ["todo", "doing", "done", "cancelled"]
      },
      %{
        "label" => "Component",
        "id" => "component",
        "type" => "text"
      }
    ]

This can be created and changed by users. From this schema, I’m building an Ecto.Changeset with the validation rules in place:

defp to_changeset(schema, values, params \\ %{}) do
    names = Enum.map(schema, &Map.get(&1, "id"))

    types =
      Enum.reduce(schema, %{}, fn %{"type" => type, "id" => id}, acc ->
        case type do
          "select" -> Map.put(acc, id, Select.storage_type()) # :string
          "text" -> Map.put(acc, id, Text.storage_type()) # :string
          _ -> raise "Unknown prop type #{type}"
        end
      end)

    changeset = Ecto.Changeset.cast({values, types}, params, names)

    Enum.reduce(schema, changeset, fn %{"type" => type} = prop, acc ->
      case type do
        "select" -> Select.changeset(acc, prop)
        "text" -> Text.changeset(acc, prop)
        _ -> raise "Type #{type} not known!"
      end
    end)
  end

Here, schema is the schema from above, values is a map that holds the current values stored (as a JSON object in the Database) and params is whatever has been changed in the form.

This fails with:

** (ArgumentError) cast/3 expects a list of atom keys, got key: `“status_of_ticket”``

I have converted all keys in names via String.to_atom/1 (against my better judgement), I get:

** (ArgumentError) unknown field :status_of_ticket given to cast. Either the field does not exist or it is a :through association (which are read-only). The known fields are: “component”, “status_of_ticket”

If I change the keys in types to be atoms as well, I get:

** (FunctionClauseError) no function clause matching in Ecto.Changeset.validate_change/3

At that point I figured I had fought this system enough. Is there a way I can use Ecto for this specific use-case? Or am I better off building the (currently pretty simple) validation logic myself?

I think the best way to use changesets with user defined data is by mapping user defined keys to a fixed set of atoms. Changesets aren‘t built to support string keys.