Ecto.Changeset.cast with schemaless types: where are all possible types documented?

Hello,
I am trying to cast data for which I specify types dynamically at runtime. In my particular case I’d like to validate that a single field is an array of string-keyed maps with single key/value both of which are strings, and the value should be one of a strict set of strings.

It’s about casting a standardised (in our company) sorting parameters passed to our Phoenix controllers.

The Phoenix parameters format goes like this:

%{
  sort: [ %{"birthdate" => "desc"}, %{"inserted_at" => "asc"} ]
  # all other parameters
}

(This is achieved by sending an HTTP query like so: ?sort[][birthdate]=desc&sort[][inserted_at]=asc)

At the moment I am doing this:

fields = [:sort]
params = %{"sort" => [%{"birthdate" => "desc"}, %{"inserted_at" => "desc"}]}
types = %{sort: {:array, :map}}
Ecto.Changeset.cast({%{}, types}, params, fields)

And it works (meaning the resulting changeset is valid).

However, I wonder if there’s a way to achieve finer-grained control of the type of the sort key. I blindly tried this: types = %{sort: {:array, {:map, :string}}} and it also worked, but to be completely honest I have no idea why. :003:

Is there a way to achieve this finer-grained type control when casting with Ecto’s Changeset? If so, where is it documented?

The docs start here: https://hexdocs.pm/ecto/3.1.7/Ecto.Schema.html#module-primitive-types
Wherever you read inner_type you can nest any other type available, which is what makes {:array, {:map, :string}} possible.

Edit:

I’d also not really make the casting to strict, because all you get as error would be is invalid. Rather make casting ensure you get a known format of data and let other validation steps validate the details with proper error messages.

1 Like

Thank you. But what does it represent really? A map with string keys? With string values? Or both?

Really good advice, appreciate it. How would you go about doing it?

1 Like

As keys are always in “danger” of being serialized to strings (because they’re external to the VM when stored in a db) it means {:map, :string} is a map of :string values.

2 Likes

I ended up eschewing cast for some of those special fields altogether. I ignore them in the initial cast call and then call a function that does exhaustive type and data shape validation, calling add_error or put_change wherever appropriate. Communicates errors and intent much better than having a generic "is invalid" message indeed.