brettbeatty

brettbeatty

How do y’all organize your Ecto changeset functions? Right now I’ve got a mix of two different styles in a codebase:

  • “one changeset to rule them all” I just have a changeset that can modify any non-auto fields and use it anywhere I want to perform inserts/updates with the schema
  • “a changeset per operation” I currently prefer to have one changeset per update operation. So if I have my_field, my_field_type, and my_field_status as three separate fields that usually get updated together I might have a my_fieldset_changeset function that casts and validates just those fields.

I like having a changeset per operation because I know it will only change the fields I intend to change, and (this may be bikeshedding on my part) if I change where data lives it’s easier to find the exact places that change those fields.

But sometimes I find myself wanting to break up changesets more by role, separating the fields that can be updated by my app from the fields that can be updated by a user (or whatever untrusted input). I’m envisioning either a system_changeset/2 and a user_changeset/2 or a changeset/3 that looks something like this:

def changeset(my_resource, system_attrs, user_attrs) do
  my_resource
  |> cast(system_attrs, __schema__(:fields))
  |> cast(user_attrs, @user_editable_fields)
  |> other_validations()
end

I kind of like this idea because then anywhere the user passes params, even if they pass params unintended for the operation, we still limit them to only fields they’re allowed to update anyway. So kind of like a middle ground between my existing changeset patterns. So the tradeoff with my per-operation pattern would be giving up some of the understanding of where updates happen for just having fewer changeset functions to worry about.

I do think the changeset/3 example above has one big downside: I use changesets very differently for user-provided data and system-provided data. With untrusted data I perform all the validations and do an insert/update that may return a changeset if the validations fail. If I get something wrong with the system-provided data I don’t want to tell the user they did something wrong or potentially expose fields they know nothing about. With trusted data I don’t do as many validations and do insert!/udpate! because it’s more exceptional for the data to be wrong. I could just call the changeset/3 example with an empty map in either attrs column and only do the updates I want, but I would maybe tend to just do separate changesets at that point.

I guess for me too having a more universal+/system changeset has the bonus of working better for seeds and test fixtures where I may want control of every single field.

But I’m curious how the community tends to organize changesets. What patterns or principles have you found helpful when working with changesets? I feel like I have to be over-complicating the issue.

Showing Posts 1 to 8

egze

egze

You can also do like @sasajuric https://medium.com/very-big-things/towards-maintainable-elixir-the-core-and-the-interface-c267f0da43 and move changesets to your context.

Me personally, I like to stick with a single schema changeset function and have special changeset functions rarely, when needed. For special fields like :is_admin or something.

garrison

garrison

I keep separate changesets for different functionality. You can also compose changesets, which I don’t see mentioned often.

E.g. I have a Bookmark schema which holds both user fields (the name and url) and data fetched from the web (title, page content, an image url, etc). Both need to be validated, but sometimes I only need to validate the latter (when fetching later).

So on creation I can do this:

%Bookmark{}
|> Bookmark.user_changeset(%{name: "foo"})
|> Bookmark.metadata_changeset(%{title: "Hello world"})

But if I was just updating the fetched metadata I could do this:

Bookmark.metadata_changeset(bookmark, new_metadata)

The distinction in this case is that the user fields are validated for the user while the metadata fields are validated with very large constraints mostly as a sanity check (e.g. 4096 char limit for a URL, which should never be hit by normal use).

al2o3cr

al2o3cr

IMO functions are basically free, make as many of them as you need to keep things clear.

The worst possible messes I’ve seen in code have been when everything was jammed through a “universal” function and then the needs of different callsites diverged. Suddenly every place that Foo.change_all_the_things is called needs to be reviewed (it’s covered by tests, right?)

Regarding the last case you mention (test setup / factories) - there’s no rule that says those can’t have their own test-specific modules. I’d keep the “change every column” stuff there in preference to having it sitting around in the main app waiting for misuse.

dimitarvp

dimitarvp

Same. Good prefixes do the job, provided you don’t need 20+ changesets.

tfwright

tfwright

Absolutely one of my favorite things I’ve picked up from him. That tip alone convinced me to read Elixir In Action. It can be combined with “changeset functions” by abstracting common pieces into the schema when that becomes a good refactoring opportunity.

brettbeatty

brettbeatty OP

Hmm yeah that resonates with the part of me that wants a stronger separation between my web layer and the rest of my app.

Yeah I think I’m coming back around to that approach.

I think there’s a balance there, balancing “as many of them as you need” with DRY principles. I’ve written functions before that did essentially the same thing as each other with minor differences, then as “essentially the same thing” became increasingly complex how each function handled the edge cases started unintentionally to diverge a bit. But that’s probably more a sign I needed to refactor at that point. And they weren’t just changeset functions.

I like that idea. Do you find that the common pieces are usually more like entire changeset functions for a subset of fields or more like custom validations and such?

tfwright

tfwright

Usually there is a set of base required fields and other validations that I put there. Logic that would be shared between all context functions manipulating changesets, less to DRY anything up and more to make things explicit and as self-documenting as possible. I tend to be really particular about API surface so I generally only cast fields, for example, that are needed by some client for a specific operation, so I don’t find putting cast logic there to be as helpful as it would be if you just include all fields by default.

cjbottaro

cjbottaro

I’m probably going to get drug through the mud for this, but…

I just put changeset functions directly in my LiveView, not in my models. So far, I’ve never had to reuse them, so I just make them specific to the page they are being used on.

Further, more often than not, the form being rendered does not map 100% cleanly to any model. So there is an an Ecto embedded_schema directly in the LiveView also… :smiley:

If there is a need for sharing, I’ll refactor into a model or context.

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews