- Published on
How to Change a Field Type in an Ecto Embedded Schema with a Migration
- Authors
- Name
- Iván González
- @dreamingechoes
Why I’m Writing This?
The other day, I had to change the type of a field in an Ecto embedded schema from a string to a list of strings. Sounds simple, right? Well, not quite—because this field already had existing data stored in PostgreSQL as JSONB. That meant I had to not only update my Elixir codebase but also write a migration to transform the stored data correctly.
Since this is a challenge others will likely encounter, I wanted to document the process, including the reasoning behind each step. If you ever need to change a field type in an embedded schema, this guide will help you do it safely without breaking your existing data.
Understanding the Challenge
Ecto embedded schemas (Ecto.Schema docs) are different from standard schemas because they are stored inside JSONB columns rather than as separate database tables. This makes migrations trickier—there’s no direct ALTER COLUMN
statement we can run to change the field type.
In my case, the field in question was tags
, originally defined as a string:
defmodule MyApp.Schema do
use Ecto.Schema
embedded_schema do
field :tags, :string
end
end
However, I wanted tags
to be a list of strings instead of a single string. The updated schema looks like this:
defmodule MyApp.Schema do
use Ecto.Schema
embedded_schema do
field :tags, {:array, :string}
end
end
The key change here is using {:array, :string}
as the field type, which tells Ecto to expect a list of strings instead of a single string. You can read more about supported field types in the Ecto Types documentation.
But making this change alone is not enough—existing records in the database still have tags
stored as plain strings, so we need a migration to transform that data.
Writing the Migration to Update Existing Data
Since embedded schemas are stored as JSONB in PostgreSQL, we must update the values properly using JSONB functions.
First, generate the migration:
mix ecto.gen.migration update_tags_field
Then, open the generated migration file and implement the logic to transform the field.
Migration Code:
defmodule MyApp.Repo.Migrations.UpdateTagsField do
use Ecto.Migration
def up do
execute("""
UPDATE my_table
SET embedded_schema_field = jsonb_set(
embedded_schema_field,
'{tags}',
to_jsonb(string_to_array((embedded_schema_field->>'tags'), ','))
)
WHERE embedded_schema_field ? 'tags';
""")
end
def down do
execute("""
UPDATE my_table
SET embedded_schema_field = jsonb_set(
embedded_schema_field,
'{tags}',
to_jsonb(array_to_string((embedded_schema_field->'tags')::text[], ','))
)
WHERE embedded_schema_field ? 'tags';
""")
end
end
Breaking Down the SQL:
string_to_array((embedded_schema_field->>'tags'), ',')
:- Converts the existing string into an array, splitting by commas.
to_jsonb(...)
:- Ensures the updated value remains in JSONB format.
jsonb_set(...)
:- Updates only the
tags
key inside the embedded schema field.
- Updates only the
WHERE embedded_schema_field ? 'tags'
:- Ensures we only update records that contain the
tags
field.
- Ensures we only update records that contain the
For the rollback (down/0
), we reverse the process by converting the array back into a comma-separated string.
If you're unfamiliar with these PostgreSQL functions, you can check out the official PostgreSQL JSONB functions documentation for more details.
Additionally, the PostgreSQL JSONB data type is quite powerful, and you can learn more about its capabilities in the JSON Types documentation.
Running and Verifying the Migration
Once the migration is in place, apply it:
mix ecto.migrate
Then, verify the results by querying the database:
SELECT embedded_schema_field FROM my_table WHERE embedded_schema_field ? 'tags';
Check that tags
is now stored as an array rather than a plain string.
Additionally, if you have any existing Ecto queries that previously treated tags
as a string, you’ll need to update them to work with lists instead. Learn more about working with array fields in queries in the Ecto Query documentation.
Conclusion
- Embedded schemas store data as JSONB, requiring special handling when changing field types.
- Schema updates alone are not enough—we must also migrate existing data correctly.
- PostgreSQL’s
jsonb_set()
function is a powerful way to update JSONB fields safely. - This approach ensures a smooth transition without losing or corrupting data.
If you’ve run into similar challenges with Ecto and JSONB migrations, I’d love to hear how you tackled them!