"Any" type for Ecto embedded field?

Is there a way to describe an embedded field that can be any type? Something like this:

embeds_one :filter, Filter do
  field :arg, :any
end

It feels like this should be supported somehow, but I couldn’t find anything. Should I just go ahead and custom-type it?

Edit: just realized you need to return the “underlying schema type” in a custom type, which presents the same issue.

1 Like

The only way to have any is if you can build it on top of a ecto primitive type. Using :binary and term_to_binary for example. Otherwise how should ecto be able to convert the data to something the database adapter can convert to something the db understands.

I suppose the same way it converts keys and values in any arbitrary map that we give it for a :map field. i.e. if we have:

embeds_one :object, Object do
  field :value, :map
end

And save %Object{value: %{string: "42", number: 42}} it has no issues saving the string as a string and the number as a number, even though we don’t specify their types. It just maps them to JSON types I suppose? What I’m aiming for here is having the same type-tolerant behavior for a schema field (that’s already in an embedded object).

I guess it’s easier for :map because the outer shape is encoded in the type; it’s always a map. The different values are always at least one level nested within the containing column. This constraint is not present for :any unless it’s only available to embeds. By now there are no primitive types limited like that.

1 Like

I’d also still suggest to look into custom types. Iirc the type/0 callback is not really used for embeds and with ecto 3.2 there’s even better support for encoding embedded data for storage.

Oh? That’s interesting, I’ll give it a try just returning whatever type is passed on, see what happens. Thanks for the hint :slight_smile:

For posterity, something like this does seem to work:

defmodule Ecto.Type.Any do
  use Ecto.Type
  def type, do: :map

  def cast(value)
    when is_map(value) or
         is_list(value) or
         is_number(value) or
         is_binary(value) or
         is_boolean(value), do: {:ok, value}

  def cast(value), do: {:error, "cannot cast #{inspect value}"}
  
  def load(value)
    when is_map(value) or
         is_list(value) or
         is_number(value) or
         is_binary(value) or
         is_boolean(value), do: {:ok, value}
  def load(value), do: {:error, "cannot load #{inspect value}"}

  def dump(value)
    when is_map(value) or
         is_list(value) or
         is_number(value) or
         is_binary(value) or
         is_boolean(value), do: {:ok, value}

  def dump(value), do: {:error, "cannot dump #{inspect value}"}
end

Even shorter:

defmodule EctoTypeAny do
  alias Ecto.Type
  @behaviour Type

  @impl Type
  def type, do: :any

  @impl Type
  def cast(value), do: Type.cast(:any, value)

  @impl Type
  def load(value), do: Type.load(:any, value)

  @impl Type
  def dump(value), do: Type.dump(:any, value)
end

Then in the schema:

embedded_schema do
  field :value, EctoTypeAny
end
2 Likes