Skip to content

Return :error when casting malformed datetimes without seconds - #4802

Merged
josevalim merged 1 commit into
elixir-ecto:masterfrom
ybai08:fix-short-datetime-cast-errors
Oct 17, 2026
Merged

josevalim merged 1 commit into
elixir-ecto:masterfrom
ybai08:fix-short-datetime-cast-errors

Conversation

@ybai08

@ybai08 ybai08 commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Ecto.Type.cast/2 for :naive_datetime and :utc_datetime (and the _usec variants) raises FunctionClauseError on any malformed string shaped like YYYY-MM-DD HH:MM. It should return :error. Strings like "2015-01-23 ab:50" or "abcd-01-23 23:50" match the clause for datetimes without seconds. to_i/1 turns the bad parts into nil, and those nils go straight into NaiveDateTime.new/6, which fails its guards in Calendar.ISO.valid_date?/3 / valid_time?/4.

So a form param can crash Ecto.Changeset.cast/4 where it should add a validation error:

Ecto.Changeset.cast({%{}, %{at: :naive_datetime}}, %{"at" => "2015-01-23 xx:50"}, [:at])
# before: ** (FunctionClauseError) no function clause matching in Calendar.ISO.valid_time?/4
# after:  %Ecto.Changeset{valid?: false, errors: [at: {"is invalid", ...}]}

The fix builds the date and time with the existing guarded cast_date/3 and cast_time/4 helpers, the same way the map clause of cast_naive_datetime/1 already does. Valid inputs give the same results as before.

Checks run:

  • mix test test/ecto/type_test.exs: before the fix, 2 failures (naive_datetime casting naive datetime, utc_datetime cast), both FunctionClauseError. After the fix, 99 passed.
  • mix test: 1611 passed (97 doctests, 1514 tests)
  • mix compile --force --warnings-as-errors: clean
  • mix format --check-formatted lib/ecto/type.ex test/ecto/type_test.exs: clean
  • A script that casts 33 datetime strings (valid, invalid, negative years, offsets, the no-seconds form) to all four datetime types, run before and after the change. The only differences are the "abcd-01-23 23:50" rows, which go from raising to :error.

Not run: the earthly integration matrix, and Elixir versions other than 1.20.4 / OTP 29.

Strings such as "2015-01-23 ab:50" matched the YYYY-MM-DD HH:MM clause
of the :naive_datetime and :utc_datetime casts, and the nil parts
produced by to_i/1 were passed to NaiveDateTime.new/6, which raised a
FunctionClauseError instead of returning :error. Build the date and
time through the guarded cast_date/3 and cast_time/4 helpers instead.
@josevalim
josevalim merged commit 8a16a45 into elixir-ecto:master Oct 17, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants