# Attribute serialization

**URL:** https://discourse.rom-rb.org/t/attribute-serialization/700
**Category:** Uncategorized
**Created:** [June 23, 2025, 10:18am UTC](https://discourse.rom-rb.org/t/attribute-serialization/700 "2025-06-23T10:18:51Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![pointlessone](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.rom-rb.org/pointlessone/32/322_2.png) [@pointlessone](https://discourse.rom-rb.org/u/pointlessone)
#### Post date: [June 23, 2025, 10:18am UTC](https://discourse.rom-rb.org/t/attribute-serialization/700/1 "2025-06-23T10:18:51Z")

</div>

I’m trying to figure out how to serialize attribute to store in the db.

I have Items, they can have multiple tags. On the app side I want them represented as an array of strings but in the db it’s stored in a string column.

As an example let’s use JSON.

In [Hanami docs](https://guides.hanamirb.org/v2.2/database/relations/#schema) I saw this example that I use as a basis:

```ruby
module Bookshelf
  module Relations
    class Credentials < Hanami::DB::Relation
      JWKS = Types.define(JWT::JWK::Set) do
        input { |jwks| Types::PG::JSONB[jwks.export] }
        output { |jsonb| JWT::JWK::Set.new(jsonb.to_h) }
      end

      schema infer: true do
        attribute :jwks, JWKS
      end
    end
  end
end

```

I came up with this:

```ruby
module Example
  module Relations
    class Items < Example::DB::Relation
      JSON = ROM::SQL::Types.define(Array) do
        input { |tags| ::JSON.dump(Array(tags)) }
        output { |json| Array(::JSON.parse(json)) }
      end

      schema :items, infer: true do
        attribute :tags, JSON
      end
    end
  end
end

```

Now, I encountered a few issue that I want to understand.

1. Hanami uses `books.insert` to create new records. It doesn’t use type coercion in any way. I figured out that it delegates straight to a dataset method (that is `Sequel::Dataset`) so it’s kinda understandable that it wouldn’t use any of the ROM features but it’s exposed on a ROM object so at least a warning would be nice, I guess.

.

1. I also figured out that the proper way is to use a repo command. And it sort of works. However, it seems the `output` part is called twice: once with a JSON string that comes from the DB, and the second time with already deserialized value.

I added value an call stack output to the `input` and `output` blocks of the type definition. Here’s an example from the Hanami console:

```auto
example[development]> Hanami.app["repos.items_repo"].create(tags: 'ney')
# input
[example] [INFO] [2025-06-23 12:26:50 +0300] Loaded :sqlite in 1ms INSERT INTO `items` (`tags`) VALUES ('["ney"]')
# output: "[\"ney\"]"
["~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor/function.rb:17:in 'Dry::Types::Constructor::Function::Safe#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor.rb:80:in 'Dry::Types::Constructor#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema/key.rb:44:in 'Dry::Types::Schema::Key#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:328:in 'block in Dry::Types::Schema#resolve_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:322:in 'Hash#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:322:in 'Dry::Types::Schema#resolve_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:60:in 'Dry::Types::Schema#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor.rb:80:in 'Dry::Types::Constructor#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/type.rb:47:in 'Dry::Types::Type#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/relation.rb:224:in 'block in ROM::Relation#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/dataset/actions.rb:164:in 'block in Sequel::Dataset#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:425:in 'block (2 levels) in Sequel::SQLite::Dataset#fetch_rows'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sqlite3-2.7.0-x86_64-darwin/lib/sqlite3/resultset.rb:50:in 'SQLite3::ResultSet#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:414:in 'block in Sequel::SQLite::Dataset#fetch_rows'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sqlite3-2.7.0-x86_64-darwin/lib/sqlite3/database.rb:356:in 'SQLite3::Database#query'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:259:in 'block (2 levels) in Sequel::SQLite::Database#_execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/extensions/error_sql.rb:63:in 'Sequel::ErrorSQL#log_connection_yield'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/plugins/relation/sql/instrumentation.rb:83:in 'block (2 levels) in define_log_connection_yield'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-monitor-1.0.1/lib/dry/monitor/clock.rb:15:in 'Dry::Monitor::Clock#measure'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-monitor-1.0.1/lib/dry/monitor/notifications.rb:29:in 'Dry::Monitor::Notifications#instrument'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/plugins/relation/sql/instrumentation.rb:82:in 'block in define_log_connection_yield'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:259:in 'block in Sequel::SQLite::Database#_execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/connection_pool/timed_queue.rb:90:in 'Sequel::TimedQueueConnectionPool#hold'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/database/connecting.rb:283:in 'Sequel::Database#synchronize'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:252:in 'Sequel::SQLite::Database#_execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:170:in 'Sequel::SQLite::Database#execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/dataset/actions.rb:1197:in 'Sequel::Dataset#execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/adapters/sqlite.rb:407:in 'Sequel::SQLite::Dataset#fetch_rows'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/dataset/actions.rb:164:in 'Sequel::Dataset#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/dataset/actions.rb:474:in 'Enumerable#map'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/sequel-5.93.0/lib/sequel/dataset/actions.rb:474:in 'Sequel::Dataset#map'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/relation.rb:224:in 'ROM::Relation#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/relation.rb:353:in 'Enumerator#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/relation.rb:353:in 'Enumerable#to_a'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/relation.rb:353:in 'ROM::Relation#to_a'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/create.rb:49:in 'ROM::SQL::Commands::Create#insert'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/create.rb:33:in 'ROM::SQL::Commands::Create#execute'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:280:in 'ROM::Command#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/error_wrapper.rb:18:in 'ROM::SQL::Commands::ErrorWrapper#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/commands/composite.rb:21:in 'ROM::Commands::Composite#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-repository-5.4.2/lib/rom/repository/class_interface.rb:154:in 'block in Example::Repos::ItemsRepo#define_command_method'",
 "(irb):1:in '<top (required)>'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/workspace.rb:101:in 'Kernel#eval'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/workspace.rb:101:in 'IRB::WorkSpace#evaluate'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/context.rb:631:in 'IRB::Context#evaluate_expression'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/context.rb:599:in 'IRB::Context#evaluate'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1053:in 'block (2 levels) in IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1365:in 'IRB::Irb#signal_status'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1045:in 'block in IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1124:in 'block in IRB::Irb#each_top_level_statement'",
 "<internal:kernel>:168:in 'Kernel#loop'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1121:in 'IRB::Irb#each_top_level_statement'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1044:in 'IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1025:in 'block in IRB::Irb#run'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1024:in 'Kernel#catch'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1024:in 'IRB::Irb#run'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/repl/irb.rb:34:in 'Hanami::CLI::Repl::Irb#start'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/commands/app/console.rb:48:in 'Hanami::CLI::Commands::App::Console#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/commands/app/command.rb:46:in 'Hanami::CLI::Commands::App::Command::Environment#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-cli-1.2.0/lib/dry/cli.rb:117:in 'Dry::CLI#perform_registry'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-cli-1.2.0/lib/dry/cli.rb:66:in 'Dry::CLI#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/exe/hanami:11:in '<top (required)>'",
 "~/.rbenv/versions/3.4.1/bin/hanami:25:in 'Kernel#load'",
 "~/.rbenv/versions/3.4.1/bin/hanami:25:in '<main>'"]
[example] [INFO] [2025-06-23 12:26:50 +0300] Loaded :sqlite in 3ms SELECT `items`.`tags`, `items`.`id` FROM `items` WHERE (`id` IN (10)) ORDER BY `items`.`id`
# output: ["ney"]
["~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor/function.rb:17:in 'Dry::Types::Constructor::Function::Safe#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor.rb:80:in 'Dry::Types::Constructor#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema/key.rb:44:in 'Dry::Types::Schema::Key#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:328:in 'block in Dry::Types::Schema#resolve_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:322:in 'Hash#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:322:in 'Dry::Types::Schema#resolve_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/schema.rb:60:in 'Dry::Types::Schema#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/constructor.rb:80:in 'Dry::Types::Constructor#call_unsafe'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-types-1.8.3/lib/dry/types/type.rb:47:in 'Dry::Types::Type#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/create.rb:41:in 'block in ROM::SQL::Commands::Create#finalize'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/create.rb:41:in 'Array#map'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/create.rb:41:in 'ROM::SQL::Commands::Create#finalize'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:461:in 'block in ROM::Command#apply_hooks'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:456:in 'Array#each'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:456:in 'Enumerable#reduce'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:456:in 'ROM::Command#apply_hooks'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/command.rb:291:in 'ROM::Command#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-sql-3.7.0/lib/rom/sql/commands/error_wrapper.rb:18:in 'ROM::SQL::Commands::ErrorWrapper#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-core-5.4.0/lib/rom/commands/composite.rb:21:in 'ROM::Commands::Composite#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rom-repository-5.4.2/lib/rom/repository/class_interface.rb:154:in 'block in Example::Repos::ItemsRepo#define_command_method'",
 "(irb):1:in '<top (required)>'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/workspace.rb:101:in 'Kernel#eval'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/workspace.rb:101:in 'IRB::WorkSpace#evaluate'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/context.rb:631:in 'IRB::Context#evaluate_expression'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb/context.rb:599:in 'IRB::Context#evaluate'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1053:in 'block (2 levels) in IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1365:in 'IRB::Irb#signal_status'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1045:in 'block in IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1124:in 'block in IRB::Irb#each_top_level_statement'",
 "<internal:kernel>:168:in 'Kernel#loop'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1121:in 'IRB::Irb#each_top_level_statement'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1044:in 'IRB::Irb#eval_input'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1025:in 'block in IRB::Irb#run'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1024:in 'Kernel#catch'",
 "~/.rbenv/versions/3.4.1/lib/ruby/3.4.0/irb.rb:1024:in 'IRB::Irb#run'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/repl/irb.rb:34:in 'Hanami::CLI::Repl::Irb#start'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/commands/app/console.rb:48:in 'Hanami::CLI::Commands::App::Console#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/lib/hanami/cli/commands/app/command.rb:46:in 'Hanami::CLI::Commands::App::Command::Environment#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-cli-1.2.0/lib/dry/cli.rb:117:in 'Dry::CLI#perform_registry'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/dry-cli-1.2.0/lib/dry/cli.rb:66:in 'Dry::CLI#call'",
 "~/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/hanami-cli-2.2.1/exe/hanami:11:in '<top (required)>'",
 "~/.rbenv/versions/3.4.1/bin/hanami:25:in 'Kernel#load'",
 "~/.rbenv/versions/3.4.1/bin/hanami:25:in '<main>'"]

```

I would like to understand the second point. Specifically when the type coercion happens and in which way. Is this expected and do I need to accept all possible inputs to both `input` and `output` or can I expect specific inputs to each?

---

<div class="post-metadata">

### Author: ![alassek](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.rom-rb.org/alassek/32/260_2.png) [@alassek](https://discourse.rom-rb.org/u/alassek)
#### Post date: [June 27, 2025, 2:39pm UTC](https://discourse.rom-rb.org/t/attribute-serialization/700/2 "2025-06-27T14:39:40Z")

</div>

It doesn’t work because you’re using SQLite, something about that adapter is causing the type error and I’m not certain why. The example does work with PostgreSQL.

Here’s another way you might approach it:

```ruby
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'sqlite3'
  gem 'rom-sql'
  gem 'dry-core'
  gem 'dry-auto_inject'
  gem 'pry'
end

require 'rom'
require 'rom/sql'

rom_config = ROM::Configuration.new(:sql, 'sqlite::memory')
gateway = rom_config.default_gateway

migration = gateway.migration do
  change do
    create_table :items do
      primary_key :id
      column :tags, 'text', null: false
    end
  end
end

migration.apply(gateway.connection, :up)

module Structs
  class Item < ROM::Struct
    def tags
      @tags ||= JSON.parse(attributes[:tags])
    end
  end
end

class Items < ROM::Relation[:sql]
  Tags = Types.define(Types::String) do
    input { |tags| ::JSON.dump(Array(tags)) }
    output { |json| json }
  end

  schema :items, infer: true do
    attribute :tags, Tags
  end
end

rom_config.register_relation(Items)

container = Dry::Core::Container.new
container.register("persistence.rom", ROM.container(rom_config))

Deps = Dry::AutoInject(container)

class Repository < ROM::Repository::Root
  include Deps[container: "persistence.rom"]
  def find(id) = root.by_pk(id).one
end

class ItemRepo < Repository[:items]
  struct_namespace Structs
  commands :create, update: :by_pk, delete: :by_pk
end

repo = ItemRepo.new

Pry.start(self)

```

```auto
[1] pry(main)> item = repo.create(tags: %w[foo bar])
=> #<Structs::Item tags="[\"foo\",\"bar\"]" id=1>
[2] pry(main)> item.tags
=> ["foo", "bar"]
[3] pry(main)> item = repo.update(item.id, tags: item.tags + ['baz'])
=> #<Structs::Item tags="[\"foo\",\"bar\",\"baz\"]" id=1>
[4] pry(main)> item.tags
=> ["foo", "bar", "baz"]

```

---

<div class="post-metadata">

### Author: ![pointlessone](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.rom-rb.org/pointlessone/32/322_2.png) [@pointlessone](https://discourse.rom-rb.org/u/pointlessone)
#### Post date: [June 27, 2025, 8:29pm UTC](https://discourse.rom-rb.org/t/attribute-serialization/700/3 "2025-06-27T20:29:01Z")

</div>

In the db the column is `text NOT NULL`. SQLite has native support for JSON columns but as far as I can tell ROM doesn’t use it.

Thank you for the workaround.

BTW, since you mention there’s some sort of error do you think it’s worth filing an issue?

---

<div class="post-metadata">

### Author: ![alassek](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.rom-rb.org/alassek/32/260_2.png) [@alassek](https://discourse.rom-rb.org/u/alassek)
#### Post date: [June 27, 2025, 8:52pm UTC](https://discourse.rom-rb.org/t/attribute-serialization/700/4 "2025-06-27T20:52:08Z")

</div>

> [@pointlessone](#):
>
> SQLite has native support for JSON columns but as far as I can tell ROM doesn’t use it.

SQLite does not have JSON datatypes as far as I can tell. It has built-in helper functions for working with JSON & JSONB, but the underlying column type is TEXT or BLOB. That makes it more difficult for an adapter to transparently support them as JSON, since it can’t differentiate unstructured TEXT from JSON TEXT.

However, it should be possible to override the behavior with the correct serialization rules, as you are trying to do. I would suggest opening a ticket, since this does appear to be a bug somewhere, but I can’t guarantee it will be looked at in the near term.

---

<div class="post-metadata">

### Author: ![pointlessone](https://yyz1.discourse-cdn.com/flex029/user_avatar/discourse.rom-rb.org/pointlessone/32/322_2.png) [@pointlessone](https://discourse.rom-rb.org/u/pointlessone)
#### Post date: [August 8, 2025, 1:01pm UTC](https://discourse.rom-rb.org/t/attribute-serialization/700/5 "2025-08-08T13:01:52Z")

</div>

I had a chance to look at the code a bit and here’s what I found.

It doesn’t happen in pg because pg redefines `Commands::Create#insert`

Default (and sqlite):

```ruby
def insert(tuples)
  pks = tuples.map { |tuple| relation.insert(tuple) }
  relation.where(relation.primary_key => pks).to_a
end

```

PG:

```ruby
def insert(tuples)
  dataset = tuples.flat_map do |tuple|
    returning_dataset.insert(tuple)
  end

  wrap_dataset(dataset)
end

```

In sqlite first type mapping happens in:

block{} at […]/rom-core-5.4.0/lib/rom/relation.rb:224 (in `Relation#each`)

And the second happens in `finalize` (called as the after hook).

As far as I can tell, in sqlite/default version of `insert` returns already deserialized columns. `to_a` does that (actually, `Relation#each`).

In pg this is not the case. `insert` returns values from the db and `finalize` does the actual deserialization.

I’m not sure what to do with these findings though. Is this useful? Do I file an issue on gh?
