From 81c55b2eac53984e7ce25b8d8686011cc672bc95 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 20 Aug 2025 22:50:28 +0200 Subject: [PATCH 01/10] Add spec for issue: polymorphic entity with custom documentation - Reproduces "Empty model" error when using Grape::Entity with all hidden properties - Tests custom documentation override with Array[Object] type - Includes working example showing expected behavior - Follows existing issue spec patterns in the codebase --- .rspec | 1 + ...c_entity_with_custom_documentation_spec.rb | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb diff --git a/.rspec b/.rspec index 8c18f1a..740e6ee 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --format documentation --color +--require "spec_helper" \ No newline at end of file diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb new file mode 100644 index 0000000..61a7cee --- /dev/null +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +describe '#962 empty entity with custom documentation type' do + context "when entity has no properties" do + let(:app) do + Class.new(Grape::API) do + namespace :issue962 do + class Foo < Grape::Entity + end + + class Report < Grape::Entity + expose :foo, + as: :bar, + using: Foo, + documentation: { + type: 'Array[object]', + desc: 'The bar in your report', + example: { + 'id' => 'string', + 'status' => 'string', + } + } + end + + desc 'Get a report', success: Report + get '/' do + present({ foo: [] }, with: Report) + end + end + + add_swagger_documentation format: :json + end + end + + subject(:swagger_doc) do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + specify do + expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ + 'type' => 'array', + 'description' => 'The bar in your report', + 'items' => { + '$ref' => '#/definitions/Foo' + }, + 'example' => { + 'id' => 'string', + 'status' => 'string' + } + }) + end + + specify do + expect(swagger_doc['definitions']['Foo']).to eql({ + 'type' => 'object', + 'properties' => {}, + }) + end + end + + context "when entity has only hidden properties" do + let(:app) do + Class.new(Grape::API) do + namespace :issue962 do + class Foo < Grape::Entity + expose :required_prop, documentation: { hidden: true } + expose :optional_prop, documentation: { hidden: true }, if: ->() { true } + end + + class Report < Grape::Entity + expose :foo, + as: :bar, + using: Foo, + documentation: { + type: 'Array[object]', + desc: 'The bar in your report', + example: { + 'id' => 'string', + 'status' => 'string', + } + } + end + + desc 'Get a report', success: Report + get '/' do + present({ foo: [] }, with: Report) + end + end + + add_swagger_documentation format: :json + end + end + + subject(:swagger_doc) do + get '/swagger_doc' + JSON.parse(last_response.body) + end + + specify do + expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ + 'type' => 'array', + 'description' => 'The bar in your report', + 'items' => { + '$ref' => '#/definitions/Foo' + }, + 'example' => { + 'id' => 'string', + 'status' => 'string' + } + }) + end + + it "hides optional properties only" do + expect(swagger_doc['definitions']['Foo']).to eql({ + 'type' => 'object', + 'properties' => {}, + 'required' => ['required_prop'], + }) + end + end +end From d62082f141fdc86d30edeeb18f60d6b474ad6c2d Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 24 Aug 2025 15:34:26 +0200 Subject: [PATCH 02/10] Refactor entity parser and fix array documentation bug This commit addresses a long-standing issue where array types in documentation were not correctly parsed unless the explicit `is_array` option was used. The primary changes are: - A new `array_type?` method is introduced in `attribute_parser.rb` to intelligently check for `is_array`, `type: 'array'`, or `type: 'Array[Object]'`. - The `parse_grape_entity_params` method in `parser.rb` is refactored to improve readability and reduce complexity --- .rubocop_todo.yml | 2 +- lib/grape-swagger/entity/attribute_parser.rb | 18 +++-- lib/grape-swagger/entity/parser.rb | 68 ++++++++++++------- .../entities/response_model_spec.rb | 2 + 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 67cc731..496a371 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -37,7 +37,7 @@ Metrics/AbcSize: # Offense count: 2 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 120 + Max: 132 # Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns. diff --git a/lib/grape-swagger/entity/attribute_parser.rb b/lib/grape-swagger/entity/attribute_parser.rb index 9da9e3c..f9f654f 100644 --- a/lib/grape-swagger/entity/attribute_parser.rb +++ b/lib/grape-swagger/entity/attribute_parser.rb @@ -20,7 +20,7 @@ def call(entity_options) documentation = entity_options[:documentation] return param if documentation.nil? - add_array_documentation(param, documentation) if documentation[:is_array] + add_array_documentation(param, documentation) if array_type?(documentation) add_attribute_sample(param, documentation, :default) add_attribute_sample(param, documentation, :example) @@ -37,11 +37,19 @@ def call(entity_options) def model_from(entity_options) model = entity_options[:using] if entity_options[:using].present? - model ||= entity_options[:documentation][:type] if could_it_be_a_model?(entity_options[:documentation]) + documentation = entity_options[:documentation] + model ||= documentation[:type] if could_it_be_a_model?(documentation) model end + def array_type?(documentation) + return documentation[:is_array] if documentation.key?(:is_array) + return true if documentation[:type].to_s.downcase == 'array' + + documentation[:type].to_s.match?(/\Aarray\[(?.+)\]\z/i) + end + def could_it_be_a_model?(value) return false if value.nil? @@ -57,6 +65,8 @@ def ambiguous_model_type?(type) end def primitive_type?(type) + return false if type.nil? + data_type = GrapeSwagger::DocMethods::DataType.call(type) GrapeSwagger::DocMethods::DataType.request_primitive?(data_type) end @@ -69,7 +79,7 @@ def data_type_from(entity_options) documented_data_type = document_data_type(documentation, data_type) - if documentation[:is_array] + if array_type?(documentation) { type: :array, items: documented_data_type @@ -97,7 +107,7 @@ def document_data_type(documentation, data_type) end def entity_model_type(name, entity_options) - if entity_options[:documentation] && entity_options[:documentation][:is_array] + if array_type?(entity_options[:documentation]) { 'type' => 'array', 'items' => { diff --git a/lib/grape-swagger/entity/parser.rb b/lib/grape-swagger/entity/parser.rb index 3c6a8e2..9471453 100644 --- a/lib/grape-swagger/entity/parser.rb +++ b/lib/grape-swagger/entity/parser.rb @@ -43,37 +43,57 @@ def extract_params(exposure) def parse_grape_entity_params(params, parent_model = nil) return unless params - parsed = params.each_with_object({}) do |(entity_name, entity_options), memo| - documentation_options = entity_options.fetch(:documentation, {}) - in_option = documentation_options.fetch(:in, nil).to_s - hidden_option = documentation_options.fetch(:hidden, nil) - next if in_option == 'header' || hidden_option == true - - entity_name = entity_name.original if entity_name.is_a?(Alias) - final_entity_name = entity_options.fetch(:as, entity_name) - documentation = entity_options[:documentation] - - memo[final_entity_name] = if entity_options[:nesting] - parse_nested(entity_name, entity_options, parent_model) - else - attribute_parser.call(entity_options) - end - - next unless documentation - - memo[final_entity_name][:readOnly] = documentation[:read_only].to_s == 'true' if documentation[:read_only] - memo[final_entity_name][:description] = documentation[:desc] if documentation[:desc] + required = required_params(params) + parsed_params = parse_params(params, parent_model) + + handle_discriminator(parsed_params, required) + end + + def parse_params(params, parent_model) + params.each_with_object({}) do |(entity_name, entity_options), memo| + next if skip_param?(entity_options) + + original_entity_name = entity_name.is_a?(Alias) ? entity_name.original : entity_name + final_entity_name = entity_options.fetch(:as, original_entity_name) + + memo[final_entity_name] = parse_entity_options(entity_options, original_entity_name, parent_model) + add_documentation_to_memo(memo[final_entity_name], entity_options[:documentation]) + end + end + + def skip_param?(entity_options) + documentation_options = entity_options.fetch(:documentation, {}) + in_option = documentation_options.fetch(:in, nil).to_s + hidden_option = documentation_options.fetch(:hidden, nil) + + in_option == 'header' || hidden_option == true + end + + def parse_entity_options(entity_options, entity_name, parent_model) + if entity_options[:nesting] + parse_nested(entity_name, entity_options, parent_model) + else + attribute_parser.call(entity_options) end + end + + def add_documentation_to_memo(memo_entry, documentation) + return unless documentation + + memo_entry[:readOnly] = documentation[:read_only].to_s == 'true' if documentation[:read_only] + memo_entry[:description] = documentation[:desc] if documentation[:desc] + end + def handle_discriminator(parsed, required) discriminator = GrapeSwagger::Entity::Helper.discriminator(model) if discriminator - respond_with_all_of(parsed, params, discriminator) + respond_with_all_of(parsed, required, discriminator) else - [parsed, required_params(params)] + [parsed, required] end end - def respond_with_all_of(parsed, params, discriminator) + def respond_with_all_of(parsed, required, discriminator) parent_name = GrapeSwagger::Entity::Helper.model_name(model.superclass, endpoint) { @@ -83,7 +103,7 @@ def respond_with_all_of(parsed, params, discriminator) }, [ add_discriminator(parsed, discriminator), - required_params(params).push(discriminator.attribute) + required.push(discriminator.attribute) ] ] } diff --git a/spec/grape-swagger/entities/response_model_spec.rb b/spec/grape-swagger/entities/response_model_spec.rb index a784f25..b2599e4 100644 --- a/spec/grape-swagger/entities/response_model_spec.rb +++ b/spec/grape-swagger/entities/response_model_spec.rb @@ -102,6 +102,8 @@ def app JSON.parse(last_response.body)['definitions'] end + include_context 'this api' + before :all do module TheseApi module Entities From da6c8eef5e256ad2edfeb01b665844d5fffce125 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 11:51:37 +0100 Subject: [PATCH 03/10] Fix RuboCop violations after rebase from master --- .rubocop_todo.yml | 16 +++-- lib/grape-swagger/entity/parser.rb | 2 +- ...c_entity_with_custom_documentation_spec.rb | 72 +++++++++---------- .../shared_contexts/custom_type_parser.rb | 3 +- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 496a371..dd62582 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,12 +6,13 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 3 +# Offense count: 7 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: Exclude: - 'spec/grape-swagger/entities/response_model_spec.rb' + - 'spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb' - 'spec/support/shared_contexts/inheritance_api.rb' - 'spec/support/shared_contexts/this_api.rb' @@ -21,6 +22,11 @@ Lint/EmptyBlock: Exclude: - 'spec/grape-swagger/entity/attribute_parser_spec.rb' +# Offense count: 1 +Lint/EmptyClass: + Exclude: + - 'spec/support/shared_contexts/custom_type_parser.rb' + # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods. @@ -37,7 +43,7 @@ Metrics/AbcSize: # Offense count: 2 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 132 + Max: 135 # Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -79,11 +85,12 @@ RSpec/ContextWording: - 'spec/support/shared_contexts/inheritance_api.rb' - 'spec/support/shared_contexts/this_api.rb' -# Offense count: 2 +# Offense count: 3 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: - '**/spec/features/**/*' + - '**/spec/issues/**/*' - '**/spec/requests/**/*' - '**/spec/routing/**/*' - '**/spec/system/**/*' @@ -95,10 +102,11 @@ RSpec/DescribeClass: RSpec/ExampleLength: Max: 213 -# Offense count: 26 +# Offense count: 30 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/grape-swagger/entities/response_model_spec.rb' + - '**/spec/issues/**/*' - 'spec/support/shared_contexts/inheritance_api.rb' - 'spec/support/shared_contexts/this_api.rb' diff --git a/lib/grape-swagger/entity/parser.rb b/lib/grape-swagger/entity/parser.rb index 9471453..8b1b70c 100644 --- a/lib/grape-swagger/entity/parser.rb +++ b/lib/grape-swagger/entity/parser.rb @@ -131,7 +131,7 @@ def parse_nested(entity_name, entity_options, parent_model = nil) .map(&:nested_exposures) .flatten .each_with_object({}) do |value, memo| - memo[value.attribute] = value.send(:options) + memo[value.attribute] = value.send(:options) end properties, required = parse_grape_entity_params(params, nested_entities.last) diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb index 61a7cee..3e73fd4 100644 --- a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true describe '#962 empty entity with custom documentation type' do - context "when entity has no properties" do + context 'when entity has no properties' do + subject(:swagger_doc) do + get '/swagger_doc' + JSON.parse(last_response.body) + end + let(:app) do Class.new(Grape::API) do namespace :issue962 do @@ -10,16 +15,16 @@ class Foo < Grape::Entity class Report < Grape::Entity expose :foo, - as: :bar, - using: Foo, - documentation: { - type: 'Array[object]', - desc: 'The bar in your report', - example: { - 'id' => 'string', - 'status' => 'string', - } - } + as: :bar, + using: Foo, + documentation: { + type: 'Array[object]', + desc: 'The bar in your report', + example: { + 'id' => 'string', + 'status' => 'string' + } + } end desc 'Get a report', success: Report @@ -32,11 +37,6 @@ class Report < Grape::Entity end end - subject(:swagger_doc) do - get '/swagger_doc' - JSON.parse(last_response.body) - end - specify do expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ 'type' => 'array', @@ -54,32 +54,37 @@ class Report < Grape::Entity specify do expect(swagger_doc['definitions']['Foo']).to eql({ 'type' => 'object', - 'properties' => {}, + 'properties' => {} }) end end - context "when entity has only hidden properties" do + context 'when entity has only hidden properties' do + subject(:swagger_doc) do + get '/swagger_doc' + JSON.parse(last_response.body) + end + let(:app) do Class.new(Grape::API) do namespace :issue962 do class Foo < Grape::Entity expose :required_prop, documentation: { hidden: true } - expose :optional_prop, documentation: { hidden: true }, if: ->() { true } + expose :optional_prop, documentation: { hidden: true }, if: -> { true } end class Report < Grape::Entity expose :foo, - as: :bar, - using: Foo, - documentation: { - type: 'Array[object]', - desc: 'The bar in your report', - example: { - 'id' => 'string', - 'status' => 'string', - } - } + as: :bar, + using: Foo, + documentation: { + type: 'Array[object]', + desc: 'The bar in your report', + example: { + 'id' => 'string', + 'status' => 'string' + } + } end desc 'Get a report', success: Report @@ -92,11 +97,6 @@ class Report < Grape::Entity end end - subject(:swagger_doc) do - get '/swagger_doc' - JSON.parse(last_response.body) - end - specify do expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ 'type' => 'array', @@ -111,11 +111,11 @@ class Report < Grape::Entity }) end - it "hides optional properties only" do + it 'hides optional properties only' do expect(swagger_doc['definitions']['Foo']).to eql({ 'type' => 'object', 'properties' => {}, - 'required' => ['required_prop'], + 'required' => ['required_prop'] }) end end diff --git a/spec/support/shared_contexts/custom_type_parser.rb b/spec/support/shared_contexts/custom_type_parser.rb index 7095a07..a5ae7ad 100644 --- a/spec/support/shared_contexts/custom_type_parser.rb +++ b/spec/support/shared_contexts/custom_type_parser.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -CustomType = Class.new +class CustomType +end class CustomTypeParser attr_reader :model, :endpoint From f44e2f1fdd5dd3ba9e3ed291490d2c2a066d733a Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 11:59:39 +0100 Subject: [PATCH 04/10] Add CHANGELOG entry for PR #86 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b224df..1729ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### Fixes * Your contribution here. +* [#86](https://github.com/ruby-grape/grape-swagger-entity/pull/86): Fix properly parse custom array documentation - [@numbata](https://github.com/numbata). * [#87](https://github.com/ruby-grape/grape-swagger-entity/pull/87): Remove hidden attributes from required - [@bogdan](https://github.com/bogdan). * [#88](https://github.com/ruby-grape/grape-swagger-entity/pull/88): Update danger workflows - [@numbata](https://github.com/numbata). * [#89](https://github.com/ruby-grape/grape-swagger-entity/pull/89): Remove ruby-grape-danger - [@dblock](https://github.com/dblock). From 7566ddb0edff027348e906fa28989d7f32b28ff1 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 12:08:48 +0100 Subject: [PATCH 05/10] Fix test expectation to align with PR #87 (hidden attrs removed from required) --- .../962_polymorphic_entity_with_custom_documentation_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb index 3e73fd4..bf37ed7 100644 --- a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -111,11 +111,10 @@ class Report < Grape::Entity }) end - it 'hides optional properties only' do + it 'hides all hidden properties' do expect(swagger_doc['definitions']['Foo']).to eql({ 'type' => 'object', - 'properties' => {}, - 'required' => ['required_prop'] + 'properties' => {} }) end end From 3971c76a703e346cd97faf8ec26c2657754b0412 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 12:34:08 +0100 Subject: [PATCH 06/10] Skip empty entity tests for grape-swagger < 2.1.3 grape-swagger < 2.1.3 doesn't support empty model definitions. Support was added in grape-swagger PR #963 which will be released in version 2.1.3. Tests are skipped for older versions. --- CHANGELOG.md | 2 +- ...62_polymorphic_entity_with_custom_documentation_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1729ed5..f93dfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ #### Fixes * Your contribution here. -* [#86](https://github.com/ruby-grape/grape-swagger-entity/pull/86): Fix properly parse custom array documentation - [@numbata](https://github.com/numbata). +* [#86](https://github.com/ruby-grape/grape-swagger-entity/pull/86): Fix Array[Object] documentation type parsing - [@numbata](https://github.com/numbata). * [#87](https://github.com/ruby-grape/grape-swagger-entity/pull/87): Remove hidden attributes from required - [@bogdan](https://github.com/bogdan). * [#88](https://github.com/ruby-grape/grape-swagger-entity/pull/88): Update danger workflows - [@numbata](https://github.com/numbata). * [#89](https://github.com/ruby-grape/grape-swagger-entity/pull/89): Remove ruby-grape-danger - [@dblock](https://github.com/dblock). diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb index bf37ed7..f84b6ad 100644 --- a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -1,12 +1,17 @@ # frozen_string_literal: true describe '#962 empty entity with custom documentation type' do + # grape-swagger < 2.1.3 doesn't support empty model definitions (PR #963) + let(:supports_empty_models?) { Gem::Version.new(GrapeSwagger::VERSION) >= Gem::Version.new('2.1.3') } + context 'when entity has no properties' do subject(:swagger_doc) do get '/swagger_doc' JSON.parse(last_response.body) end + before { skip 'grape-swagger < 2.1.0 does not support empty models' unless supports_empty_models? } + let(:app) do Class.new(Grape::API) do namespace :issue962 do @@ -65,6 +70,8 @@ class Report < Grape::Entity JSON.parse(last_response.body) end + before { skip 'grape-swagger < 2.1.0 does not support empty models' unless supports_empty_models? } + let(:app) do Class.new(Grape::API) do namespace :issue962 do From a3b6c2a7c1ca77719fb5096d098d3c11f0867990 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 13:33:57 +0100 Subject: [PATCH 07/10] Improve spec: fix skip message and add descriptive test names --- ...olymorphic_entity_with_custom_documentation_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb index f84b6ad..c62ab43 100644 --- a/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb +++ b/spec/issues/962_polymorphic_entity_with_custom_documentation_spec.rb @@ -10,7 +10,7 @@ JSON.parse(last_response.body) end - before { skip 'grape-swagger < 2.1.0 does not support empty models' unless supports_empty_models? } + before { skip 'grape-swagger < 2.1.3 does not support empty models' unless supports_empty_models? } let(:app) do Class.new(Grape::API) do @@ -42,7 +42,7 @@ class Report < Grape::Entity end end - specify do + it 'parses Array[object] type as array with $ref items' do expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ 'type' => 'array', 'description' => 'The bar in your report', @@ -56,7 +56,7 @@ class Report < Grape::Entity }) end - specify do + it 'generates empty Foo entity definition' do expect(swagger_doc['definitions']['Foo']).to eql({ 'type' => 'object', 'properties' => {} @@ -70,7 +70,7 @@ class Report < Grape::Entity JSON.parse(last_response.body) end - before { skip 'grape-swagger < 2.1.0 does not support empty models' unless supports_empty_models? } + before { skip 'grape-swagger < 2.1.3 does not support empty models' unless supports_empty_models? } let(:app) do Class.new(Grape::API) do @@ -104,7 +104,7 @@ class Report < Grape::Entity end end - specify do + it 'parses Array[object] type as array with $ref items' do expect(swagger_doc['definitions']['Report']['properties']['bar']).to eql({ 'type' => 'array', 'description' => 'The bar in your report', From 7d13b80536ac60fa6e6f87b6c96d902c60c3b7fa Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 13:40:44 +0100 Subject: [PATCH 08/10] Add nil guard to array_type? and fix double-wrapping for bare Array type - Add nil guard for safety - Only wrap arrays when is_array: true or type: 'Array[ElementType]' - Bare type: 'Array' no longer double-wraps into array-of-arrays --- lib/grape-swagger/entity/attribute_parser.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/grape-swagger/entity/attribute_parser.rb b/lib/grape-swagger/entity/attribute_parser.rb index f9f654f..88cff70 100644 --- a/lib/grape-swagger/entity/attribute_parser.rb +++ b/lib/grape-swagger/entity/attribute_parser.rb @@ -43,11 +43,18 @@ def model_from(entity_options) model end + # Checks if documentation indicates an array type that needs items wrapping. + # Returns true for: + # - is_array: true (explicit flag) + # - type: 'Array[Something]' (array with element type) + # Returns false for: + # - type: 'Array' (bare array - already an array type, no wrapping needed) def array_type?(documentation) + return false if documentation.nil? return documentation[:is_array] if documentation.key?(:is_array) - return true if documentation[:type].to_s.downcase == 'array' - documentation[:type].to_s.match?(/\Aarray\[(?.+)\]\z/i) + # Only match Array[ElementType] syntax, not bare 'Array' + documentation[:type].to_s.match?(/\Aarray\[.+\]\z/i) end def could_it_be_a_model?(value) From f546877908339c53f6d357e6b305afb783c16db2 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 13:59:22 +0100 Subject: [PATCH 09/10] Add tests for array_type? method coverage - Test Array[Type] syntax detection - Test bare 'Array' type doesn't double-wrap - Test is_array: false is respected - Test case-insensitive array[type] syntax --- .../entity/attribute_parser_spec.rb | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/grape-swagger/entity/attribute_parser_spec.rb b/spec/grape-swagger/entity/attribute_parser_spec.rb index 71ada22..12ab99a 100644 --- a/spec/grape-swagger/entity/attribute_parser_spec.rb +++ b/spec/grape-swagger/entity/attribute_parser_spec.rb @@ -218,6 +218,29 @@ def self.to_s it { is_expected.to include(type: :array) } it { is_expected.to include(items: { type: 'string' }) } + context 'when using Array[Type] syntax' do + # Array[Type] syntax for primitives preserves the type string as-is. + # For entity models, use the `using:` option with Array[Object] syntax. + let(:entity_options) { { documentation: { type: 'Array[String]', desc: 'Colors' } } } + + it { is_expected.to include(type: :array) } + it { is_expected.to include(items: { type: 'Array[String]' }) } + end + + context 'when using lowercase array[type] syntax' do + let(:entity_options) { { documentation: { type: 'array[integer]', desc: 'Numbers' } } } + + it { is_expected.to include(type: :array) } + it { is_expected.to include(items: { type: 'array[integer]' }) } + end + + context 'when is_array is explicitly false' do + let(:entity_options) { { documentation: { type: 'string', desc: 'Single value', is_array: false } } } + + it { is_expected.to include(type: 'string') } + it { is_expected.not_to include(:items) } + end + context 'when it contains example' do let(:entity_options) do { documentation: { type: 'string', desc: 'Colors', is_array: true, example: %w[green blue] } } @@ -262,6 +285,13 @@ def self.to_s it { is_expected.not_to include('$ref') } end + context 'when using bare Array type' do + let(:entity_options) { { documentation: { type: 'Array', desc: 'Generic array' } } } + + it { is_expected.to include(type: 'array') } + it { is_expected.not_to include(:items) } + end + context 'when it is exposed as a Boolean class' do let(:entity_options) do { documentation: { type: Grape::API::Boolean, example: example_value, default: example_value } } From 6244ff2c981a25573e786dc3bf4784bd4188e42b Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 28 Jan 2026 14:16:59 +0100 Subject: [PATCH 10/10] Extract documentation variable in entity_model_type for clarity Address CoPilot review feedback for more explicit nil handling pattern. --- lib/grape-swagger/entity/attribute_parser.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/grape-swagger/entity/attribute_parser.rb b/lib/grape-swagger/entity/attribute_parser.rb index 88cff70..9067cc6 100644 --- a/lib/grape-swagger/entity/attribute_parser.rb +++ b/lib/grape-swagger/entity/attribute_parser.rb @@ -114,7 +114,9 @@ def document_data_type(documentation, data_type) end def entity_model_type(name, entity_options) - if array_type?(entity_options[:documentation]) + documentation = entity_options[:documentation] + + if array_type?(documentation) { 'type' => 'array', 'items' => {