Skip to content

(cannot_coerce) cannot cast type jsonb to jsonb[] when updating list of embeds field with jsonb as underlying type #614

@martosaur

Description

@martosaur

Code of Conduct

  • I agree to follow this project's Code of Conduct

AI Policy

  • I agree to follow this project's AI Policy, or I agree that AI was not used while creating this issue.

Versions

1.18.4 OTP 27
27.3.3

Operating system

macos

Current Behavior

Following up discussion on Elixir Discord. jsonb (or map) is the recommended type for storing list of embeds in Ecto. Naturally, I tried to do the same with Ash, but run into an error during update.

Reproduction

Here's a one file reproduction
Mix.install(
  [
    {:ash, "~> 3.0"},
    {:ash_postgres, "~> 2.0"}
  ],
  consolidate_protocols: false
)

Application.put_env(:myapp, MyRepo,
  database: "mix_install_examples",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"
)

defmodule MyRepo do
  use AshPostgres.Repo, otp_app: :myapp
end

defmodule Migration0 do
  use Ecto.Migration

  def change do
    create table("profiles") do
      add(:name, :string)
      add(:identities, :jsonb)
      
      add :inserted_at, :utc_datetime_usec,
        null: false,
        default: fragment("(now() AT TIME ZONE 'utc')")

      add :updated_at, :utc_datetime_usec,
        null: false,
        default: fragment("(now() AT TIME ZONE 'utc')")
    end
  end
end

defmodule Accounts.Profile.Identity do
  use Ash.TypedStruct

  typed_struct do
    field :first_name, :string, allow_nil?: false
    field :last_name, :string
  end
end

defmodule Accounts.Profile do
  use Ash.Resource,
    domain: Accounts,
    data_layer: AshPostgres.DataLayer
    
  postgres do
    table "profiles"
    repo MyRepo
  end

  actions do
    defaults [:read, :destroy, create: [:name, :identities], update: [:name, :identities]]
  end

  attributes do
    integer_primary_key :id
    attribute :name, :string
    attribute :identities, {:array, Accounts.Profile.Identity}, default: []
    timestamps()
  end
end

defmodule Accounts do
  use Ash.Domain, validate_config_inclusion?: false

  resources do
    resource Accounts.Profile do
      define :all_profiles, action: :read
      define :profile_by_id, action: :read, get_by: [:id]
      define :create_profile, args: [:name], action: :create
      define :update_profile, action: :update
      define :delete_profile, action: :destroy
    end
  end
end

defmodule Main do
  import Ecto.Query, warn: false

  def main do
    MyRepo.__adapter__().storage_down(MyRepo.config())
    :ok = MyRepo.__adapter__().storage_up(MyRepo.config())
    {:ok, _} = MyRepo.start_link([])
    Ecto.Migrator.run(MyRepo, [{0, Migration0}], :up, all: true, log_migrations_sql: :info)

    profile =
      Accounts.Profile
      |> Ash.Changeset.for_create(:create, %{name: "Joe Armstrong"})
      |> Ash.create!()
    
    profile.id
    |> Accounts.update_profile!(%{identities: [%{first_name: "Foo", last_name: "Bar"}]})
    |> IO.inspect()
  end
end

Main.main()
error
elixir ash_postgres.exs

17:11:16.391 [info] == Running 0 Migration0.change/0 forward

17:11:16.393 [info] create table profiles

17:11:16.395 [info] QUERY OK db=2.0ms
CREATE TABLE "profiles" ("id" bigserial, "name" varchar(255), "identities" jsonb, "inserted_at" timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, "updated_at" timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, PRIMARY KEY ("id")) []

17:11:16.396 [info] == Migrated 0 in 0.0s

17:11:16.429 [debug] QUERY OK db=0.6ms idle=57.8ms
begin []

