Skip to content

Commit 54a56e4

Browse files
chore: fix error serialization for string errors
When adding custom validation errors, Rails allows both Symbols and Strings: ``` class Foo < ActiveRecord::Base validate :add_symbol_error validate :add_string_error def add_symbol_error errors.add(:title, :too_short) end def add_string_error errors.add(:title, 'is too short') end end ``` The Symbol error will later be translated by Rails using the i18n logic, when calling `foo.errors.full_messages` or similar methods. The String error will stay as it is. see https://api.rubyonrails.org/v7.0.8.7/classes/ActiveModel/Errors.html#method-i-add This commit introduces support for these String errors in the error serialization class. Before this change, the error response for the validation that was added in the specs looked like this: ```ruby [ { "status"=>"422", "source"=>{"pointer"=>"/data/attributes/title"}, "title"=>"Unprocessable Entity", "detail"=> "Title translation missing: en.activerecord.errors.models.note.attributes.title.is not so nice", "code"=>"is_not_so_nice" } ] ``` Please note the "translation missing" part in the `detail`, and the `code` that is a parameterized version of the String error. After this change, the error response looks like this: ```ruby [ { "status"=>"422", "source"=>{"pointer"=>"/data/attributes/title"}, "title"=>"Unprocessable Entity", "detail"=>"Title is not so nice", "code"=>"invalid" } ] ``` The `detail` is now the String error as passed in by Rails, and the `code` is a generic `"invalid"` one.
1 parent 6ff33ee commit 54a56e4

3 files changed

Lines changed: 48 additions & 4 deletions

File tree

lib/jsonapi/active_model_error_serializer.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ class ActiveModelErrorSerializer < ErrorSerializer
1313

1414
attribute :code do |object|
1515
_, error_hash = object
16-
code = error_hash[:error] unless error_hash[:error].is_a?(Hash)
16+
unless error_hash[:error].is_a?(Hash) || error_hash[:error].is_a?(String)
17+
code = error_hash[:error]
18+
end
1719
code ||= error_hash[:message] || :invalid
1820
# `parameterize` separator arguments are different on Rails 4 vs 5...
1921
code.to_s.delete("''").parameterize.tr('-', '_')
@@ -29,9 +31,15 @@ class ActiveModelErrorSerializer < ErrorSerializer
2931
error_key, nil, error_hash[:error]
3032
)
3133
elsif error_hash[:error].present?
32-
message = errors_object.generate_message(
33-
error_key, error_hash[:error], error_hash
34-
)
34+
# if the error was added as a string, we do not need to generate an
35+
# error message, but use the error as is
36+
if error_hash[:error].is_a?(String)
37+
message = error_hash[:error]
38+
else
39+
message = errors_object.generate_message(
40+
error_key, error_hash[:error], error_hash
41+
)
42+
end
3543
else
3644
message = error_hash[:message]
3745
end

spec/dummy.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ class Note < ActiveRecord::Base
4343
validates_format_of :title, without: /BAD_TITLE/
4444
validates_numericality_of :quantity, less_than: 100, if: :quantity?
4545
belongs_to :user, required: true
46+
47+
validate :nice_title
48+
49+
# this validation is added to test that a validation error added as a string
50+
# (instead of a symbol) works as well
51+
#
52+
# see https://api.rubyonrails.org/v7.0.8.7/classes/ActiveModel/Errors.html#method-i-add
53+
#
54+
# > If `type` is a string, it will be used as error message.
55+
#
56+
def nice_title
57+
return unless title == 'NOT_SO_NICE_TITLE'
58+
59+
errors.add(:title, 'is not so nice')
60+
end
4661
end
4762

4863
class CustomNoteSerializer

spec/errors_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@
119119
end
120120
end
121121

122+
context 'with a validation error type that is a string' do
123+
let(:params) do
124+
payload = note_params.dup
125+
payload[:data][:attributes][:title] = 'NOT_SO_NICE_TITLE'
126+
payload
127+
end
128+
129+
it do
130+
expect(response).to have_http_status(:unprocessable_entity)
131+
expect(response_json['errors'].size).to eq(2)
132+
expect(response_json['errors'][0]['status']).to eq('422')
133+
expect(response_json['errors'][0]['code']).to include('invalid')
134+
expect(response_json['errors'][0]['title'])
135+
.to eq(Rack::Utils::HTTP_STATUS_CODES[422])
136+
expect(response_json['errors'][0]['source'])
137+
.to eq('pointer' => '/data/attributes/title')
138+
expect(response_json['errors'][0]['detail'])
139+
.to eq('Title is not so nice')
140+
end
141+
end
142+
122143
context 'as a param attribute' do
123144
let(:params) do
124145
payload = note_params.dup

0 commit comments

Comments
 (0)