Using embeds_one as a form helper and deriving persisted fields — what is the correct approach?

I have a schema like this:

embeds_one :product_profile, ProductProfile, on_replace: :delete, load_in_query: false

I use product_profile as a form/helper structure in LiveView (e.g. live_select), not something I want to persist.

However, I also rely on it for more than just selecting a product: it drives a lot of calculations (pricing, discounts, etc.), so keeping it around in the changeset is very convenient.

From it, I derive product_id inside the changeset:

|> cast_embed(:product_profile)
|> sync_product_id()

Before inserting, I manually transform the params:

filtered_items =
  proposal_items_list
  |> Enum.map(fn {_i, item} ->
    product_id = get_in(item, ["product_profile", "product_id"])

    item
    |> Map.put("product_id", product_id)
    |> Map.delete("product_profile")
  end)

This works, but feels a bit manual / hacky.

Is this considered an acceptable pattern in Ecto (using an embedded schema mainly as a transient helper for UI + calculations, while extracting a persisted field from it), or is there a more idiomatic approach to avoid reshaping the params before insert?

Parsing/validating params is one of the explicit use cases for Ecto, so no question there, I don’t think.

Personally I consider this a bit of an anti-pattern. To me it feels much cleaner to cast everything into my object first, then validate and make adjustments based on the resulting changes, all within the scope of the same Changeset. Then after applying changes I can pass the resulting object fully confident of its shape/data.

I ended up dropping embeds_one and went with a custom Ecto.Type instead:

field :product_profile, ProductProfile, virtual: true

This way I still get a proper struct (not just a raw map), I can handle casting inside the type, and use it for calculations + deriving product_id in the changeset.

It’s virtual, so nothing gets persisted, which is what I wanted in the first place.

Previously I was manually reshaping the params and then validating again with a separate changeset before Repo.insert, but this feels cleaner now since everything happens in one place.

Would this be considered an acceptable approach in Ecto?

For context, after casting the params, the product_profile looks roughly like this:

product_profile: %Pacta.ProductProfile{
product_name: “ADF Monitorif”,
product_id: 45,
category: :ADR,
base_price: %Money{amount: 12600000, currency: :HUF},
discounts: [
%{
discounted_price: %Money{amount: 11500000, currency: :HUF},
max_months: 6,
min_months: 3
},
%{
discounted_price: %Money{amount: 10300000, currency: :HUF},
max_months: 11,
min_months: 7
},
%{
discounted_price: %Money{amount: 9300000, currency: :HUF},
max_months: 12,
min_months: 12
}
]
}

I use this not only for calculations and deriving product_id, but also for UI/display purposes in LiveView (so the same data is used for visual rendering as well)

Hey there,

I’m a little confused as to what the data model looks like and why there’s a need to dynamically determine things like the product_id and price. If this is some product page wouldn’t you be showing a list of products, I would think that each product would hold the information about price in the database

I think the confusion is around how pricing works here.

I don’t store multiple fixed prices on the product. The database has the “base price + default discount tiers” (in a separate discounts table), and the final price depends on the selected duration.

For example:

  • 1–2 months → base price
  • 3–6 months → discounted price A
  • 7–11 months → discounted price B
  • 12 months → discounted price C

In the form I use a “form changeset”, so I’m not trying to mirror the DB structure exactly. The product_profile is just a temporary helper struct that combines the product and its discounts so I can calculate and display the actual prices in the UI.

Nothing from it is persisted — I only derive things like product_id from it in the changeset.

So the DB holds the rules, and the form works with a computed, UI-friendly version of it.