TypedEAV
Add runtime-defined custom fields to Active Record models, with typed values, validation, and SQL filtering, sorting, and summaries.
Scalar values use native PostgreSQL columns such as integer_value and
date_value; collection and JSON fields use JSONB. Field definitions control
casting and validation, and can be shared globally or scoped to tenants.
TypedEAV is useful when users define fields at runtime and your application needs to query them through a consistent typed interface. JSONB also works well for many workloads; choose based on your query patterns and operational needs. See Storage and performance for the tradeoffs, indexing guidance, and benchmark evidence.
Compatibility
The canonical support contract lives in
.github/compatibility.json. Typed EAV supports:
| Runtime | Supported versions |
|---|---|
| Ruby | 3.3 through 4.0 (>= 3.3, < 4.1) |
| Rails | 7.2 through 8.1 (>= 7.2, < 8.2) |
| PostgreSQL | 15 through 18 |
CI proves representative floor, middle, and ceiling combinations rather than every Cartesian product. Versions outside these ranges and prerelease versions are outside the support guarantee. PostgreSQL compatibility claims assume the current minor release for each supported major version.
Installation
Add to your Gemfile:
gem "typed_eav"
Run the install migration:
bin/rails typed_eav:install:migrations
bin/rails db:migrate
PostgreSQL is required; MySQL and SQLite are not supported. For existing installations, see Upgrading and the Changelog.
Quick Start
Assuming your application already has a Contact model and table:
class Contact < ApplicationRecord
has_typed_eav
end
TypedEAV::Field::Integer.create!(
name: "age",
entity_type: Contact.polymorphic_name,
options: { min: 0, max: 150 }
)
contact = Contact.new
contact.set_typed_eav_value("age", "40")
contact.save! # Supply any other attributes your Contact model requires.
contact.typed_eav_value("age") # => 40 (Integer)
contact.typed_eav_hash # => { "age" => 40 }
Contact.with_field("age", :gteq, 21)
.order_typed_eav("age", direction: :desc)
.limit(25)
Fields cast and validate both assigned values and query operands. Queries
return Active Record relations, so you can combine them with ordinary host
filters. Use Contact.polymorphic_name when creating definitions to respect
Rails' STI and namespaced-polymorphism settings.
See Reading, writing, and forms for bulk assignment, nested attributes, form helpers, and the admin scaffold, or Querying typed fields for operators, multi-field filters, sorting, distinct values, counts, and numeric aggregates.
Field Types
| Type | Column | Ruby Type | Options | |
|---|---|---|---|---|
Text |
string_value |
String | min_length, max_length, pattern |
|
LongText |
text_value |
String | min_length, max_length |
|
Integer |
integer_value |
Integer | min, max |
|
Decimal |
decimal_value |
BigDecimal | min, max, precision_scale |
|
Boolean |
boolean_value |
Boolean | ||
Date |
date_value |
Date | min_date, max_date |
|
DateTime |
datetime_value |
Time | min_datetime, max_datetime |
|
Select |
string_value |
String | options via TypedEAV::Option |
|
MultiSelect |
json_value |
Array | options via TypedEAV::Option |
|
IntegerArray |
json_value |
Array | min_size, max_size, min, max |
|
DecimalArray |
json_value |
Array | min_size, max_size |
|
TextArray |
json_value |
Array | min_size, max_size |
|
DateArray |
json_value |
Array | min_size, max_size |
|
Email |
string_value |
String | auto-downcases, strips whitespace | |
Url |
string_value |
String | strips whitespace | |
Color |
string_value |
String | hex color values | |
Json |
json_value |
Hash/Array | arbitrary JSON | |
Currency |
decimal_value + string_value |
{amount: BigDecimal, currency: String} |
default_currency, allowed_currencies |
|
Percentage |
decimal_value |
BigDecimal (0..1 range) | decimal_places, `display_as: :fraction \ |
:percent` |
Image |
string_value (signed_id) + :attachment has_one_attached |
String (Active Storage signed_id) | allowed_content_types, max_size_bytes |
|
File |
string_value (signed_id) + :attachment has_one_attached |
String (Active Storage signed_id) | allowed_content_types, max_size_bytes |
|
Reference |
integer_value (FK) |
Integer (target record ID) | target_entity_type, target_scope |
See Field types and validation for options, casting rules, attachments, sections, and custom single- or multi-column field types.
Essential Behavior
- Scoping selects definitions, not host records. Apply tenant and authorization filters to the host relation. Scoped models require scope by default; global definitions can be overridden by more specific partitions. See Multi-tenant scoping.
- Missing rows and explicit NULLs differ. Filtering, sorting, summaries, and reads have documented rules for each. See Queries and Bulk reads.
- The admin scaffold requires authorization. Its generated controller
returns
404until you implementauthorize_typed_eav_admin!directly on that controller. See Admin scaffold. - Versioning is opt-in. Audit rows share the value transaction; public change hooks run after commit. In-memory dirty tracking is separate from audit history. See Events and versioning and Typed-value changes.
- Bulk write APIs have different guarantees. The regular writer saves hosts with callbacks and validations. Fast upsert requires explicit acknowledgment that host saves, persistence callbacks, and versioning are skipped. See Bulk operations.
Documentation
Start at the documentation home, or browse the public API and configuration reference.
| Guide | Covers |
|---|---|
| Reading, writing, and forms | Assignment by name or ID, nested forms, admin scaffold, pending and saved changes |
| Querying typed fields | Operators, filter composition, sorting, distinct values, counts, aggregates |
| Multi-tenant scoping | Resolvers, two-level partitions, precedence, background jobs, administrative queries |
| Field types and validation | Built-in types, options, sections, attachments, custom types, validation contracts |
| Bulk operations | Selective and preloaded reads, semantic writes, fast upsert, transactions, default backfills |
| Events and versioning | Callbacks, context, actor resolution, audit history, reverting |
| CSV mapping | Header/index mapping, typed casting, row errors, saving mapped values |
| Database schema and portability | Tables, PostgreSQL requirements, read-only schema previews |
| Storage and performance | JSONB tradeoffs, optional indexes and statistics, query strategy, evidence limits |
Development and Upgrades
- Architecture and design decisions
- Development and test isolation
- Benchmark guide
- Upgrading and Changelog
- Release process
License
MIT