Skip to content

4. Array and Hash and other custom types

Yaro edited this page May 15, 2025 · 2 revisions

This gem uses ActiveModel::Attributes under the hood:

The ActiveModel::Attributes module provides a powerful way to define attributes on non-ActiveRecord models, handling type casting and default values similarly to ActiveRecord.

Built-in Types

Out of the box, ActiveModel::Attributes supports the following primitive types, typically registered via symbols:

  • :string - For standard string values.
  • :text - For longer text content.
  • :integer - For whole numbers.
  • :float - For floating-point numbers.
  • :decimal - For high-precision decimal numbers (suitable for currency).
  • :boolean - For true/false values (casts common representations like 1, '1', 't', 'true', 0, '0', 'f', 'false').
  • :date - For date values (without time).
  • :time - For time values (without date).
  • :datetime - For timestamp values including both date and time.
  • :binary - For raw binary data.

Custom Types (Example: Array and Hash)

If you need to handle attributes as specific data structures like Arrays or Hashes, or custom Value Objects, you must define and register custom types. ActiveModel doesn't include :array or :hash types by default.

Here's how you can implement custom types for Array and Hash attributes using JSON serialization:

1. Custom Type Definitions

Place the following class definitions in appropriate files, for example, app/types/array_type.rb and app/types/hash_type.rb:

app/types/array_type.rb

require 'active_model'
require 'json'

class ArrayType < ActiveModel::Type::Value
  def cast(value)
    case value
    when String
      begin
        decoded = JSON.parse(value)
        decoded.is_a?(Array) ? decoded : nil
      rescue JSON::ParserError
        nil
      end
    when Array
      value
    when nil
      nil
    else
      nil
    end
  end

  def serialize(value)
    case value
    when Array
      value.to_json
    when nil
      nil
    else
      Array(value).to_json
    end
  end

  def type
    :array
  end
end

app/types/hash_type.rb

require 'active_model'
require 'json'

class HashType < ActiveModel::Type::Value
  def cast(value)
    case value
    when String
      begin
        decoded = JSON.parse(value)
        decoded.is_a?(Hash) ? decoded : nil
      rescue JSON::ParserError
        nil
      end
    when Hash
      value
    when nil
      nil
    else
      nil
    end
  end

  def serialize(value)
    case value
    when Hash
      value.to_json
    when nil
      nil
    else
      nil
    end
  end

  def type
    :hash
  end
end

2. Registration in Initializer

Register these custom types so ActiveModel knows about them. Create or modify an initializer file, for example, config/initializers/custom_types.rb:

# Ensure the type classes are loaded if they aren't automatically
# (though Rails typically autoloads from app/types)
# require_relative '../../app/types/array_type' # Adjust path as necessary
# require_relative '../../app/types/hash_type'  # Adjust path as necessary

ActiveModel::Type.register(:array, ArrayType)
ActiveModel::Type.register(:hash, HashType)

3. Usage in Form

You can now use these custom types directly in your Form class alongside the other attributes:

class YourCustomForm
  ...
  attribute :tags, :array, default: -> { [] }
  attribute :config, :hash, default: -> { {} }
  attribute :name, :string
  ...
end
Clone this wiki locally