Ecto.CastError, expected params to be a :map

Hi there!
I am the new one in Elixir. Lately, I constantly facing the Ecto.CastError, expected params to be a :map.

I created a GraphQL API suing Phoenix and Absinthe. My goal is to convert one struct into another and insert it into another table in database. I mean, I have X and i wanna modify them to become an Y data type and insert them to Y table in DB.

I wrote code below (maybe there is an easier/better way to achieve my goal)

requested_chars.ex

import Ecto.Query, warn: false
  alias AngotiaCatalogsApi.Repo

  alias AngotiaCatalogsApi.RequestedChars.Char, as: RChar
  alias AngotiaCatalogsApi.Chars.Char

defp push_to_prod(char) do
    req_char = elem(char, 1)
    prod_char = req_char
      |> Map.delete(:__meta__)
      |> Map.delete(:__struct__)
      |> Map.delete(:author)
      |> Map.delete(:id)

    prod_char_struct = struct(Char, prod_char)

    %Char{}
      |> Char.changeset(prod_char_struct)
      |> Repo.insert()
  end

Where Char is another db table struct.

char.ex

defmodule AngotiaCatalogsApi.Chars.Char do
  use Ecto.Schema
  import Ecto.Changeset
  alias AngotiaCatalogsApi.Chars.Statistics
  alias AngotiaCatalogsApi.Chars.Settings
  alias AngotiaCatalogsApi.Chars.Monolog
  alias AngotiaCatalogsApi.Chars.Dialog


  schema "chars" do
    field :name, :string
    field :_id, :string
    field :field_diameter, :integer
    field :type, :string, default: "static"
    field :choosed, :string
    field :has_visible_level, :boolean, default: true
    field :char_pic, :string
    field :mob_range, :string
    field :is_agressive_mob, :boolean
    embeds_one :statistics, Statistics, on_replace: :update
    embeds_one :settings, Settings, on_replace: :update
    embeds_many :monologs, Monolog, on_replace: :delete
    embeds_many :dialogs, Dialog, on_replace: :delete

    timestamps()
  end

  def changeset(char, attrs) do
    char
    |> cast(attrs, [:name, :_id, :field_diameter, :type, :choosed, :has_visible_level, :char_pic, :mob_range, :is_agressive_mob])
    |> validate_required([:name, :_id, :field_diameter, :choosed, :has_visible_level, :char_pic])
    |> cast_embed(:statistics, with: &Statistics.changeset/2)
    |> cast_embed(:settings, with: &Settings.changeset/2)
    |> cast_embed(:monologs, with: &Monolog.changeset/2)
    |> cast_embed(:dialogs, with: &Dialog.changeset/2)
  end  
end

Full stack trace:

"# Ecto.CastError at POST /graphiql\n\nException:\n\n    ** (Ecto.CastError) expected params to be a :map, got: `%AngotiaCatalogsApi.Chars.Char{__meta__: #Ecto.Schema.Metadata<:built, \"chars\">, _id: \"160d1867-605f-4949-a6f0-ce9e2de068d6\", char_pic: \"afwa\", choosed: \"NPC\", dialogs: [], field_diameter: 0, has_visible_level: true, id: nil, inserted_at: ~N[2021-01-26 17:00:40], is_agressive_mob: false, mob_range: nil, monologs: [], name: \"test1\", settings: %AngotiaCatalogsApi.Chars.Settings{id: \"a93830bd-912b-4e1b-9a20-1f9ba9fae56a\", resp_time: %AngotiaCatalogsApi.Chars.Settings.RespTime{id: \"b5b6507c-b23e-49a2-af33-c4174284c89c\", max: 130, min: 60}, time_of_occurance: %AngotiaCatalogsApi.Chars.Settings.TimeOfOccurance{id: \"2a9674f4-d804-4e4c-b8cd-d85caf9bedd9\", max: 24, min: 0}}, statistics: %AngotiaCatalogsApi.Chars.Statistics{attack: 1, attack_range: 1, attack_speed: 100, defence: 1, dexterity: 1, health: 1000, id: \"142460ee-bf0c-4654-8ea6-dbbf1f9f4200\", inteligence: 1, jink: 1, level: 1, speed: 1, strength: 1}, type: \"STATIC\", updated_at: ~N[2021-01-26 17:00:40]}`\n        (ecto 3.4.5) lib/ecto/changeset.ex:457: Ecto.Changeset.cast/4\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api/chars/char.ex:30: AngotiaCatalogsApi.Chars.Char.changeset/2\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api/requested_chars/requested_chars.ex:38: AngotiaCatalogsApi.RequestedChars.push_to_prod/1\n        (absinthe 1.5.1) lib/absinthe/resolution.ex:209: Absinthe.Resolution.call/2\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:230: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:185: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/3\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:170: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:67: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3\n        (absinthe 1.5.1) lib/absinthe/pipeline.ex:368: Absinthe.Pipeline.run_phase/3\n        (absinthe_plug 1.5.0) lib/absinthe/plug.ex:445: Absinthe.Plug.run_query/4\n        (absinthe_plug 1.5.0) lib/absinthe/plug.ex:258: Absinthe.Plug.call/2\n        (phoenix 1.5.7) lib/phoenix/router/route.ex:41: Phoenix.Router.Route.call/2\n        (phoenix 1.5.7) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api_web/endpoint.ex:1: AngotiaCatalogsApiWeb.Endpoint.plug_builder_call/2\n        (angotia_catalogs_api 1.0.0) lib/plug/debugger.ex:132: AngotiaCatalogsApiWeb.Endpoint.\"call (overridable 3)\"/2\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api_web/endpoint.ex:1: AngotiaCatalogsApiWeb.Endpoint.call/2\n        (phoenix 1.5.7) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4\n        (cowboy 2.8.0) /home/kostek/Documents/Programming/angotia-catalogs/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2\n    \n\n## Connection details\n\n### Params\n\n    %{\"query\" => \"mutation {\\n  acceptRequestedChar(id: 68) {\\n    id\\n  }\\n}\", \"variables\" => %{}}\n\n### Request info\n\n  * URI: http://localhost:4000/graphiql\n  * Query string: \n\n### Headers\n  \n  * accept: application/json\n  * accept-encoding: gzip, deflate\n  * accept-language: en-US,en;q=0.5\n  * cache-control: no-cache\n  * connection: keep-alive\n  * content-length: 86\n  * content-type: application/json\n  * cookie: next-i18next=en\n  * host: localhost:4000\n  * origin: http://localhost:4000\n  * pragma: no-cache\n  * referer: http://localhost:4000/graphiql?variables=%7B%7D&query=mutation%20%7B%0A%20%20acceptRequestedChar(id%3A%2068)%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D\n  * user-agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0\n\n### Session\n\n    %{}\n"

