From 42b802deb64dbbfe68c5102b8b47ba6e31684a1c Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Mon, 26 Jan 2026 09:47:07 -0700 Subject: [PATCH] Fix Type#deserialize to handle arrays for multiple: true attributes Previously, deserializing an array would return nil instead of the enumerated values. This bug only affected ActiveModel::Attributes usage (not ActiveRecord) and was exposed when used with gems like store_model v2.0.0+ that call deserialize during load. The fix checks if the value is an array AND the attribute is declared with multiple: true, then uses find_values(*value) instead of find_value(value). Added 10 test cases covering: - Single value deserialization (existing behavior) - Array deserialization for multiple: true attributes - Array rejection for non-multiple attributes - Empty arrays, nil handling, and invalid value filtering - Round-trip serialization for both single and multiple values --- CHANGELOG.md | 2 + lib/enumerize/activemodel.rb | 10 ++++- test/activemodel_test.rb | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4360c4ce..b588ebb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### bug fix +* Fixed `Enumerize::ActiveModelAttributesSupport::Type#deserialize` to properly handle arrays for `multiple: true` attributes. Previously, deserializing an array would return `nil` instead of the enumerated values. This bug only affected ActiveModel::Attributes usage (not ActiveRecord) and was exposed when used with gems like store_model v2.0.0+ that call `deserialize` during load. + ### enchancements * Support only Ruby 3.1+ and Rails 7.0+. (by [@nashby](https://github.com/nashby)) diff --git a/lib/enumerize/activemodel.rb b/lib/enumerize/activemodel.rb index ae007355..b57a043c 100644 --- a/lib/enumerize/activemodel.rb +++ b/lib/enumerize/activemodel.rb @@ -40,7 +40,15 @@ def serialize(value) end def deserialize(value) - @attr.find_value(value) + return nil if value.nil? + + # Use find_values for arrays on multiple: true attributes + # Otherwise use find_value for single values + if value.is_a?(Array) && @attr.arguments[:multiple] + @attr.find_values(*value) + else + @attr.find_value(value) + end end end end diff --git a/test/activemodel_test.rb b/test/activemodel_test.rb index af98af53..b2c2e52a 100644 --- a/test/activemodel_test.rb +++ b/test/activemodel_test.rb @@ -107,6 +107,82 @@ class InterestsRequiredActiveModelUser < ActiveModelUser expect(user.errors[:interests]).must_be_empty end + + describe 'Type#deserialize' do + it 'deserializes single value' do + type = model.attribute_types['sex'] + result = type.deserialize('male') + expect(result).must_be_instance_of Enumerize::Value + expect(result.to_s).must_equal 'male' + end + + it 'returns nil for nil single value' do + type = model.attribute_types['sex'] + result = type.deserialize(nil) + expect(result).must_be_nil + end + + it 'returns nil for invalid single value' do + type = model.attribute_types['sex'] + result = type.deserialize('invalid') + expect(result).must_be_nil + end + + it 'treats array as invalid for non-multiple attribute' do + type = model.attribute_types['sex'] + result = type.deserialize(['male', 'female']) + expect(result).must_be_nil + end + + it 'deserializes array of valid values for multiple attribute' do + type = model.attribute_types['interests'] + result = type.deserialize(['music', 'sports']) + expect(result).must_be_instance_of Array + expect(result.map(&:to_s)).must_equal ['music', 'sports'] + end + + it 'deserializes empty array for multiple attribute' do + type = model.attribute_types['interests'] + result = type.deserialize([]) + expect(result).must_equal [] + end + + it 'filters out invalid values from array' do + type = model.attribute_types['interests'] + result = type.deserialize(['music', 'invalid', 'sports']) + expect(result.map(&:to_s)).must_equal ['music', 'sports'] + end + + it 'returns nil for nil array value' do + type = model.attribute_types['interests'] + result = type.deserialize(nil) + expect(result).must_be_nil + end + + it 'preserves values through serialize/deserialize cycle for single value' do + type = model.attribute_types['sex'] + user = model.new(sex: 'female') + + serialized = type.serialize(user.sex) + expect(serialized).must_equal 'female' + + deserialized = type.deserialize(serialized) + expect(deserialized.to_s).must_equal 'female' + end + + it 'preserves values through serialize/deserialize cycle for multiple values' do + type = model.attribute_types['interests'] + user = model.new(interests: ['music', 'programming']) + + # Serialize the entire set + serialized = user.interests.map { |v| type.serialize(v) } + expect(serialized).must_equal ['music', 'programming'] + + # Deserialize back + deserialized = type.deserialize(serialized) + expect(deserialized.map(&:to_s)).must_equal ['music', 'programming'] + end + end end else