17:11:16.442 [debug] QUERY OK source="profiles" db=5.9ms
INSERT INTO "profiles" ("name","identities","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "updated_at","inserted_at","identities","name","id" ["Joe Armstrong", [], ~U[2025-09-09 00:11:16.426384Z], ~U[2025-09-09 00:11:16.426384Z]]

17:11:16.445 [debug] QUERY OK db=0.8ms
commit []

17:11:16.501 [debug] QUERY ERROR source="profiles" db=0.0ms queue=3.0ms idle=127.1ms
UPDATE "profiles" AS p0 SET "updated_at" = s1."__new_updated_at", "identities" = s1."__new_identities" FROM (SELECT sp0."id" AS "id", (CASE WHEN $1::jsonb[] != sp0."identities"::jsonb[] THEN $2::timestamp ELSE sp0."updated_at"::timestamp END)::timestamp AS "__new_updated_at", $3::jsonb[] AS "__new_identities" FROM "profiles" AS sp0 WHERE (sp0."id"::bigint = $4::bigint) LIMIT $5) AS s1 WHERE (p0."id" = s1."id") RETURNING p0."id", p0."name", p0."identities", p0."inserted_at", p0."updated_at" [[%Accounts.Profile.Identity{first_name: "Foo", last_name: "Bar"}], ~U[2025-09-09 00:11:16.495357Z], [%Accounts.Profile.Identity{first_name: "Foo", last_name: "Bar"}], 1, 1]
** (Ash.Error.Unknown)
Bread Crumbs:
  > Returned from bulk query update: Accounts.Profile.update

Unknown Error

* ** (Postgrex.Error) ERROR 42846 (cannot_coerce) cannot cast type jsonb to jsonb[]

    query: UPDATE "profiles" AS p0 SET "updated_at" = s1."__new_updated_at", "identities" = s1."__new_identities" FROM (SELECT sp0."id" AS "id", (CASE WHEN $1::jsonb[] != sp0."identities"::jsonb[] THEN $2::timestamp ELSE sp0."updated_at"::timestamp END)::timestamp AS "__new_updated_at", $3::jsonb[] AS "__new_identities" FROM "profiles" AS sp0 WHERE (sp0."id"::bigint = $4::bigint) LIMIT $5) AS s1 WHERE (p0."id" = s1."id") RETURNING p0."id", p0."name", p0."identities", p0."inserted_at", p0."updated_at"
  (ecto_sql 3.13.2) lib/ecto/adapters/sql.ex:1098: Ecto.Adapters.SQL.raise_sql_call_error/1
  (ecto_sql 3.13.2) lib/ecto/adapters/sql.ex:996: Ecto.Adapters.SQL.execute/6
  (ecto 3.13.2) lib/ecto/repo/queryable.ex:241: Ecto.Repo.Queryable.execute/4
  (ash_postgres 2.6.17) lib/data_layer.ex:1577: AshPostgres.DataLayer.update_query/4
  (ash 3.5.39) lib/ash/actions/update/bulk.ex:608: Ash.Actions.Update.Bulk.do_atomic_update/5
  (ash 3.5.39) lib/ash/actions/update/bulk.ex:293: Ash.Actions.Update.Bulk.run/6
  (ash 3.5.39) lib/ash.ex:3320: Ash.bulk_update!/4
  /Users/amartsinovich/Library/Caches/mix/installs/elixir-1.18.4-erts-15.2.6/617f01c5d311df92f70ed4380fa03884/deps/ash/lib/ash/code_interface.ex:1215: Accounts.update_profile!/3
    (ash 3.5.39) lib/ash.ex:3340: Ash.bulk_update!/4
    /Users/amartsinovich/Library/Caches/mix/installs/elixir-1.18.4-erts-15.2.6/617f01c5d311df92f70ed4380fa03884/deps/ash/lib/ash/code_interface.ex:1215: Accounts.update_profile!/3
    ash_postgres.exs:99: Main.main/0
    ash_postgres.exs:104: (file)

Expected Behavior

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions