{"id":13879472,"url":"https://github.com/jejacks0n/schemattr","last_synced_at":"2026-03-10T07:01:33.368Z","repository":{"id":28675932,"uuid":"32195722","full_name":"jejacks0n/schemattr","owner":"jejacks0n","description":"Schemattr: Simple schema-less column definitions for ActiveRecord that can change easily over time.","archived":false,"fork":false,"pushed_at":"2023-07-23T15:36:04.000Z","size":65,"stargazers_count":8,"open_issues_count":2,"forks_count":7,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-10-17T08:58:37.795Z","etag":null,"topics":["activerecord","postgres","rails","ruby","schema"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jejacks0n.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2015-03-14T04:24:38.000Z","updated_at":"2023-07-22T22:37:38.000Z","dependencies_parsed_at":"2024-01-13T20:57:17.022Z","dependency_job_id":"6912923e-1b8f-453e-83f2-5bd9dc46cb1c","html_url":"https://github.com/jejacks0n/schemattr","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/jejacks0n/schemattr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jejacks0n%2Fschemattr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jejacks0n%2Fschemattr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jejacks0n%2Fschemattr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jejacks0n%2Fschemattr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jejacks0n","download_url":"https://codeload.github.com/jejacks0n/schemattr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jejacks0n%2Fschemattr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30326893,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T05:25:20.737Z","status":"ssl_error","status_checked_at":"2026-03-10T05:25:17.430Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","postgres","rails","ruby","schema"],"created_at":"2024-08-06T08:02:22.145Z","updated_at":"2026-03-10T07:01:33.321Z","avatar_url":"https://github.com/jejacks0n.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Schemattr - Schema-ish ActiveRecord attributes \n\n\u003cimg alt=\"Shemattr\" width=\"150\" height=\"144\" src=\"https://user-images.githubusercontent.com/13765/255353418-61285561-09cf-4208-8dd5-c00455e4288a.png\" align=\"right\" hspace=\"20\"\u003e\n\n[![Gem Version](https://img.shields.io/gem/v/schemattr.svg?label=gem%20version)](https://rubygems.org/gems/schemattr)\n[![MIT License](https://img.shields.io/github/license/jejacks0n/schemattr.svg)](https://opensource.org/licenses/MIT)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/jejacks0n/schemattr/ci.yml.svg?label=build%20status)](https://github.com/jejacks0n/schemattr/actions/workflows/ci.yml)\n[![Maintainability](https://img.shields.io/codeclimate/coverage-letter/jejacks0n/schemattr.svg?label=maintainability)](https://codeclimate.com/github/jejacks0n/schemattr/maintainability)\n[![Test Coverage](https://img.shields.io/codeclimate/coverage/jejacks0n/schemattr.svg?label=test%20coverage)](https://codeclimate.com/github/jejacks0n/schemattr/test_coverage)\n[![RubyGems](https://img.shields.io/gem/dt/schemattr.svg?label=rubygems%20downloads)](https://rubygems.org/gems/schemattr)\n\nSchemattr is an ActiveRecord extension that provides a helpful schema-less attribute DSL. It can be used to define a\nsimple schema for a single attribute that can change over time without having to migrate existing data.\n\n### Background\n\nLet's say you have a User model, and that model has a simple concept of settings -- just one for now. It's a boolean\nnamed `opted_in`, and it means that the user is opted in to receive email updates. Sweet, we go add a migration for this\nsetting and migrate. Ship it, we're done with that feature.\n\nOk, so now it's a year later and your project has grown a lot. You have over 4MM users, and in that year there's been a\nlot of business requirements that necessitated new settings for users. Each setting has been added ad hoc, as needed --\nthere's now three email lists, and users can opt in and out of each one independently.\n\nThis is where Schemattr comes in. Adding a new setting, or changing the name of an existing setting is non-trivial at\nthis point of your projects life-cycle, and requires a multi-step migration. You'll need to add the column (don't set a\ndefault for that column, because that locks the table!), then you'll need to update each record in batches, once\ncomplete you'll set a default, and finally you'll want to add a null constraint. This can become a hassle, and\nintroduces complexity to your deployments.\n\nSchemattr allows you to move all of those settings into a single JSON (or similarly serialized) column. It can behave as\nthough the column is defined on the record itself through delegation, allows providing overrides for getter/setter\nmethods, can keep a real column synced with one if its fields, and more.\n\nIf you're using Schemattr and want to add a new setting field, it's as simple as adding a new field to the attribute\nschema and setting a default right there in the code. No migrations, no hassles, easy deployment.\n\n## Download and Installation\n\nAdd this line to your Gemfile:\n\n```ruby\ngem \"schemattr\"\n```\n\nOr install the latest version with RubyGems:\n\n```bash\ngem install schemattr\n```\n\nSource code can be downloaded as part of the project on GitHub:\n\n* https://github.com/jejacks0n/schemattr\n\n## Usage\n\nIn the examples we assume there's already a User model and table.\n\nFirst, let's create a migration to add your schema-less attribute. In postgres you can use a JSON column. We use the\npostgres JSON type as our example because the JSON type allows queries and indexing, and hstore does annoying things to\nbooleans. We don't need to set our default value to an empty object because Schemattr handles that for us.\n\n_Note_: If you're using a different database provider, like sqlite3 for instance, you can use a text column and tell\nActiveRecord to serialize that column (e.g. `serialize :settings` in your model). Though, you won't be able to easily\nquery in these cases so consider your options.\n\n```ruby\nclass AddSettingsToUsers \u003c ActiveRecord::Migration\n  def change\n    add_column :users, :settings, :json\n  end\nend\n```\n\nSchemattr hooks into ActiveRecord and provides the `attribute_schema` method on any model that inherits from\nActiveRecord. This method provides a simple DSL that allows you to define the schema for the attribute. You can define\nvarious fields, specify their types, defaults if needed, and additional options.\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  attribute_schema :settings do\n    boolean :opted_in, default: true\n    boolean :email_list_advanced, default: false\n    boolean :email_list_expert, default: false\n  end\nend\n```\n\nNotice that we've done nothing else, but we already have a working version of what we want. It's shippable.\n\n```\nuser = User.new\nuser.settings.opted_in? # =\u003e true\nuser.settings.email_group_advanced? # =\u003e false\nuser.settings.email_group_expert? # =\u003e false\n```\n\nIf we save the user at this point, these settings will be persisted. We can also make changes to them at this point, and\nwhen they're persisted they'll include whatever we've changed them to be. If we don't save the user, that's ok too --\nthey'll just be the defaults if we ever ask again.\n\n### Field types\n\nThe various field types are outlined below. When you define a string field for instance, the value will be coerced into\na string at the time that it's set.\n\n| type     | description                               |\n|----------|-------------------------------------------|\n| boolean  | boolean value                             |\n| string   | string value                              |\n| text     | same as string type                       |\n| integer  | number value                              |\n| bigint   | same as integer                           |\n| float    | floating point number value               |\n| decimal  | same as float                             |\n| datetime | datetime object                           |\n| time     | time object (stored the same as datetime) |\n| date     | date object                               |\n| hash     | hash object                               |\n\nYou can additionally define your own types using `field :foo, :custom_type` and there will no coercion at the time the\nfield is set -- this is intended for when you need something that doesn't care what type it is. This generally makes it\nharder to use in forms however.\n\n### Delegating\n\nIf you don't like the idea of having to access these attributes at `user.settings` you can specify that you'd like them\ndelegated. This adds delegation of the methods that exist on settings to the User instances.\n\n```ruby\nattribute_schema :settings, delegated: true do\n  field :opted_in, :boolean, default: true\nend\n```\n\n```ruby\nuser = User.new\nuser.opted_in = false\nuser.settings.opted_in? # =\u003e false\nuser.opted_in? # =\u003e false\n```\n\n### Strict mode vs. arbitrary fields\n\nBy default, Schemattr doesn't allow arbitrary fields to be added, but it supports it. When strict mode is disabled, it\nallows any arbitrary field to be set or asked for.\n\n_Note_: When delegated and strict mode is disabled, you cannot set arbitrary fields on the model directly and must\naccess them through the attribute that you've defined -- in our case, it's `settings`.\n\n```ruby\nattribute_schema :settings, delegated: true, strict: false do\n  field :opted_in, :boolean, default: true\nend\n```\n\n```ruby\nuser = User.new\nuser.settings.foo # =\u003e nil\nuser.settings.foo = \"bar\"\nuser.settings.foo # =\u003e \"bar\"\nuser.foo # =\u003e NoMethodError\n```\n\n### Overriding\n\nSchemattr provides the ability to specify your own attribute class. By doing so you can provide your own getters and\nsetters and do more complex logic. In this example we're providing the inverse of `opted_in` with an `opted_out` psuedo\nfield.\n\n```ruby\nclass UserSettings \u003c Schemattr::Attribute\n  def opted_out\n    !self[:opted_in]\n  end\n  alias_method :opted_out, :opted_out?\n\n  def opted_out=(val)\n    self.opted_in = !val\n  end\nend\n```\n\n```ruby\nattribute_schema :settings, class: UserSettings do\n  field :opted_in, :boolean, default: true\nend\n```\n\n```ruby\nuser = User.new\nuser.settings.opted_out? # =\u003e false\nuser.settings.opted_in? # =\u003e true\nuser.settings.opted_out = true\nuser.settings.opted_in? # =\u003e false\n```\n\nOur custom `opted_out` psuedo field won't be persisted, because it's not a defined field and is just an accessor for an\nexisting field that is persisted (`opted_in`).\n\n#### Getters and setters\n\nWhen overriding the attribute class with your own, you can provide your own custom getters and setters as well. These\nwill not be overridden by whatever Schemattr thinks they should do. Take this example, where when someone turns on or\noff a setting we want to subscribe/unsubscribe them to an email list via a third party.\n\n```ruby\nclass UserSettings \u003c Schemattr::Attribute\n  def opted_in=(val)\n    if val\n      SubscribeEmail.perform_async(model.email)\n    else\n      UnsubscribeEmail.perform_async(model.email)\n    end\n    # there is no super, so you must set it manually.\n    self[:opted_in] = val\n  end\nend\n```\n\n_Note_: This is not a real world scenario but serves our purposes of describing an example.\n\n### Renaming fields\n\nSchemattr makes it easy to rename fields as well. Let's say you've got a field named `opted_in`, as the examples have\nshown thus far. But you've added new email lists, and you think `opted_in` is too vague. Like, opted in for what?\n\nWe can create a new field that is correctly named, and specify what attribute we want to pull the value from.\n\n```ruby\nattribute_schema :settings do\n  # field :opted_in, :boolean, default: true\n  field :email_list_beginner, :boolean, value_from: :opted_in, default: true\nend\n```\n\nSpecifying the `value_from: :opted_in` option will tell Schemattr to look for the value that may have already been defined in\n`opted_in` before the rename. This allows for slow migrations, but you can also write a migration to ensure this happens\nquickly.\n\nThis previously used the keyword `from`, and this keyword is deprecated.\n\n### Syncing attributes\n\nThere's a down side to keeping some things internal to this settings attribute. You can query JSON types in postgres,\nbut it may not be optimal given your indexing strategy. Schemattr provides a mechanism to keep an attribute in sync, but\nit's important to understand it and handle it with care.\n\nLet's say we want to be able to be able to easily query users who have opted in. We can add the `opted_in` column to (or\nleave it, as the case may be) on the users table.\n\n```ruby\nattribute_schema :settings do\n  field :email_list_beginner, :boolean, default: true, sync: :opted_in\nend\n```\n\n```ruby\nuser = User.new\nuser.settings.email_list_beginner = false\nuser.read_attribute(:opted_in) # =\u003e false\nuser.save!\nUser.where(opted_in: false) # =\u003e user\n```\n\nBy adding the sync option to the field, Schemattr will try to keep that attribute in sync. There are some caveats that\ncan lead to confusion however.\n\nFirst, when you do this, it forces delegation of `user.opted_in` to `user.settings.opted_in` -- this is to make keeping\nthings in sync easier. The second issue can arise is when this attribute is set directly in the database -- which means\nusing things like `user.update_column(:opted_in, false)`, and `User.update_all(opted_in: false)` will allow things to\nget out of sync.\n\n## Querying a JSON column\n\nThis has come up a little bit, and so it's worth documenting -- though it has very little to do with Schemattr. When you\nhave a JSON column in postgres, you can query values from within that column in various ways.\n\n[The documentation](http://www.postgresql.org/docs/9.4/static/functions-json.html) can be a little hard to grok, so\nthese are the common scenarios that we've used.\n\n```\nUser.where(\"(settings-\u003e\u003e'opted_in')::boolean\") # boolean query\nUser.where(\"settings-\u003e\u003e'string_value' = ?\", \"some string\") # string query\n```\n\n## License\n\nThis project is released under the MIT license:\n\n* https://opensource.org/licenses/MIT\n\nCopyright 2023 [jejacks0n](https://github.com/jejacks0n)\n\n## Make Code Not War\n\n[![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjejacks0n%2Fschemattr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjejacks0n%2Fschemattr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjejacks0n%2Fschemattr/lists"}