diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ff7c9974..128628a57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe57e7c2..c66dfd93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index 014c2f9c6..94133a956 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true -gem 'multi_json' +gem 'multi_json', '>= 1.21' eval_gemfile '../Gemfile' diff --git a/gemfiles/multi_json_1_20.gemfile b/gemfiles/multi_json_1_20.gemfile new file mode 100644 index 000000000..01db9b069 --- /dev/null +++ b/gemfiles/multi_json_1_20.gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +gem 'multi_json', '< 1.21' + +eval_gemfile '../Gemfile' diff --git a/lib/grape/json.rb b/lib/grape/json.rb index a30014538..8d71118ef 100644 --- a/lib/grape/json.rb +++ b/lib/grape/json.rb @@ -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 diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 7f7194615..1b954c32d 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -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 @@ -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 diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index 56ded26b0..7a7dd309d 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -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