Skip to content

Commit f4643d6

Browse files
committed
Add coerce_response_values option to enable type coercion in response validation
Fixes: #445
1 parent f1131c7 commit f4643d6

5 files changed

Lines changed: 43 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ This piece of middleware validates the parameters of incoming requests to make s
3939
|coerce_path_params| false | true | The same as `coerce_form_params`, but tries to coerce parameters encoded in a request's URL path. |
4040
|coerce_query_params| false | true | The same as `coerce_form_params`, but tries to coerce `GET` parameters encoded in a request's query string. |
4141
|coerce_recursive| false | always true | Coerce data in arrays and other nested objects |
42+
|coerce_response_values| false | false | Enable type coercion for response body validation. When `true`, allows string values like `"726"` to be coerced to numbers. When `false` (default), response bodies must match exact types defined in the schema. |
4243
|optimistic_json| false | false | Will attempt to parse JSON in the request body even without a `Content-Type: application/json` before falling back to other options. |
4344
|raise| false | false | Raise an exception on error instead of responding with a generic error body. |
4445
|strict| false | false | Puts the middleware into strict mode, meaning that paths which are not defined in the schema will be responded to with a 404 instead of being run. |

lib/committee/schema_validator/open_api_3/operation_wrapper.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,15 @@ def validate_path_and_query_params(path_params, query_params, headers, validator
138138
def response_validate_options(strict, check_header, validator_options: {})
139139
options = { strict: strict, validate_header: check_header }
140140

141-
if OpenAPIParser::SchemaValidator::ResponseValidateOptions.method_defined?(:validator_options)
142-
::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options, **validator_options)
143-
else
144-
::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options)
141+
if validator_options[:coerce_value]
142+
options[:coerce_value] = validator_options[:coerce_value]
143+
end
144+
145+
if validator_options[:allow_empty_date_and_datetime]
146+
options[:allow_empty_date_and_datetime] = validator_options[:allow_empty_date_and_datetime]
145147
end
148+
149+
::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(**options)
146150
end
147151
end
148152
end

lib/committee/schema_validator/open_api_3/response_validator.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ def initialize(operation_wrapper, validator_option)
1313
@validate_success_only = validator_option.validate_success_only
1414
@check_header = validator_option.check_header
1515
@allow_empty_date_and_datetime = validator_option.allow_empty_date_and_datetime
16+
@coerce_response_values = validator_option.coerce_response_values
1617
end
1718

1819
def call(status, headers, response_data, strict)
1920
return unless Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
2021

21-
validator_options = { allow_empty_date_and_datetime: @allow_empty_date_and_datetime }
22+
validator_options = { allow_empty_date_and_datetime: @allow_empty_date_and_datetime, coerce_value: @coerce_response_values }
2223

2324
operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header, validator_options: validator_options)
2425
end

lib/committee/schema_validator/option.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Committee
44
module SchemaValidator
55
class Option
66
# Boolean Options
7-
attr_reader :allow_blank_structures, :allow_empty_date_and_datetime, :allow_form_params, :allow_get_body, :allow_query_params, :allow_non_get_query_params, :check_content_type, :check_header, :coerce_date_times, :coerce_form_params, :coerce_path_params, :coerce_query_params, :coerce_recursive, :optimistic_json, :validate_success_only, :parse_response_by_content_type, :parameter_overwrite_by_rails_rule
7+
attr_reader :allow_blank_structures, :allow_empty_date_and_datetime, :allow_form_params, :allow_get_body, :allow_query_params, :allow_non_get_query_params, :check_content_type, :check_header, :coerce_date_times, :coerce_form_params, :coerce_path_params, :coerce_query_params, :coerce_recursive, :coerce_response_values, :optimistic_json, :validate_success_only, :parse_response_by_content_type, :parameter_overwrite_by_rails_rule
88

99
# Non-boolean options:
1010
attr_reader :headers_key, :params_key, :query_hash_key, :request_body_hash_key, :path_hash_key, :prefix
@@ -28,6 +28,7 @@ def initialize(options, schema, schema_type)
2828
@check_content_type = options.fetch(:check_content_type, true)
2929
@check_header = options.fetch(:check_header, true)
3030
@coerce_recursive = options.fetch(:coerce_recursive, true)
31+
@coerce_response_values = options.fetch(:coerce_response_values, false)
3132
@optimistic_json = options.fetch(:optimistic_json, false)
3233
@parse_response_by_content_type = options.fetch(:parse_response_by_content_type, true)
3334

test/middleware/response_validation_open_api_3_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,36 @@ def app
225225
end
226226
end
227227

228+
describe 'response type validation' do
229+
it "detects string value for number field when coerce_response_values is false" do
230+
@app = new_response_rack({ integer: '726' }.to_json, {}, app_status: 400, schema: open_api_3_schema, raise: true, validate_success_only: false, coerce_response_values: false)
231+
232+
e = assert_raises(Committee::InvalidResponse) do
233+
get "/characters"
234+
end
235+
236+
assert_match(/expected integer, but received String/i, e.message)
237+
end
238+
239+
it "passes string value for number field when coerce_response_values is true" do
240+
@app = new_response_rack({ integer: '726' }.to_json, {}, app_status: 400, schema: open_api_3_schema, raise: true, validate_success_only: false, coerce_response_values: true)
241+
242+
get "/characters"
243+
244+
assert_equal 400, last_response.status
245+
end
246+
247+
it "detects string value for number field by default (coerce_response_values defaults to false)" do
248+
@app = new_response_rack({ integer: '726' }.to_json, {}, app_status: 400, schema: open_api_3_schema, raise: true, validate_success_only: false)
249+
250+
e = assert_raises(Committee::InvalidResponse) do
251+
get "/characters"
252+
end
253+
254+
assert_match(/expected integer, but received String/i, e.message)
255+
end
256+
end
257+
228258
it 'does not suppress application error' do
229259
@app = Rack::Builder.new {
230260
use Committee::Middleware::ResponseValidation, { schema: open_api_3_schema, raise: true }

0 commit comments

Comments
 (0)