Skip to content

Commit

Permalink
Make ActiveModel::Serializers::JSON#from_json compatible with `#ass…
Browse files Browse the repository at this point in the history
…ign_attributes`

Prior to this commit, models that inherit from
[ActiveModel::AttributeAssignment][] (either directly or through
including [ActiveModel::API][]) lose their ability to override the
attribute assignment utilized during calls to
[ActiveModel::Serializers::JSON#from_json][].

Incidentally, `#from_json` calls `#attributes=` (instead of
`#assign_attributes`), whereas models that inherit from
`ActiveModel::AttributeAssignment` have `#attributes=` [automatically
aliased to `#assign_attributes`][alias].

This has two unintended side effects:

1. calls to `#from_json` will never invoke `#assign_attributes`
   overrides, since they invoke `#attributes=` directly

2. overrides to `#assign_attributes` won't have any effects on
   `#attributes=`, since that alias is defined on the original
   implementation

This commit attempts to remedy that issue by attempting to call
`#assign_attributes` first before falling back to `#attributes=`.

A change-free solution would be to encourage (through documentation) a
corresponding `alias :attributes= assign_attributes` line any time
models override `assign_attributes`.

[ActiveModel::AttributeAssignment]: https://edgeapi.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html
[ActiveModel::API]: https://edgeapi.rubyonrails.org/classes/ActiveModel/API.html
[ActiveModel::Serializers::JSON#from_json]: https://edgeapi.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-from_json
[alias]: https://github.com/rails/rails/blob/be0cb4e8f9aa0b105ddd035061202a5d23491b5a/activemodel/lib/active_model/attribute_assignment.rb#L37
  • Loading branch information
seanpdoyle committed May 10, 2024
1 parent be0cb4e commit 7b1da65
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 1 deletion.
4 changes: 4 additions & 0 deletions activemodel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* Make `ActiveModel::Serializers::JSON#from_json` compatible with `#assign_attributes`

*Sean Doyle*

* Fix a bug where type casting of string to `Time` and `DateTime` doesn't
calculate minus minute value in TZ offset correctly.

Expand Down
8 changes: 7 additions & 1 deletion activemodel/lib/active_model/serializers/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,13 @@ def as_json(options = nil)
def from_json(json, include_root = include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
self.attributes = hash

if respond_to?(:assign_attributes)
assign_attributes(hash)
else
self.attributes = hash
end

self
end
end
Expand Down
20 changes: 20 additions & 0 deletions activemodel/test/cases/serializers/json_serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
require "active_support/core_ext/object/instance_variables"

class JsonSerializationTest < ActiveModel::TestCase
class CamelContact < Contact
include ActiveModel::AttributeAssignment

def assign_attributes(attributes)
super(attributes.deep_transform_keys(&:underscore))
end
end

def setup
@contact = Contact.new
@contact.name = "Konata Izumi"
Expand Down Expand Up @@ -178,6 +186,18 @@ def @contact.favorite_quote; "Constraints are liberating"; end
assert_equal result.preferences, @contact.preferences
end

test "from_json supports models that include ActiveModel::AttributeAssignment and override assign_attributes" do
serialized = @contact.as_json
serialized.deep_transform_keys! { |key| key.camelize(:lower) }
result = CamelContact.new.from_json(serialized.to_json)

assert_equal result.name, @contact.name
assert_equal result.age, @contact.age
assert_equal Time.parse(result.created_at), @contact.created_at
assert_equal result.awesome, @contact.awesome
assert_equal result.preferences, @contact.preferences
end

test "custom as_json should be honored when generating json" do
def @contact.as_json(options = nil); { name: name, created_at: created_at }; end
json = @contact.to_json
Expand Down

0 comments on commit 7b1da65

Please sign in to comment.