attr_json Documentation

repository·master·Indexed 20 days ago

https://github.com/jrochkind/attr_json

AttrJson allows ActiveRecord attributes to be backed by a single JSON or JSONB database column. It provides type-casting, nested models via AttrJson::Model, and PostgreSQL containment querying through AttrJson::Record::QueryScopes. It supports Rails 6.0.x through 7.0.x and Ruby 2.7+, offering features like round-trip serialization, validation for nested structures, and integration with Rails forms via AttrJson::NestedAttributes.

Tokens
8.7K
Snippets
24
Records
32
Agent score
70%

What's inside attr_json

  1. When to use AttrJson

    master

    AttrJson is designed for scenarios where you want to access complex, structured data in an object-oriented fashion without creating a highly normalized RDBMS schema.

    Use cases include:

    • Complex Data Structures: When you need to avoid complicated normalized schemas but still want to interact with data as objects.
    • Single-Table Inheritance (STI): When subclasses have non-shared data fields that would otherwise require many sparse columns in a single table.
    • Content Management Systems (CMS): When you need varied, plugin-driven, or configuration-dependent structured data that doesn't require heavy relational querying.
    • Model Versioning: When you want to minimize associations by inlining complex data into a single table row to simplify versioning.
    • Hybrid Document Store: When you want to treat PostgreSQL as a simple object-oriented document store while still maintaining standard RDBMS features like foreign keys and associations on the same row.

    When to avoid AttrJson:

    • If you require sophisticated querying, reporting, or high performance on complex data, a traditional normalized RDBMS schema is preferred over jsonb.
  2. Important: Use Optimistic Locking with AttrJson

    master

    When saving a record that contains attr_json attributes, the library overwrites the entire JSON structure in the relevant database column for that row. Unlike standard ActiveRecord attributes, which only update changed fields, attr_json updates the whole blob.

    To prevent race conditions where one process overwrites updates made by another process, you should implement ActiveRecord Optimistic Locking.

  3. How nested models and structured data work

    master

    You can represent complex, object-oriented data structures within a single jsonb column by using AttrJson::Model. This mix-in allows you to define models that can be nested (singly or as arrays) to any depth. These models can be used as types for an AttrJson::Record via the .to_type method.

    Key features:

    • Round-trip serialization: Complex graphs of models are serialized to JSON and reconstructed as model instances upon retrieval.
    • Nested updates: You can edit nested model attributes "in place" (e.g., m.nested_model.attr = value) and they will be properly saved.
    • Validation: Validations defined within an AttrJson::Model work and errors are posted up to the parent AttrJson::Record.
    • Form Support: For Rails forms, use include AttrJson::NestedAttributes and attr_json_accepts_nested_attributes_for to enable behavior similar to Rails' accepts_nested_attributes_for.
    class LangAndValue
      include AttrJson::Model
    
      attr_json :lang, :string, default: "en"
      attr_json :value, :string
    
      validates :lang, inclusion: %w[en es fr]
    end
    
    class MyModel < ActiveRecord::Base
      include AttrJson::Record
      include AttrJson::Record::QueryScopes
    
      # Single nested model
      attr_json :lang_and_value, LangAndValue.to_type
    
      # Array of nested models
      attr_json :lang_and_value_array, LangAndValue.to_type, array: true
    end
    
    # Usage
    m = MyModel.new(lang_and_value: { lang: 'fr', value: "S'il vous plaît" })
    m.save!
    
    # In-place update
    m.lang_and_value.lang = "de"
    m.save!
  4. Install and set up AttrJson for ActiveRecord

    master

    AttrJson allows you to use a database JSON/JSONB column as a typed object store within your ActiveRecord models.

    To use it with PostgreSQL, create a migration that adds a jsonb column. By default, AttrJson expects the column to be named json_attributes. If you plan to perform containment queries, you should add a GIN index to that column.

    Supported environments: Rails 6.0.x through 7.0.x and Ruby 2.7+.

    # migration, default column used is `json_attributes, but this can be changed
    class CreateMyModels < ActiveRecord::Migration[5.0]
      def change
        create_table :my_models do |t|
          t.jsonb :json_attributes
        end
    
        # If you plan to do any querying with jsonb_contains below..
        add_index :my_models, :json_attributes, using: :gin
      end
    end
  5. Work with arrays of simple attributes in forms

    master

    Handling an array of primitive types (like strings) in Rails forms is non-standard. To ensure values are submitted and updated correctly, use the multiple: true option on text fields.

    Handling Empty Arrays: If JavaScript removes all elements from an array, Rails may not send any parameters for that attribute, preventing it from being cleared. To fix this, use a hidden field with the _attributes suffix. If you use attr_json_accepts_nested_attributes_for on the attribute, it will automatically filter out empty strings sent by the hidden field.

    Recommendation: If the logic becomes too complex, consider wrapping your primitives in an AttrJson::Model with a single attribute to use standard nested attribute patterns.

    # Model
    attr_json :string_array, :string, array: true
    <%# Using a hidden field to ensure empty arrays are handled correctly %>
    <%= f.hidden_field "string_array_attributes[]", "" %>
    <%= f.input :string_array_attributes do %>
        <% f.object.string_array.each do |str| %>
            <%= f.text_field(:string_array_attributes, value: str, class: "form-control", multiple: true) %>
        <% end %>
    <% end %>
  6. Serialize an entire AttrJson::Model to a single JSON column

    master

    Instead of combining multiple attributes into one column, you can map a single AttrJson::Model class to an entire JSON column using ActiveRecord's serialize feature. This is useful when a model represents a standalone complex object.

    Rails 7.1+ Syntax: Use the coder: keyword in the serialize method.

    class MyModel
      include AttrJson::Model
    
      attr_json :some_string, :string
      attr_json :some_int, :int
    end
    
    class MyTable < ApplicationRecord
      # For Rails 7.1+
      serialize :some_json_column, coder: MyModel.to_serialization_coder
    
      # For older Rails versions
      # serialize :some_json_column, MyModel.to_serialization_coder
    end
    
    # Usage
    MyTable.create(some_json_column: MyModel.new(some_string: "string"))
    MyTable.create(some_json_column: { some_int: 12 }) # Automatically cast from hash
  7. Use simple attributes with standard Rails form builders

    master

    Simple attr_json attributes (non-nested) can be used with standard Rails form builders exactly like ordinary ActiveRecord attributes. This includes support for Rails' multi-parameter handling for date/datetime fields.

    Note: You must handle strong parameters for these attributes in your controller just as you would for any other attribute.

    # Model definition
    attr_json :some_string, :string
    attr_json :some_datetime, :datetime
    <%# Form template %>
    <%= f.text_field :some_string %>
    <%= f.datetime_field :some_datetime %>
  8. Integrate AttrJson with Cocoon

    master

    To use Cocoon for JS-powered add/remove functionality with nested AttrJson::Model attributes, you must include the AttrJson::Model::CocoonCompat module in your AttrJson::Model classes. This provides the ActiveRecord-style methods Cocoon expects.

    class Event
      include AttrJson::Model
      include AttrJson::Model::CocoonCompat # Required for Cocoon support
    
      attr_json :name
    end
  9. Handle nested AttrJson::Model attributes in forms

    master

    To treat nested or compound AttrJson::Model objects as if they were Rails associations in forms, you must follow these steps:

    1. Include AttrJson::NestedAttributes in your main AttrJson::Record model.
    2. Include AttrJson::Model in your nested model classes.
    3. Use attr_json_accepts_nested_attributes_for instead of the standard Rails accepts_nested_attributes_for.

    attr_json_accepts_nested_attributes_for always includes allow_destroy. It is recommended to use the reject_if: :all_blank option to ignore empty hashes.

    Build Methods: Including AttrJson::NestedAttributes automatically adds Rails-style build_ methods (e.g., build_one_event) for your attributes. You can disable this by passing define_build_method: false to attr_json_accepts_nested_attributes_for.

    Strong Params: You must handle strong parameters for nested attributes manually, following the same pattern used for standard Rails associations.

    class Event
      include AttrJson::Model
      attr_json :name
      attr_json :datetime
    end
    
    class MyRecord < ActiveRecord::Base
      include AttrJson::Record
      include AttrJson::NestedAttributes
    
      attr_json :one_event, Event.to_type
      attr_json :many_events, Event.to_type, array: true
    
      attr_json_accepts_nested_attributes_for :one_event, :many_events, reject_if: :all_blank
    end
    <%# In a form template %>
    <%= form_for(record) do |f|
      <%= f.fields_for :one_event do |one_event_f|
        <%= one_event_f.text_field :name %>
        <%= one_event_f.datetime_field :datetime %>
      <% end %>
    
      <%= f.fields_for :many_events do |many_events_f|
        <%= many_events_f.text_field :name %>
        <%= many_events_f.datetime_field :datetime %>
      <% end %>
    <% end %>
  10. Set default values for AttrJson::Model types

    master

    When defining an attr_json attribute that uses an AttrJson::Model type, you can specify a default value.

    To avoid issues with shared global state (similar to Ruby Hash defaults), it is recommended to use a proc for the default value. However, you can also provide a plain Hash, which will be automatically cast to the model type.

    # Recommended: Use a proc to ensure a new instance is created for every record
    attr_json :lang_and_value, LangAndValue.to_type, default: -> { LangAndValue.new(lang: "en", value: "default") }
    
    # Alternative: Use a Hash (will be cast to the model)
    attr_json :lang_and_value, LangAndValue.to_type, default: { lang: "en", value: "default" }
  11. How AttrJson::Model::Type handles data casting and serialization

    master

    The AttrJson::Type::Model class is an ActiveModel::Type::Value implementation designed to map JSON data to nested AttrJson::Model objects. It manages the lifecycle of data between a database-friendly serializable hash and a rich Ruby model object.

    Casting Behavior

    When calling cast(v), the type attempts to convert the input into an instance of the configured AttrJson::Model class:

    • If v is already an instance of the model, it is returned as-is.
    • If v responds to to_hash or to_h, it initializes a new model instance using that hash.
    • If the model's configuration bad_cast is set to :as_nil, it returns nil for non-castable values. Otherwise, it raises an AttrJson::Type::Model::BadCast error.

    Serialization and Deserialization

    • serialize(v): Converts a model instance (or a castable value) into a serializable hash using v.serializable_hash(strip_nils: strip_nils). This is used when saving data to the database.
    • deserialize(v): Converts a serializable hash (from the database) back into a model instance using model.new_from_serializable(v.to_hash).

    Nil Handling during Serialization

    The strip_nils option controls how nil values are treated when converting the model to a hash. This value is passed to AttrJson::Model#serialized_hash.

  12. How nested attributes are assigned to primitive arrays

    master

    When an attr_json attribute is defined as an array of primitive types (e.g., string, integer, boolean) rather than nested models, AttrJson provides specialized assignment logic.

    This logic automatically filters out blank? values (including empty strings and nil) from the input array before assignment. This is particularly useful when using forms with JavaScript (like Cocoon) where empty fields might be submitted as empty strings, but you want the underlying JSON store to only contain actual values.

    Note: This automatic filtering ignores any reject_if configuration provided, as the primary purpose is to ensure clean primitive arrays.