paper-trail-gem/paper_trail · warning

Attribute # does not exist on # (Version id: # ).

Error message

Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id}).

What it means

When reifying a version (e.g. `version.reify`), PaperTrail deserializes the `object` blob and applies each key to a fresh model instance. reify_attribute in lib/paper_trail/reifier.rb:99 first tries `model.has_attribute?(k)` (a real column), then a setter via `respond_to?(:"#{k}=")`; only when BOTH are missing does it log this message through `version.logger.warn` and silently skip the value. It is not an exception — the reified record is still returned — but it means historical data for that key is being dropped because the current schema no longer has the attribute.

Solutions

  1. If the attribute is still meaningful, add it back to the model (revert the drop, or re-add the column via a migration) so reify populates it again.
  2. If the column is gone for good, define a virtual setter on the model (`attr_accessor :old_column`) so historical values land somewhere instead of being skipped.
  3. If old data is disposable, migrate the version rows themselves: backfill/strip removed keys from the `object` YAML/JSON (or delete obsolete versions) so stored blobs match the current schema.
  4. Do nothing — the value is only logged and skipped, reify still returns the model with all remaining attributes.

Example fix

# Migration dropped :legacy_status after versions were recorded; reify logs:
# "Attribute legacy_status does not exist on Article (Version id: 42)"

# app/models/article.rb - after: virtual attribute absorbs historical values
class Article < ApplicationRecord
  has_paper_trail
  attr_accessor :legacy_status
end
Defensive patterns

Strategy: validation

Validate before calling

# Before reifying, verify the stored blob matches the current schema
klass = version.item_type.constantize
model = klass.new
missing = version.attributes["object"].to_s.keys.map(&:to_s).reject do |k|
  model.has_attribute?(k) || model.respond_to?("#{k}=")
end
Rails.logger.warn("reify will drop: #{missing.inspect} for #{klass}") unless missing.empty?

Type guard

def reify_safe?(version)
  model = version.item_type.constantize.new
  version.attributes["object"].to_s.keys.all? do |k|
    model.has_attribute?(k.to_s) || model.respond_to?("#{k}=")
  end
end

Prevention

When it happens

Trigger: Calling `PaperTrail::Version#reify` (directly or via `.previous`, `.versions.last.reify`, etc.) on a version whose serialized `object` contains a key that is neither a column of `version.item_type`'s class nor backed by a `#{k}=` setter. Classic cause: a column was dropped or renamed AFTER those versions were recorded, or the item_type's schema differs across environments.

Common situations: Running a `remove_column`/`rename_column` migration without pruning or rewriting old PaperTrail versions; restoring a deleted record (`versions.last.reify.save!`) months after a schema change and wondering why the old field is gone; multi-tenant or sharded setups where one environment's model lacks columns present when the version row was written.

Related errors


AI-assisted analysis of paper-trail-gem/paper_trail@098058ae47 (2026-08-21). Data as JSON: /api/errors/e1846062f071cb43. Report an issue: GitHub.

Appendix: source

Thrown at lib/paper_trail/reifier.rb:99

      def init_unversioned_attrs(attrs, model)
        (model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
      end

      # Reify onto `model` an attribute named `k` with value `v` from `version`.
      #
      # `ObjectAttribute#deserialize` will return the mapped enum value and in
      # Rails < 5, the []= uses the integer type caster from the column
      # definition (in general) and thus will turn a (usually) string to 0
      # instead of the correct value.
      #
      # @api private
      def reify_attribute(k, v, model, version)
        if model.has_attribute?(k)
          model[k.to_sym] = v
        elsif model.respond_to?(:"#{k}=")
          model.send(:"#{k}=", v)
        elsif version.logger
          version.logger.warn(
            "Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
          )
        end
      end

      # Reify onto `model` all the attributes of `version`.
      # @api private
      def reify_attributes(model, version, attrs)
        AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
        attrs.each do |k, v|
          reify_attribute(k, v, model, version)
        end
      end

      # Given a `version`, return the class to reify. This method supports
      # Single Table Inheritance (STI) with custom inheritance columns and
      # custom inheritance column values.
      #

View on GitHub (pinned to 098058ae47)