Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- ruby: '4.0'
gemfile: gemfiles/multi_json.gemfile
specs: 'spec/integration/multi_json'
- ruby: '4.0'
gemfile: gemfiles/multi_json_1_20.gemfile
specs: 'spec/integration/multi_json'
- ruby: '4.0'
gemfile: gemfiles/multi_xml.gemfile
specs: 'spec/integration/multi_xml'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
* [#2706](https://github.com/ruby-grape/grape/pull/2706): Fix `optional :foo, message: 'oops'` raising `UnknownValidator` - [@ericproulx](https://github.com/ericproulx).
* [#2751](https://github.com/ruby-grape/grape/pull/2751): Fix structured error messages leaking the raw i18n key for an undefined optional step such as `summary` (closes #2748) - [@ericproulx](https://github.com/ericproulx).
* [#2759](https://github.com/ruby-grape/grape/pull/2759): Use `create_additions: false` in `Grape::Json.load` to prevent object instantiation via the `json_class` key when using the stdlib JSON fallback - [@dblock](https://github.com/dblock).
* [#2764](https://github.com/ruby-grape/grape/pull/2764): Route `Grape::Json` through the non-deprecated `MultiJSON` API - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

### 3.2.1 (2026-04-16)
Expand Down
2 changes: 1 addition & 1 deletion gemfiles/multi_json.gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

gem 'multi_json'
gem 'multi_json', '>= 1.21'

eval_gemfile '../Gemfile'
5 changes: 5 additions & 0 deletions gemfiles/multi_json_1_20.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

gem 'multi_json', '< 1.21'

eval_gemfile '../Gemfile'
40 changes: 38 additions & 2 deletions lib/grape/json.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
# frozen_string_literal: true

module Grape
if defined?(::MultiJson)
Json = ::MultiJson
if defined?(::MultiJSON)
# Since multi_json 1.21.0, MultiJSON.dump is deprecated in favor of
# MultiJSON.generate (removed in 2.0). Keep Grape's dump surface but route
# it to the non-deprecated name — identical output, no deprecation warning.
# https://github.com/sferik/multi_json/blob/v1.21.1/CHANGELOG.md#deprecated
module Json
ParseError = ::MultiJSON::ParseError

class << self
def dump(object)
::MultiJSON.generate(object)
end

# parse is not deprecated; it's re-exposed (not renamed) because this
# facade is its own module and no longer inherits MultiJSON's methods.
def parse(source)
::MultiJSON.parse(source)
end
end
end
elsif defined?(::MultiJson)
# Legacy multi_json (< 1.21) predates generate/parse and only exposes
# dump/load. Map Grape's surface onto them so the call sites stay
# engine-agnostic (these names are not deprecated on < 1.21).
module Json
# Mutually exclusive with the MultiJSON branch above; only one runs.
ParseError = ::MultiJson::ParseError # rubocop:disable Lint/ConstantReassignment

class << self
def dump(object)
::MultiJson.dump(object)
end

def parse(source)
::MultiJson.load(source)
end
end
end
else
Json = ::JSON
Json::ParseError = Json::ParserError
Expand Down
4 changes: 2 additions & 2 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2580,7 +2580,7 @@ def self.call(error:, **)
raise 'rain!'
end
get '/exception'
json = Grape::Json.load(last_response.body)
json = Grape::Json.parse(last_response.body)
expect(json['error']).to eql 'rain!'
expect(json['backtrace'].length).to be > 0
end
Expand Down Expand Up @@ -3975,7 +3975,7 @@ def my_method

it 'path' do
get '/endpoint/options'
options = Grape::Json.load(last_response.body)
options = Grape::Json.parse(last_response.body)
expect(options['path']).to eq(['/endpoint/options'])
expect(options['source_location'][0]).to include 'api_spec.rb'
expect(options['source_location'][1].to_i).to be > 0
Expand Down
26 changes: 24 additions & 2 deletions spec/integration/multi_json/json_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
# frozen_string_literal: true

# grape_entity depends on multi-json and it breaks the test.
describe Grape::Json, if: defined?(MultiJson) && !defined?(Grape::Entity) do
describe Grape::Json, if: (defined?(MultiJSON) || defined?(MultiJson)) && !defined?(Grape::Entity) do
subject { described_class }

it { is_expected.to eq(MultiJson) }
# Exercise the full request/response JSON stack (parser + formatter) through
# the active multi_json backend (MultiJSON on >= 1.21, the legacy MultiJson
# facade on < 1.21); a deprecated call anywhere in the path would raise via
# the suite's deprecation handler.
context 'with a Grape API that parses and renders JSON' do
let(:app) do
Class.new(Grape::API) do
format :json

post '/echo' do
{ received: params[:value] }
end
end
end

it 'parses the JSON body and renders the JSON response' do
env = Rack::MockRequest.env_for('/echo', method: Rack::POST, input: JSON.dump(value: 'hi'), 'CONTENT_TYPE' => 'application/json')
response = Rack::MockResponse[*app.call(env)]

expect(response.status).to eq(201)
expect(JSON.parse(response.body)).to eq('received' => 'hi')
end
end
end
Loading