Ecto changeset validating all `attrs` are `cast`

I am looking to validate that every key in a changeset attrs has been cast to in a changeset function.

My current approach is something like this:

  def validate_fields(changeset, %{} = attrs) do
    attrs
    |> Map.keys()
    |> Enum.filter(fn k when is_binary(k) ->
      # TODO could cause atom exhaustion. Maybe convert keys in types to string instead?
        not Map.has_key?(changeset.types, String.to_atom(k))
      k ->
        not Map.has_key?(changeset.types, k)
    end)
    |> Enum.reduce(changeset, fn invalid_key, acc ->
      add_error(acc, invalid_key, "invalid attr", [expected_attrs: Map.keys(changeset.types)])
    end)
  end

Used like this:

schema "person" do
  field :name, :string
  embeds_many :pets, Pet
  field :_private, :string
end

def changeset(person, attrs) do
  person
  |> cast(attrs, [:name, :dob, :job])
  |> cast_embed(:pets)
  |> validate_fields(attrs)
end

One issue I have is that I have ‘private‘ fields, that I do not want to be passed in as attrs, in the example above changeset(%{}, %{_private: “blah“}) would not be flagged. What I am looking for is a way to check that only fields that have been cast or cast_embed are present in the attrs, but I can’t see any way to get these out of the Changeset?

I am surprised this is not a common validator people would want, for use cases like validating JSON requests in an API.

I’d argue it’s a bad idea. I makes evolution of data unnecessarily hard as provider of data and receiver of data need to be updated in lockstep. You wouldn’t be able to let the provider send more data in advance before the receiver requires said additional data to be provided. Simply ignoring what you’re not expecting is much more flexible.

But on the question of how to do it: Unless you want to hack support in Changesets do not really support your usecase. Errors and validations are only expected to exist for fields the changeset knows about, not for unknown fields. You got changeset.params for the params used on a changeset, but no record of which fields are provided to cast/4 calls.