Polymorphic embedded structs
I've recently solved a similar issue, and as I see it you have two options. Either you ...
Use a custom Ecto.Type
This allows you to exactly control what kind of data you want to encode into the field. By doing this you can retain the module of the struct and the fields with relative ease.
A possible implementation might look like this:
defmodule EctoStruct do
use Ecto.Type
def type, do: :map
def cast(%_{} = struct), do: {:ok, struct}
def cast(_), do: :error
def dump(%module{} = struct) do
data = %{
"module" => Atom.to_string(module),
"fields" => Map.from_struct(struct)
}
{:ok, data}
end
def load(%{"module" => module, "fields" => fields}) do
module = String.to_existing_atom(module)
fields = Enum.map(fields, fn {k, v} -> {String.to_existing_atom(k), v} end)
{:ok, struct!(module, fields)}
rescue
_ -> :error
end
end
With this in place you can "simply" use field :my_struct, EctoStruct
in your schema.
Alternatively you ...
Reconsider your choice of database
A tree is an inherently connected data structure. Depending on your exact requirements and how deep your tree becomes, traversing said tree with Postgres can become very slow very fast.
While I solved the issue mentioned earlier I hit said performance issues quite early, having to use recursive joins and materialized views to stay close to usable response times.
Since then I switched over to a graph database (Neo4j) and my performance issues completely disappeared. This would also easily allow you to encode various differing struct types into your tree by making use of Labels
.
Depending on your particular requirements this might be worth a thought.