I see that my struct properly refer to Ecto “chars” schema. So whats wrong?

The input parameter attrs in your code to Ecto.Changeset.cast/4 should be a map, not a struct. The point of the changeset functionality is to build a set of changes to a struct (the char struct that you also input). Providing the changes as a struct doesn’t really make sense then.

So, instead of passing a struct to the changeset function, just pass a map.

Thanks for reply!
I changed my code into:

defp push_to_prod(char) do
    req_char = elem(char, 1)
    prod_char = req_char
      |> Map.delete(:__meta__)
      |> Map.delete(:__struct__)
      |> Map.delete(:author)
      |> Map.delete(:id)

    %Char{}
      |> Char.changeset(prod_char)
      |> Repo.insert()
  end

Stack trace:

"# Ecto.CastError at POST /graphiql\n\nException:\n\n    ** (Ecto.CastError) expected params to be a :map, got: `%AngotiaCatalogsApi.Chars.Statistics{attack: 1, attack_range: 1, attack_speed: 100, defence: 1, dexterity: 1, health: 1000, id: \"142460ee-bf0c-4654-8ea6-dbbf1f9f4200\", inteligence: 1, jink: 1, level: 1, speed: 1, strength: 1}`\n        (ecto 3.4.5) lib/ecto/changeset.ex:457: Ecto.Changeset.cast/4\n        (ecto 3.4.5) lib/ecto/changeset/relation.ex:128: Ecto.Changeset.Relation.do_cast/6\n        (ecto 3.4.5) lib/ecto/changeset/relation.ex:317: Ecto.Changeset.Relation.single_change/5\n        (ecto 3.4.5) lib/ecto/changeset/relation.ex:112: Ecto.Changeset.Relation.cast/5\n        (ecto 3.4.5) lib/ecto/changeset.ex:800: Ecto.Changeset.cast_relation/4\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api/chars/char.ex:32: AngotiaCatalogsApi.Chars.Char.changeset/2\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api/requested_chars/requested_chars.ex:36: AngotiaCatalogsApi.RequestedChars.push_to_prod/1\n        (absinthe 1.5.1) lib/absinthe/resolution.ex:209: Absinthe.Resolution.call/2\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:230: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:185: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/3\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:170: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:67: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3\n        (absinthe 1.5.1) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3\n        (absinthe 1.5.1) lib/absinthe/pipeline.ex:368: Absinthe.Pipeline.run_phase/3\n        (absinthe_plug 1.5.0) lib/absinthe/plug.ex:445: Absinthe.Plug.run_query/4\n        (absinthe_plug 1.5.0) lib/absinthe/plug.ex:258: Absinthe.Plug.call/2\n        (phoenix 1.5.7) lib/phoenix/router/route.ex:41: Phoenix.Router.Route.call/2\n        (phoenix 1.5.7) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2\n        (angotia_catalogs_api 1.0.0) lib/angotia_catalogs_api_web/endpoint.ex:1: AngotiaCatalogsApiWeb.Endpoint.plug_builder_call/2\n    \n\n## Connection details\n\n### Params\n\n    %{\"query\" => \"mutation {\\n  acceptRequestedChar(id: 68) {\\n    id\\n  }\\n}\", \"variables\" => %{}}\n\n### Request info\n\n  * URI: http://localhost:4000/graphiql\n  * Query string: \n\n### Headers\n  \n  * accept: application/json\n  * accept-encoding: gzip, deflate\n  * accept-language: en-US,en;q=0.5\n  * connection: keep-alive\n  * content-length: 86\n  * content-type: application/json\n  * cookie: next-i18next=en\n  * host: localhost:4000\n  * origin: http://localhost:4000\n  * referer: http://localhost:4000/graphiql?variables=%7B%7D&query=mutation%20%7B%0A%20%20acceptRequestedChar(id%3A%2068)%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D\n  * user-agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0\n\n### Session\n\n    %{}\n"

It looks like error happens due to %AngotiaCatalogsApi.Chars.*{ marks instead just {. Is there an easy way to remove them?

Your current error is because:

cast_embed(:statistics, with: &Statistics.changeset/2)

passes a AngotiaCatalogsApi.Chars.Statistics struct as the second argument to Statistics.changeset/2 but it expects a map.

Okey, I found out whats goin on.

Nicd was right about Map type. My error was due to nested Structs inside Map. So I needed to convert nested Struct into nested Map and everything works fine!

About conversion: Convert a nested struct into a nested map