Why can't Ecto cast integer to string?

iex(1)> import Ecto.Type
iex(2)> cast(:integer, "3")
{:ok, 3}
iex(3)> cast(:string, 3)

What’s the reason for this behavior?

I had some data I passed to changeset and was sure it will handle the types, though numbers instead of strings (Poison’s “fault”) triggered errors which was not expected so I’d like to know more to be able to adjust my expectations :slight_smile:

Usually, casting number values to String depends on the base used, as in Integer.to_string. Check the iex help with h Integer.to_string/2 for more information.

Ecto does not cast this value because it is ambiguous.

1 Like

Ecto will cast from strings since when using HTML forms strings is all you get from browsers. Casting to string, on the other hand, is rarely required. If it’s something you need, the best way would probably be with a custom type.

1 Like

@arvidkahl there’s also to_string function that uses String.Chars protocol and inspect that casts anything to string though it would not be ambiguous if ecto did it since Ecto.Type.cast is a low level function that is used by changeset’s cast where I just list the fields and expect them to be casted. To get around it I have to cast data before passing it to changeset or use custom Ecto types.

@michalmuskala thanks that’s what I thought. Though it breaks when the data comes from other source (in my case from a channel via Poison), I think casting given data to given type without making further assumptions would be more predictable and consistent.

1 Like

I know this is an old discussion, but I believe it’s worth rethinking. I believe the assumption that the primary method of obtaining input is HTML is faulty. JSON bodies, either with the application acting as an API server or via channel, are an extremely common use case. Since JSON can pass typed data, the conversion from something to a string is needed. Otherwise, everywhere you need to construct a query against a string field, you need to add defensive programming to ensure that a bad query does not cause an exception and a 500 response.

I created a custom Ecto.Type that I named Stringable for this very purpose. We take in the same logical data from different sources and have to map things into common Ecto structs. When we want to preserve the source system’s own id, we use a string field, but some source systems give us integer ids. This custom type removed a lot of defensive calls in our code.

The core of the custom Stringable is

  def cast(s) when is_binary(s), do: {:ok, s}

  def cast(val) do
    if String.Chars.impl_for(val) do
      {:ok, to_string(val)}
      {:error, "cannot convert to string"}