jsonb_accessor: typed fields on top of a Postgres jsonb column, and the scopes that query them
Adds typed jsonb backed fields to your ActiveRecord models.
At a glance
- What is it?
- jsonb_accessor is a Ruby gem that adds typed jsonb backed fields as first class citizens to ActiveRecord models, with type casting, defaults, store key renaming, prefixes and suffixes, and query scopes that let you filter on jsonb values with operators, ranges, and plain English comparisons. It is in maintenance mode and MIT licensed.
- Who is it for?
- jsonb_accessor fits a Rails team with a Postgres jsonb column that has grown into a set of real attributes, where they want ActiveRecord typing, validations, and query scopes rather than raw hash access. It is the wrong choice if your data is genuinely schemaless and better served by a document store, and it is the wrong choice for a new project, because the project is in maintenance mode with no active development of new features planned.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 123 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A jsonb column that behaves like a table of columns
jsonb_accessor adds typed jsonb backed fields as first class citizens to your ActiveRecord models. The gem is similar in spirit to HstoreAccessor, and the project explains why Postgres jsonb is the better base: the jsonb column in PostgreSQL has a few distinct advantages, mostly around nested documents and support for collections.
The mechanism is a class-level declaration. You create a model with a jsonb column, and then you declare which fields inside that column should be exposed as attributes:
class Product < ActiveRecord::Base
jsonb_accessor :data,
title: :string,
external_id: :integer,
reviewed_at: :datetime
endThe first argument is the jsonb column name, and the rest is a hash of field name to type. Any type the ActiveRecord `attribute` API supports works, and the project notes you can also implement your own type by following the example in the `attribute` documentation.
That last point is the design decision that makes this more than a convenience. Rather than implementing its own type system, the gem leans on the framework's. A `jsonb_accessor` field is an ActiveRecord attribute, which means it gets the framework's type casting, its dirty tracking, its serialization, and its interaction with the rest of the ORM, and a custom type is a custom type in the ordinary ActiveRecord sense.
The column itself is created with a normal migration:
class CreateProducts < ActiveRecord::Migration
def change
create_table :products do |t|
t.jsonb :data
end
end
endSo the shape of the data is a jsonb column, and the shape of the code is a set of columns. That mismatch is the whole point of the gem, and it is why a schema-less column often becomes a de facto table.
Defaults, arrays, and renamed store keys
The options you can pass to a field are the options of the underlying attribute API, passed in an array.
class Product < ActiveRecord::Base
jsonb_accessor :data,
title: [:string, default: "Untitled"],
previous_titles: [:string, array: true, default: []]
endThe `default` option works pretty much as you would expect in practice. If no values are set for the attributes, a hash of the specified default values is saved to the jsonb column. That phrasing matters, because it describes the mechanism rather than the effect: the defaults materialise in the column on save rather than appearing to exist in memory.
The `array: true` option gives you a collection inside the jsonb, which is one of the two advantages the project claims for jsonb over hstore, alongside nested documents.
The `store_key` option is the one that changes the data rather than the code. You can use `title` for the getters and setters while the key in the jsonb column is something else:
class Product < ActiveRecord::Base
jsonb_accessor :data, title: [:string, store_key: :t]
endproduct = Product.new(title: "Foo")
product.title #=> "Foo"
product.data #=> { "t" => "Foo" }The `prefix` and `suffix` options do the opposite. They let the accessor name differ from the key in the column, and they can also be set to `true` to use the jsonb accessor name as the prefix or suffix:
class Product < ActiveRecord::Base
jsonb_accessor :data,
title: [:string, prefix: :data],
external_id: [:integer, suffix: :attr]
endproduct = Product.new(data_title: "Foo", external_id_attr: 12314122)
product.data_title #=> "Foo"
product.external_id_attr #=> 12314122
product.data #=> { "title" => "Foo", "external_id" => 12314122 }The pair of options covers both directions, and the second example shows the difference clearly: with `prefix`, the accessor is `data_title` and the key is `title`, while with `store_key`, the accessor is `title` and the key is `t`. The distinction is worth keeping straight, because the two look similar in a declaration and produce opposite results in the stored data.
Global options apply to every field, and can be overridden
Options can also be applied to all fields at once by passing an options hash as the second parameter:
class Product < ActiveRecord::Base
jsonb_accessor :data, { prefix: true },
title: :string,
external_id: :integer,
price: :decimal
endThis applies the prefix to all fields, and you can still override it for individual fields:
class Product < ActiveRecord::Base
jsonb_accessor :data, { prefix: :product },
title: :string,
external_id: [:integer, prefix: :custom],
price: :decimal
end
product = Product.new(product_title: "Widget", custom_external_id: 123, product_price: 19.99)
product.product_title #=> "Widget"
product.custom_external_id #=> 123
product.product_price #=> 19.99
product.data #=> { "title" => "Widget", "external_id" => 123, "price" => 19.99 }Global options currently support `:prefix` and `:suffix`.
The current limitation is worth stating plainly, because it defines what you can do. The global options hash is not a general pass-through of the attribute API options, it is a two-key namespace for prefix and suffix. Anything else you want applied to every field, such as a default, you still have to write out per field.
The per-field override is a plain last-one-wins. In the example, `external_id` gets `prefix: :custom` while `title` and `price` inherit `prefix: :product`, and the stored keys are still the clean `title`, `external_id`, and `price`. The prefix and suffix affect the Ruby method names, not the column keys, so the jsonb data stays tidy no matter how you name the accessors.
Scopes that query jsonb without hand-writing SQL
The gem also adds generic scopes for querying jsonb columns, and they are the part that removes the last reason to reach for raw SQL.
Four scopes are available on all ActiveRecord::Base subclasses and do not require that you make use of the jsonb_accessor declaration: `jsonb_contains`, `jsonb_number_where`, `jsonb_time_where`, and `jsonb_where`. The generic `jsonb_where` takes store keys directly, which is what you need when the column is not declared through the accessor.
When a class does have a jsonb_accessor declaration, one custom scope is defined, named after the json column with a `_where` suffix, so a `data` column gives you `data_where`, and a `data_where_not` alongside it:
Product.all.data_where(name: "Granite Towel", price: 17)Product.all.data_where_not(name: "Plasma Fork")For number fields you can query using the operators or plain English, whichever you prefer:
Product.all.data_where(price: { <: 15 })
Product.all.data_where(price: { <=: 15 })
Product.all.data_where(price: { less_than: 15 })
Product.all.data_where(price: { less_than_or_equal_to: 15 })
Product.all.data_where(price: { >: 15 })
Product.all.data_where(price: { >=: 15 })
Product.all.data_where(price: { greater_than: 15 })
Product.all.data_where(price: { greater_than_or_equal_to: 15 })
Product.all.data_where(price: { greater_than: 15, less_than: 30 })Time related fields use `before` and `after`:
Product.all.data_where(reviewed_at: { before: Time.current.beginning_of_week, after: 4.weeks.ago })And ranges work directly, for records within a certain time, date, or number range, as shorthand for the options above:
Product.all.data_where(price: 10..20)
Product.all.data_where(price: 10...20)
Product.all.data_where(reviewed_at: Time.current..3.days.from_now)The scope is a convenient wrapper around the `jsonb_where` scope that saves you from having to convert the given keys to the store keys, which is the whole reason it exists when you have renamed keys with `store_key`.
The one thing to keep in mind is that all of this compiles down to a jsonb containment or comparison query, and jsonb queries on a large table are not free. The scopes make the queries readable, not the indexes.
Maintenance mode is the headline, and it should be your filter
The status section is the first thing after the introduction, and it is the most important paragraph in the project summary.
This gem is in maintenance mode and no active development of new features is planned. The major focus is to keep it working with new Ruby and Rails versions and fix any bugs reported. Any pull requests for feature requests or enhancements will be reviewed and merged, so contributions are encouraged.
Read that carefully, because the middle sentence and the last sentence are in tension and both are true. The project will not originate features, but it will accept them. That is a workable arrangement for a small gem, and it is also a clear signal about the risk profile of adopting it.
The release history is consistent with it. The tags are v1.4 from 2023-10-15, v1.4.1 from 2026-01-22, and v1.4.2 from 2026-03-20. Two patch releases in 2026 after a long gap in 2023, which is what a compatibility maintenance stream looks like: a Rails or Ruby release lands, something breaks, a patch fixes it.
The last push to the repository was on 2026-06-22, and the project is not archived. So the code is still being touched and the gem still installs, but the trajectory is clear, and the maintenance statement is the project telling you that in as many words.
The repository reflects the same working practices. There is an `UPGRADE_GUIDE.md` at the root, which is what a gem with behaviour changes across versions carries, and a `CHANGELOG.md`. There is a `Makefile` with two build targets, one for a standard Ruby build and one for JRuby, and the JRuby one passes `--platform java` to the gem build, which is the difference between a native and a Java platform gem. Both build inside Docker, with a version argument for the Ruby or JRuby image.
There is a `.ruby-version` file, a `Gemfile` and `Rakefile` for the usual development loop, a `.rspec` for the spec runner, a `.rubocop.yml` for the linter, and `spec/` and `db/` directories. The gem is published as `jsonb_accessor`, built from `jsonb_accessor.gemspec`.
Where jsonb_accessor is the wrong choice
The boundaries here are about data modelling rather than about the gem's quality.
First, the fundamental question is whether the jsonb column should hold attributes at all. This gem's premise is that a jsonb column is a table of columns in disguise, and if that is true for your data, the honest answer is usually a real column, or a separate table, or a document store. The gem is at its best in the specific case where the schema is genuinely fluid but a handful of fields are queried and validated constantly, and at its worst when a jsonb column has quietly become a second schema that nobody is validating.
Second, the maintenance mode is a real constraint for a library in your data access layer. It is not a dependency you can upgrade and expect new capability from, and if a future Rails release changes the attribute API in a way this gem does not adapt to, the fix depends on someone else doing it. The gem's own compatibility surface is the ActiveRecord attribute API, and that API is where the risk lives.
Third, the query performance is not addressed by any of this. The scopes make jsonb queries readable, and jsonb containment queries on large tables need appropriate indexes and can be slower than querying real columns. A dataset where the jsonb fields are filtered on every request is a dataset where a column-based schema would be faster, and the gem does not change that.
Fourth, the option surface is uneven. The `attribute` API options are passed per field, while the global options hash supports only `:prefix` and `:suffix`. If you want a consistent default or a consistent array type across a hundred fields, you are writing it out a hundred times.
Fifth, and this is a general caveat rather than a specific one, the `store_key` and prefix and suffix options make the mapping between the Ruby attribute names and the stored keys non-obvious to anyone reading the database. That is a deliberate trade, and it is the right one when the stored keys come from an external system, but it is a cost when the keys are chosen for readability and the accessor names are chosen for a different reason.
Sixth, the gem is Postgres only, because jsonb is a Postgres type. A team that wants to change database engines later is signing up for work, and one that has no Postgres jsonb column at all cannot use it.
Editorial conclusion
jsonb_accessor fits a Rails team with a Postgres jsonb column that has grown into a set of real attributes, where they want ActiveRecord typing, validations, and query scopes rather than raw hash access. It is the wrong choice if your data is genuinely schemaless and better served by a document store, and it is the wrong choice for a new project, because the project is in maintenance mode with no active development of new features planned. Check three things before adopting it: that you accept a gem that only maintains compatibility and fixes bugs, that the jsonb column is on Postgres, and that you read the upgrade guide at the root of the repository, because a gem with an UPGRADE_GUIDE.md has changed its behaviour across versions.
Frequently asked questions
What does jsonb_accessor add to an ActiveRecord model?
It adds typed jsonb backed fields as first class citizens to the model, so keys inside a jsonb column behave like ordinary attributes with type casting, defaults, and dirty tracking. The gem is similar in spirit to HstoreAccessor, and jsonb has advantages around nested documents and support for collections.
How do I install jsonb_accessor and declare the fields?
Add `gem "jsonb_accessor"` to your Gemfile and run `bundle install`. Then declare the accessor on the model, for example `jsonb_accessor :data, title: :string, external_id: :integer, reviewed_at: :datetime`, where the first argument is the jsonb column and the rest are field name and type pairs.
How do I query jsonb columns with jsonb_accessor?
Four generic scopes are available on all ActiveRecord::Base subclasses without needing a declaration: `jsonb_contains`, `jsonb_number_where`, `jsonb_time_where`, and `jsonb_where`. A class with a `jsonb_accessor :data` declaration also gets a `data_where` scope and a `data_where_not` scope, supporting operator keys such as `<:`, plain English such as `less_than:`, `before` and `after` for times, and Ruby ranges.
Is jsonb_accessor still actively developed?
The project states it is in maintenance mode with no active development of new features planned, and the focus is on keeping it working with new Ruby and Rails versions and fixing reported bugs, though pull requests for features are still reviewed and merged. The last release was v1.4.2 on 2026-03-20 and the last push was on 2026-06-22.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/madeintandem-jsonb-accessor)Was this useful?