Schemaless Changeset with nested associations for JSON Schema

The schema is not known ahead of time, and I’m looking at my options how to generate a form at runtime and validate inputs.

1st option is to use a JSON Schema Based Editor with JS. I think it will work, and there’s even a validation library in Elixir for JSON schemas.

But I’m wondering what would it take to build a flexible form renderer and validator with pure Elixir, which can work based on JSON schemas.

I looked at Ecto Schemaless Changesets, with the thought that I could create my changeset at runtime, but either associations are not supported, or I’m not getting how to use it.

defmodule SchemaConverter do
  
  defp types do
    friend_type = %{
      name: :string,
      age: :integer
    }

    %{
      name: :string,
      age: :integer,
      friends: {:array, friend_type}
    }
  end

  defp params do
    %{
      "name" => "Bob",
      "age" => 25,
      "friends" => [
        %{"name" => "Alice", "age" => 30}
      ]}
  end

  def run do
    Ecto.Changeset.cast({%{}, types()}, params(), [:name, :age, :friends])
  end

end

This doesn’t work. Should I create a custom Friend Ecto.Type at runtime somehow? Is it possible?

** (FunctionClauseError) no function clause matching in Ecto.Type.cast_fun/1    
    
    The following arguments were given to Ecto.Type.cast_fun/1:
    
        # 1
        %{age: :integer, name: :string}
    
    Attempted function clauses (showing 10 out of 25):
    
        defp cast_fun(:integer)
        defp cast_fun(:float)
        defp cast_fun(:boolean)
        defp cast_fun(:map)
        defp cast_fun(:string)
        defp cast_fun(:binary)
        defp cast_fun(:id)
        defp cast_fun(:binary_id)
        defp cast_fun(:any)
        defp cast_fun(:decimal)
        ...
        (15 clauses not shown)

Is there something I’m missing for changesets? Or is there another solution for Elixir?

Just found this cool library: xema

It creates a schema at runtime from json with from_json_schema/2 and can cast and validate. This is pretty sweet and will get me very far.

Now the hard part is rendering the form with the ability to have dynamic nested fields, support enums, fields types, etc. Maybe there’s a xema_form library? :smile:

Also still wondering about the Ecto Schemaless Changesets.

2 Likes

Hello, as I am currently facing a similar problem, I would like to benefit from your previous experience. Have you investigated this further? If so, how did you proceed? Thanks in advance.

You can use something like this:


Mix.install([
  {:ecto, "~> 3.0"}
])

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field(:name, :string)

    embeds_many :address, Address do
      field(:street, :string)
      field(:pin, :integer)
    end
  end

  def changeset(attrs) do
    %__MODULE__{}
    |> cast(attrs, [:name])
    |> cast_embed(:address, with: &child_changeset/2)
    # |> IO.inspect() # uncomment to print result
  end

  def child_changeset(schema, attrs) do
    schema
    |> cast(attrs, [:street, :pin])
  end
end

User.changeset(%{
  "name" => "John Doe",
  "address" => [%{"street" => "Any", "pin" => "123456"}]
})

# run command where file exists: elixir user_file.exs