Skip to content

Commit cb2ba4d

Browse files
committed
Make clear the distinction of emptiness between cast and validate_required
Also ensure `field_missing?` considers empty maps/lists as missing. Closes #4673.
1 parent d4cc6c4 commit cb2ba4d

File tree

4 files changed

+399
-199
lines changed

4 files changed

+399
-199
lines changed

lib/ecto/changeset.ex

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule Ecto.Changeset do
1414
a third-party, you must explicitly list which data you accept.
1515
For example, you most likely don't want to allow a user to set
1616
its own "is_admin" field to true
17-
17+
1818
* **type casting** - a web form sends most of its data as strings.
1919
When the user types the number "100", Ecto will receive it as
2020
the string "100", which must then be converted to 100.
@@ -161,22 +161,15 @@ defmodule Ecto.Changeset do
161161
162162
When applying changes using `cast/4`, an empty value will be automatically
163163
converted to the field's default value. If the field is an array type, any
164-
empty value inside the array will be removed. When a plain map is used in
165-
the data portion of a schemaless changeset, every field's default value is
166-
considered to be `nil`. For example:
167-
168-
iex> data = %{name: "Bob"}
169-
iex> types = %{name: :string}
170-
iex> params = %{name: ""}
171-
iex> changeset = Ecto.Changeset.cast({data, types}, params, Map.keys(types))
172-
iex> changeset.changes
173-
%{name: nil}
164+
empty value inside the array will be removed. For schemaless changesets,
165+
the default value is always `nil`.
174166
175167
Empty values are stored as a list in the changeset's `:empty_values` field.
176168
The list contains elements of type `t:empty_value/0`. Those are either values,
177-
which will be considered empty if they
178-
match, or a function that must return a boolean if the value is empty or
179-
not. By default, Ecto uses `Ecto.Changeset.empty_values/0` which will mark
169+
which will be considered empty if they match, or a function that must return
170+
a boolean if the value is empty or not.
171+
172+
By default, Ecto uses `Ecto.Changeset.empty_values/0` which will mark
180173
a field as empty if it is a string made only of whitespace characters.
181174
You can also pass the `:empty_values` option to `cast/4` in case you want
182175
to change how a particular `cast/4` work.
@@ -2524,22 +2517,21 @@ defmodule Ecto.Changeset do
25242517
You can pass a single field name or a list of field names that
25252518
are required.
25262519
2527-
If the value of a field is `nil` or a string made only of whitespace,
2520+
If the value of a field is `nil` or an empty string/array/map,
25282521
the changeset is marked as invalid, the field is removed from the
2529-
changeset's changes, and an error is added. An error won't be added if
2530-
the field already has an error.
2531-
2532-
If a field is given to `validate_required/3` but it has not been passed
2533-
as parameter during `cast/3` (i.e. it has not been changed), then
2534-
`validate_required/3` will check for its current value in the data.
2535-
If the data contains a non-empty value for the field, then no error is
2536-
added. This allows developers to use `validate_required/3` to perform
2537-
partial updates. For example, on `insert` all fields would be required,
2538-
because their default values on the data are all `nil`, but on `update`,
2539-
if you don't want to change a field that has been previously set,
2540-
you are not required to pass it as a parameter, since `validate_required/3`
2541-
won't add an error for missing changes as long as the value in the
2542-
data given to the `changeset` is not empty.
2522+
changeset's changes, and an error is added. An error won't be added
2523+
if the field already has an error.
2524+
2525+
Keep in mind that, because `validate_required/3` is almost always called
2526+
after `cast/4`, the `:empty_values` property of `cast/4` plays an important
2527+
role in `validate_required/3`. Fields which are considered empty (such as
2528+
strings made of whitespace) are discarded from the changeset, and therefore
2529+
`validate_required/3` will check for the current value in the data. If the
2530+
current value is `nil` or an empty string/array/map, then an error is
2531+
added. In other words, `validate_required/3` validates the shape of the data
2532+
after casting. If you have complex rules for when a field is required or not,
2533+
then those rules should be applied on `cast/4`, and `validate_required/3` then
2534+
performs a simple value check.
25432535
25442536
Do not use this function to validate associations that are required,
25452537
instead pass the `:required` option to `cast_assoc/3` or `cast_embed/3`.
@@ -2619,9 +2611,9 @@ defmodule Ecto.Changeset do
26192611
26202612
"""
26212613
@spec field_missing?(t(), atom()) :: boolean()
2622-
def field_missing?(%Changeset{} = changeset, field) when not is_nil(field) do
2623-
ensure_field_not_many!(changeset.types, field) && missing?(changeset, field) &&
2624-
ensure_field_exists!(changeset, changeset.types, field)
2614+
def field_missing?(%Changeset{types: types} = changeset, field) when not is_nil(field) do
2615+
ensure_field_not_many!(types, field) && missing?(changeset, field) &&
2616+
ensure_field_exists!(changeset, types, field)
26252617
end
26262618

26272619
@doc """
@@ -2855,14 +2847,8 @@ defmodule Ecto.Changeset do
28552847
"before calling validate_required/3 or field_missing?/2. " <>
28562848
"You may also consider passing the :required option to Ecto.Changeset.cast_assoc/3"
28572849

2858-
value when is_binary(value) ->
2859-
value == ""
2860-
2861-
nil ->
2862-
true
2863-
2864-
_ ->
2865-
false
2850+
value ->
2851+
value in [nil, "", [], %{}]
28662852
end
28672853
end
28682854

test/ecto/changeset/embedded_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ defmodule Ecto.Changeset.EmbeddedTest do
760760

761761
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
762762
changeset = Changeset.validate_required(base_changeset, :posts)
763-
assert changeset.valid?
763+
refute changeset.valid?
764764
end) =~ ~r/attempting to determine the presence of embed_many field/
765765
end
766766

0 commit comments

Comments
 (0)