Accepting attributes by different names than in the resource

Hi all,

I’m creating a resource that represents entities that can be either:

  • :natural persons or
  • :legal persons.

These have most attributes and actions in common and will primarily have to meet the need of having relationships along the lines of claim - belongs_to -> entity where the entity can be of either type. Of course, one would like to be able to query all entities and have their proper full name returned and so on.

I’m strongly suspecting that I should create a polymorphic relationship as described in the guides with an Entity resource and NaturalPerson and LegalPerson resources respectively, but a preliminary question has come to mind while evaluating the actual need for that.

If one were to have a resource Entity without that kind of polymorphism, could one have different :create actions that accept different attributes for the kinds of names that are then stored in :name1, :name2 and :name3 attributes on the Entity resource? Or would one “just” handle that in the UI?


  • :name1:first_name for :natural_person:s and :company_name for :legal_person:s:
  • :name2:middle_name for :natural_person:s
  • :name3:last_name for :natural_person:s

When for instance creating entities:s, I’d like to – somehow! – accept :first_name, :middle_name and :last_name in a :create_natural_person action and :company_name in a :create_legal_person action.

That does not feel very Ash:y however and I guess that it can lead to issues down the line.


As I noted above, I strongly suspect that this is a case of polymorphism.

Asking for a friend etc. :otter:

Thank you in advance good people!

Kind regards,
Carl

You can definitely do it with actions. Their job is to accept whatever inputs and encapsulate some given domain operation:

create :create_natural_person do
  argument :first_name, :string
  argument :middle_name, :string
  ...

  change set_attribute(:name1, arg(:first_name))
  change set_attribute(:name2, arg(:middle_name))
  change set_attribute(:type, :natural)
end

create :create_legal_person do
  argument :company_name, :string

  change set_attribute(:name1, arg(:company_name))
  change set_attribute(:type, :legal)
end

The actions guide and changes guide should have more examples, set_attribute/2 is a builtin but you can write your own.

That’s really great, sometimes it’s simpler than one thinks – thank you! :folded_hands:t2:

Thanks again and that’s up and running for :create actions now, with really nifty calculations and all :pinched_fingers:

But for updates such as :update_natural_person I couldn’t quite figure out the plumbing of it all.

My first thought was to create a custom change that gets the attributes and populates the arguments:

change fn changeset, _context ->
  case Ash.Changeset.get_attribute(changeset, :type) do
    :natural ->
      first_name = Ash.Changeset.get_attribute(changeset, :name1)
      middle_name = Ash.Changeset.get_attribute(changeset, :name2)
      last_name = Ash.Changeset.get_attribute(changeset, :name3)

      changeset
      |> Ash.Changeset.set_argument(:first_name, first_name)
      |> Ash.Changeset.set_argument(:middle_name, middle_name)
      |> Ash.Changeset.set_argument(:last_name, last_name)

    :legal ->
      # TBD
      changeset
  end
end

which indeed does populates the arguments, in AshAdmin, with the current data. But upon any revalidation, one gets errors on the arguments.

Also then tried combining this with force_change_attribute/3 at the end of the action to set the attributes from the arguments, but I realise that it’s biting myself in the behind since I’m getting the old arguments back and no changes are persisted. So there is a lifecycle challenge here obviously. Couldn’t figure it out from the lifecycle documentation unfortunately.

To exhaust all available options before asking I did consult an ML-tool that generated a response “suggesting” that one should use prepare_changes :laughing: I don’t think that is an Ash function. Please note that I never vibe code! Need to exercise by brain (obviously).

Option B: Define a prepare_changes that backfills arguments (:warning: hacky)

If there is an obvious approach here I would be most grateful for any guidance of course.

:thinking: that should work I think? What kind of errors do you get on the arguments?

I would say that it might be easier to handle this in your LiveView though, using value={AshPhoenix.Form.value(form, :name1)} etc.

You might need to only set the argument if its not already set for example.

Thanks for getting back!

The validation errors are just “required” but the action goes through since the arguments have (some) value after all. I have to |> dbg()-look at this with fresh eyes in the morning with a cuppa joe to make complete sense :hot_beverage:

Will revert with findings, including LiveView handling, in the hope that they prove useful for the community :slight_smile:

Got it working by only setting the argument – from data this time – if the argument is nil.

But there is still a validation issue that might deserve a new changset function remove_error/3.

change SetUpdateArguments
@impl true
def change(changeset, _opts, _context) do
  case Ash.Changeset.get_attribute(changeset, :type) do
    :natural ->
      changeset
      |> maybe_set_argument(:first_name, :name1)
      |> maybe_set_argument(:middle_name, :name2)
      |> maybe_set_argument(:last_name, :name3)

    :legal ->
      changeset
      |> maybe_set_argument(:company_name, :name1)
  end
end

defp maybe_set_argument(changeset, argument, attribute) do
  if Ash.Changeset.get_argument(changeset, argument) do
    changeset
  else
    changeset
    |> Ash.Changeset.set_argument(
      argument,
      Ash.Changeset.get_attribute(changeset, attribute)
    )
  end
end

This pre-populates the arguments and the set_argument/2 doesn’t get in the way of provided changes (someone was tired yesterday).

But the thing is that the changeset has validation errors for all the required arguments from the get-go:

[(my_app 0.1.0) lib/my_app/entities/actor/changes/set_update_arguments.ex:25: MyApp.Entities.Actor.Changes.SetUpdateArguments.change/3]
changeset #=> #Ash.Changeset<
  domain: MyApp.Entities,
  action_type: :update,
  action: :update_natural_person,
  tenant: "null",
  attributes: %{},
  relationships: %{},
  arguments: %{first_name: "Kurt", middle_name: "Arne", last_name: "Olsson"},
  errors: [
    %Ash.Error.Changes.Required{
      field: :last_name,
      type: :argument,
      resource: MyApp.Entities.Actor,
      splode: nil,
      bread_crumbs: [],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :invalid
    },
    %Ash.Error.Changes.Required{
      field: :first_name,
      type: :argument,
      resource: MyApp.Entities.Actor,
      splode: nil,
      bread_crumbs: [],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :invalid
    }
  ],
  data: %MyApp.Entities.Actor{
    id: "019870c3-ed58-76a7-9a21-b7948c72e72c",
    type: :natural,
    name1: "Kurt",
    name2: "Arne",
    name3: "Olsson",
    inserted_at: ~U[2025-08-03 16:29:05.240479Z],
    updated_at: ~U[2025-08-04 05:30:37.616108Z],
    name: "Kurt Arne Olsson",
    formal_name: "Olsson, Kurt Arne",
    __meta__: #Ecto.Schema.Metadata<:loaded, "actors">
  },
  valid?: false
>

So when touching the form in AshAdmin the validation errors show even though the changeset reasonably should be valid.

Before touching:

After touching:

Maybe it would be hacky, but wouldn’t it be possible to have a remove_error/3 akin to add_error/3? I noted that add_error/3 also sets the changeset to invalid – maybe it’s impossible to say whether it should be valid or invalid in that case? But if errors are empty after pre-populating the arguments one could perhaps dare to mark the changeset as valid?