How to validate dynamic fields in embedded_schema

Hi all folks!
In the last few days I was trying to make my embedded schema fields dynamic. What I mean is that I’ve got a User model that embeds_one profile:

defmodule MyApp.User do
  schema "users" do
    field :email
    field :status
    ...
    embeds_one :profile, MyApp.Profile
  end
end

and

defmodule MyApp.Profile do
  embedded_schema do
    field :first_name
    field :last_name
    ...
  end

Everything works fine with static embedded fields declaration, but what about dynamic fields?

If I would inject Profile fields dinamically say by using a configuration like this:

[
%{
      name: "first_name",
      data_type: "string",
      rule_type: "required",
      default: ""
    },
    %{
      name: "last_name",
      data_type: "string",
      rule_type: "required",
      default: ""
    },
    %{
      name: "fiscal_code",
      data_type: "string",
      rule_type: "required",
      default: ""
    },
    %{
      name: "date_of_birth",
      data_type: "string",
      rule_type: "optional",
      default: ""
    }
]

I can use a simple field :profile into User model instead of embeds_one :profile and remove embedded_schema entirely in Profile model and use cast to validate

    {data_value, data_type}
    |> cast(params, Map.keys(data_type))
    ...

So good so far but I cannot use cast_embed validation on User model anymore like this:

    struct
    |> changeset(params)
    |> cast_embed(:profile, required: true, with: &MyApp.Profile.registration_changeset/2)

cause cast_embed and put_embed is expecting an embedded static schema.

Is there an alternative way to validate User and Profile models using dynamic fields?

Thanks for your help

I’m looking for a reasonable way of doing the same thing. Did you manage to make this work?

it’s 2019 and there doesn’t seem to be much info about this usecase - would appreciate extra details :slight_smile:

1 Like

Changesets work with atom keys, so they’re inherently unfit for dynamic validations, as converting user data to atoms (field names) is essentially a DOS vector on the beam. There are a few ways around that:

  • Validate each field on it’s own using a generic key like :field in the changeset. This works well for validation, but not for passing errors back to e.g. a phoenix form.
  • Same as above, but implement Phoenix.HTML.FormData for a custom struct, which stores information like errors.
  • Mapping string field names to a fixed set of atom keys you control. This way you have the best of both worlds. Atoms for the changeset and still string keys for your setup, but you need to map between both representations. I’ve gone this route for my use-case and just wrapped all the mapping logic in a struct, which holds all the necessary data and the resulting changeset.
  • Implement FormData and validation custom. Maybe there are even validation libraries out there being able to work with string keyed data.

If you have atom keys or you’re not worried about converting strings to atoms then you can look into schemaless changeset, which are actually quite simple to use. I’d suggest the “what’s new in ecto 2” ebook for more information on those.

Edit: Also try to not nest those dynamic data as this makes things even more complex. Rather validate things in multiple steps and put dynamic fields at the root level of your forms.

4 Likes