From 7b1aa0eabc5a8555b37f470411e61f4549d504c6 Mon Sep 17 00:00:00 2001 From: Olivier Bellone Date: Tue, 20 May 2025 17:17:19 -0700 Subject: [PATCH 1/7] Fix issue where array params are sometimes omitted when array_use_braces is true --- lib/grape-swagger/doc_methods/parse_params.rb | 7 ++ spec/swagger_v2/params_array_spec.rb | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/lib/grape-swagger/doc_methods/parse_params.rb b/lib/grape-swagger/doc_methods/parse_params.rb index 4350a482c..4b327a8a8 100644 --- a/lib/grape-swagger/doc_methods/parse_params.rb +++ b/lib/grape-swagger/doc_methods/parse_params.rb @@ -17,6 +17,7 @@ def call(param, settings, path, route, definitions, consumes) # rubocop:disable in: param_type(value_type, consumes), name: settings[:full_name] || param } + fix_name_of_body_array_param(value_type) # optional properties document_description(settings) @@ -35,6 +36,12 @@ def call(param, settings, path, route, definitions, consumes) # rubocop:disable private + def fix_name_of_body_array_param(value_type) + return unless @parsed_param[:in] == 'body' && value_type[:is_array] + + @parsed_param[:name] = @parsed_param[:name].delete_suffix('[]') + end + def document_description(settings) description = settings[:desc] || settings[:description] @parsed_param[:description] = description if description diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index 510309b21..331dd930c 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -95,6 +95,27 @@ { 'declared_params' => declared(params) } end + helpers do + params :common_params do + requires :array_of_string, type: Array[String] + requires :array_of_integer, type: Array[Integer] + end + end + + params do + use :common_params + end + get '/endpoint_with_common_params' do + { 'declared_params' => declared(params) } + end + + params do + use :common_params + end + post '/endpoint_with_common_params' do + { 'declared_params' => declared(params) } + end + add_swagger_documentation array_use_braces: array_use_braces end end @@ -220,6 +241,53 @@ expect(subject['paths']['/array_of_entities']['post']['parameters']).to eql(parameters) end end + + describe 'retrieves the documentation for parameters shared between GET and POST endpoints' do + subject do + get '/swagger_doc/endpoint_with_common_params' + JSON.parse(last_response.body) + end + + describe 'for non-body parameters' do + specify do + expect(subject['paths']['/endpoint_with_common_params']['get']['parameters']).to eql( + [ + { 'in' => 'query', 'name' => "array_of_string#{braces}", 'type' => 'array', 'items' => { 'type' => 'string' }, 'required' => true }, + { 'in' => 'query', 'name' => "array_of_integer#{braces}", 'type' => 'array', 'items' => { 'type' => 'integer', 'format' => 'int32' }, 'required' => true } + ] + ) + end + end + + describe 'for body parameters' do + specify do + expect(subject['paths']['/endpoint_with_common_params']['post']['parameters']).to eql( + [ + { 'in' => 'body', 'name' => 'postEndpointWithCommonParams', 'schema' => { '$ref' => '#/definitions/postEndpointWithCommonParams' }, 'required' => true } + ] + ) + expect(subject['definitions']['postEndpointWithCommonParams']).to eql( + 'type' => 'object', + 'properties' => { + 'array_of_string' => { # no braces, even if array_use_braces is true + 'type' => 'array', + 'items' => { + 'type' => 'string' + } + }, + 'array_of_integer' => { # no braces, even if array_use_braces is true + 'type' => 'array', + 'items' => { + 'type' => 'integer', + 'format' => 'int32' + } + } + }, + 'required' => %w[array_of_string array_of_integer] + ) + end + end + end end end end From 1ad5ad6174e367d3630d85b24002f2256254b441 Mon Sep 17 00:00:00 2001 From: Olivier Bellone Date: Mon, 2 Jun 2025 11:30:41 -0700 Subject: [PATCH 2/7] Address review comments --- lib/grape-swagger/doc_methods/move_params.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grape-swagger/doc_methods/move_params.rb b/lib/grape-swagger/doc_methods/move_params.rb index e92d647e5..b5f20e8fc 100644 --- a/lib/grape-swagger/doc_methods/move_params.rb +++ b/lib/grape-swagger/doc_methods/move_params.rb @@ -137,7 +137,7 @@ def add_to_required(definition, value) return if value.blank? definition[:required] ||= [] - definition[:required].push(*value) + definition[:required].push(*value).uniq! end def build_body_parameter(name, body_name) From 898e99d0d6ff7553c1f44abf096846ff0cfd3a32 Mon Sep 17 00:00:00 2001 From: Olivier Bellone Date: Thu, 29 May 2025 09:59:57 -0700 Subject: [PATCH 3/7] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bab3faf6..8965e46d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [#905](https://github.com/ruby-grape/grape-swagger/pull/905): Allow Float, BigDecimal and beginless ranges in `values` to set `minimum` and `maximum`, and skip infinite bounds that cannot be serialized to JSON - [@artyomb](https://github.com/artyomb). * [#1016](https://github.com/ruby-grape/grape-swagger/pull/1016): Hide the nested parameters of a Hash parameter marked `documentation: { hidden: true }`, and pass the resolved `token_owner` to parameter-level `hidden:` procs like route-level ones (raising `TokenOwnerNotFound` if the configured method is missing) - [@numbata](https://github.com/numbata). * [#1013](https://github.com/ruby-grape/grape-swagger/pull/1013): Fix array `default` values being placed inside `items` instead of on the array parameter or property, and emit an empty array `default` - [@numbata](https://github.com/numbata). +* [#957](https://github.com/ruby-grape/grape-swagger/pull/957): Fix issue where array params are sometimes omitted when `array_use_braces` is true - [@olivier-thatch](https://github.com/olivier-thatch). * [#1012](https://github.com/ruby-grape/grape-swagger/pull/1012): Keep the Danger PR check running in CI by pinning `git` below 6 - [@numbata](https://github.com/numbata). * [#1011](https://github.com/ruby-grape/grape-swagger/pull/1011): Preserve string length, array item and object property limits in body parameter definitions, and emit `minLength`/`maxLength` only for strings (`minProperties`/`maxProperties` for hashes) instead of any non-array type - [@numbata](https://github.com/numbata). * [#1009](https://github.com/ruby-grape/grape-swagger/pull/1009): Bump the Danger `danger-comment.yml` workflow from 0.1.0 to 0.1.1 - [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D). From ba0940c7fe99c5ee5df3353b0ce2cdba112dd9dc Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 3 Nov 2026 13:42:35 +0100 Subject: [PATCH 4/7] Drop duplicate braced body params so array groups keep their object type With array_use_braces, the body parser adds a braced copy of every array param next to the original. Once the brace is stripped for body params the two collide, and the copy is merged over the group's object definition, turning items.type from object into string. Dropping the duplicate by name and location also removes the duplicate required entries, so the uniq! on required is no longer needed. --- lib/grape-swagger/doc_methods/move_params.rb | 2 +- lib/grape-swagger/endpoint.rb | 3 ++ spec/swagger_v2/params_array_spec.rb | 37 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/grape-swagger/doc_methods/move_params.rb b/lib/grape-swagger/doc_methods/move_params.rb index b5f20e8fc..e92d647e5 100644 --- a/lib/grape-swagger/doc_methods/move_params.rb +++ b/lib/grape-swagger/doc_methods/move_params.rb @@ -137,7 +137,7 @@ def add_to_required(definition, value) return if value.blank? definition[:required] ||= [] - definition[:required].push(*value).uniq! + definition[:required].push(*value) end def build_body_parameter(name, body_name) diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index e116e6084..caeeb7c12 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -188,6 +188,9 @@ def params_object(route, options, path, consumes) memo << GrapeSwagger::DocMethods::ParseParams.call(param, value, path, route, @definitions, consumes) end + # braced array names collapse onto their unbraced twins once body params are renamed + parameters.uniq! { |param| [param[:name], param[:in]] } + if GrapeSwagger::DocMethods::MoveParams.can_be_moved?(route.request_method, parameters) parameters = GrapeSwagger::DocMethods::MoveParams.to_definition(path, parameters, route, @definitions) end diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index 331dd930c..9dbfa179f 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -116,6 +116,16 @@ { 'declared_params' => declared(params) } end + params do + requires :typed_group, type: Array do + requires :id, type: Integer + optional :tags, type: Array[String] + end + end + post '/nested_group_body' do + { 'declared_params' => declared(params) } + end + add_swagger_documentation array_use_braces: array_use_braces end end @@ -288,6 +298,33 @@ end end end + + describe 'retrieves the documentation for a group of parameters sent as body' do + subject do + get '/swagger_doc/nested_group_body' + JSON.parse(last_response.body) + end + + specify do + expect(subject['definitions']['postNestedGroupBody']).to eql( + 'type' => 'object', + 'properties' => { + 'typed_group' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'properties' => { + 'id' => { 'type' => 'integer', 'format' => 'int32' }, + 'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' } } + }, + 'required' => ['id'] + } + } + }, + 'required' => ['typed_group'] + ) + end + end end end end From 9377deb5fb0a3e6612acf9a9dbcdcf6d20da9dd2 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 3 Nov 2026 13:43:44 +0100 Subject: [PATCH 5/7] Cover query and explicit body arrays under array_use_braces --- spec/swagger_v2/params_array_spec.rb | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index 9dbfa179f..4b3cd0093 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -126,6 +126,22 @@ { 'declared_params' => declared(params) } end + params do + requires :query_array, type: Array[String], documentation: { param_type: 'query' } + requires :body_array, type: Array[Integer] + end + post '/mixed_param_types' do + { 'declared_params' => declared(params) } + end + + params do + requires :explicit_body_array, type: Array[String], documentation: { param_type: 'body' } + requires :implicit_body_array, type: Array[Integer] + end + post '/explicit_body' do + { 'declared_params' => declared(params) } + end + add_swagger_documentation array_use_braces: array_use_braces end end @@ -325,6 +341,42 @@ ) end end + + describe 'retrieves the documentation for an array that stays a query parameter on a POST endpoint' do + subject do + get '/swagger_doc/mixed_param_types' + JSON.parse(last_response.body) + end + + specify do + expect(subject['paths']['/mixed_param_types']['post']['parameters'].first).to eql( + 'in' => 'query', 'name' => "query_array#{braces}", 'type' => 'array', 'items' => { 'type' => 'string' }, 'required' => true + ) + expect(subject['definitions']['postMixedParamTypes']).to eql( + 'type' => 'object', + 'properties' => { 'body_array' => { 'type' => 'array', 'items' => { 'type' => 'integer', 'format' => 'int32' } } }, + 'required' => ['body_array'] + ) + end + end + + describe 'retrieves the documentation for arrays when a body parameter is declared explicitly' do + subject do + get '/swagger_doc/explicit_body' + JSON.parse(last_response.body) + end + + specify do + expect(subject['definitions']['postExplicitBody']).to eql( + 'type' => 'object', + 'properties' => { + 'explicit_body_array' => { 'type' => 'array', 'items' => { 'type' => 'string' } }, + 'implicit_body_array' => { 'type' => 'array', 'items' => { 'type' => 'integer', 'format' => 'int32' } } + }, + 'required' => %w[explicit_body_array implicit_body_array] + ) + end + end end end end From cf31c0395dff888abfc02a74def6c119b826f7b6 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 3 Nov 2026 13:46:29 +0100 Subject: [PATCH 6/7] Cover PUT, PATCH and form encoded arrays under array_use_braces --- spec/swagger_v2/params_array_spec.rb | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index 4b3cd0093..db8d47dd3 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -142,6 +142,28 @@ { 'declared_params' => declared(params) } end + params do + requires :array_of_string, type: Array[String] + end + put '/put_array' do + { 'declared_params' => declared(params) } + end + + params do + requires :array_of_string, type: Array[String] + end + patch '/patch_array' do + { 'declared_params' => declared(params) } + end + + params do + requires :array_of_string, type: Array[String] + end + desc 'form encoded array', consumes: ['application/x-www-form-urlencoded'] + post '/form_encoded_array' do + { 'declared_params' => declared(params) } + end + add_swagger_documentation array_use_braces: array_use_braces end end @@ -377,6 +399,36 @@ ) end end + + %w[put patch].each do |verb| + describe "retrieves the documentation for an array sent as body of a #{verb.upcase} endpoint" do + subject do + get "/swagger_doc/#{verb}_array" + JSON.parse(last_response.body) + end + + specify do + expect(subject['definitions']["#{verb}#{verb.capitalize}Array"]).to eql( + 'type' => 'object', + 'properties' => { 'array_of_string' => { 'type' => 'array', 'items' => { 'type' => 'string' } } }, + 'required' => ['array_of_string'] + ) + end + end + end + + describe 'retrieves the documentation for an array sent form encoded' do + subject do + get '/swagger_doc/form_encoded_array' + JSON.parse(last_response.body) + end + + specify do + expect(subject['paths']['/form_encoded_array']['post']['parameters']).to eql( + [{ 'in' => 'formData', 'name' => "array_of_string#{braces}", 'type' => 'array', 'items' => { 'type' => 'string' }, 'required' => true }] + ) + end + end end end end From 0e0272968b7a1827c3715f794e5fd9be75a9255e Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 3 Nov 2026 13:56:28 +0100 Subject: [PATCH 7/7] Document nested arrays with one array level per nesting Grape passes the declared type as e.g. "[[String]]", but only the innermost type was documented, so Array[Array[String]] came out as a flat array of strings. Wrap the items in one array level per leading bracket. --- CHANGELOG.md | 1 + lib/grape-swagger/doc_methods/parse_params.rb | 7 ++- spec/swagger_v2/params_array_spec.rb | 57 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8965e46d5..f73adf529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#1016](https://github.com/ruby-grape/grape-swagger/pull/1016): Hide the nested parameters of a Hash parameter marked `documentation: { hidden: true }`, and pass the resolved `token_owner` to parameter-level `hidden:` procs like route-level ones (raising `TokenOwnerNotFound` if the configured method is missing) - [@numbata](https://github.com/numbata). * [#1013](https://github.com/ruby-grape/grape-swagger/pull/1013): Fix array `default` values being placed inside `items` instead of on the array parameter or property, and emit an empty array `default` - [@numbata](https://github.com/numbata). * [#957](https://github.com/ruby-grape/grape-swagger/pull/957): Fix issue where array params are sometimes omitted when `array_use_braces` is true - [@olivier-thatch](https://github.com/olivier-thatch). +* [#957](https://github.com/ruby-grape/grape-swagger/pull/957): Document nested arrays such as `Array[Array[String]]` with one `array` level per nesting instead of flattening them to a single level - [@numbata](https://github.com/numbata). * [#1012](https://github.com/ruby-grape/grape-swagger/pull/1012): Keep the Danger PR check running in CI by pinning `git` below 6 - [@numbata](https://github.com/numbata). * [#1011](https://github.com/ruby-grape/grape-swagger/pull/1011): Preserve string length, array item and object property limits in body parameter definitions, and emit `minLength`/`maxLength` only for strings (`minProperties`/`maxProperties` for hashes) instead of any non-array type - [@numbata](https://github.com/numbata). * [#1009](https://github.com/ruby-grape/grape-swagger/pull/1009): Bump the Danger `danger-comment.yml` workflow from 0.1.0 to 0.1.1 - [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D). diff --git a/lib/grape-swagger/doc_methods/parse_params.rb b/lib/grape-swagger/doc_methods/parse_params.rb index 4b327a8a8..9671139ec 100644 --- a/lib/grape-swagger/doc_methods/parse_params.rb +++ b/lib/grape-swagger/doc_methods/parse_params.rb @@ -89,11 +89,16 @@ def document_array_param(value_type, definitions) value_type ) - @parsed_param[:items] = array_items + @parsed_param[:items] = nest_array_items(array_items, value_type[:type]) @parsed_param[:type] = 'array' @parsed_param[:collectionFormat] = collection_format if DataType.collections.include?(collection_format) end + def nest_array_items(items, declared_type) + depth = declared_type.to_s[/\A\[*/].size + (depth - 1).times.reduce(items) { |inner, _| { type: 'array', items: inner } } + end + def parse_array_item(definitions, type, value_type) array_items = {} if definitions[value_type[:data_type]] diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index db8d47dd3..119a9e8b2 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -164,6 +164,21 @@ { 'declared_params' => declared(params) } end + params do + requires :matrix, type: Array[Array[String]] + requires :cube, type: Array[Array[Array[Integer]]] + end + post '/nested_arrays_body' do + { 'declared_params' => declared(params) } + end + + params do + requires :matrix, type: Array[Array[String]] + end + get '/nested_arrays_query' do + { 'declared_params' => declared(params) } + end + add_swagger_documentation array_use_braces: array_use_braces end end @@ -429,6 +444,48 @@ ) end end + + describe 'retrieves the documentation for nested arrays sent as body' do + subject do + get '/swagger_doc/nested_arrays_body' + JSON.parse(last_response.body) + end + + specify do + expect(subject['definitions']['postNestedArraysBody']).to eql( + 'type' => 'object', + 'properties' => { + 'matrix' => { 'type' => 'array', 'items' => { 'type' => 'array', 'items' => { 'type' => 'string' } } }, + 'cube' => { + 'type' => 'array', + 'items' => { + 'type' => 'array', + 'items' => { 'type' => 'array', 'items' => { 'type' => 'integer', 'format' => 'int32' } } + } + } + }, + 'required' => %w[matrix cube] + ) + end + end + + describe 'retrieves the documentation for nested arrays sent as query' do + subject do + get '/swagger_doc/nested_arrays_query' + JSON.parse(last_response.body) + end + + specify do + expect(subject['paths']['/nested_arrays_query']['get']['parameters']).to eql( + [ + { + 'in' => 'query', 'name' => "matrix#{braces}", 'type' => 'array', 'required' => true, + 'items' => { 'type' => 'array', 'items' => { 'type' => 'string' } } + } + ] + ) + end + end end end end