Conversation
A query parameter whose schema is an object and whose style/explode are left at their defaults — style: form, explode: true — must go on the wire as one parameter per entry, keyed by the property name alone. The ruby client assigned the whole hash under the parameter name, leaving the http library to serialize it in rails bracket style (filter[tld]=com&filter[createdDate%3Agte]=...). The generated api now merges an exploded map into query_params entry by entry, which fixes all three http libraries (typhoeus, faraday, httpx) at once, since they all consume the query_params hash the api builds. deepObject and explode: false objects keep their previous wire format, byte for byte. The new test fixture covers the four style/explode combinations that decide the wire format; the test fails without the template change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxDNCjqJycKTfzVWg2SeTJ
There was a problem hiding this comment.
2 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/client/petstore/ruby-faraday/lib/petstore/api/fake_api.rb">
<violation number="1" location="samples/client/petstore/ruby-faraday/lib/petstore/api/fake_api.rb:1575">
P3: The exploded entries use string keys (`query_params[name.to_s]`), while every other declared parameter in this method is set with a symbol key (`query_params[:'pipe']`, `query_params[:'context']`, etc.). Because Ruby treats `:"pipe"` and `"pipe"` as distinct hash keys, a `language` map entry whose name collides with a declared parameter is not overwritten as the PR description claims - it is serialized in addition to the declared parameter, producing two same-named query parameters on the wire (e.g. the array `context` plus `context=<string>`). Use the same key namespace or document the collision behavior; mixing symbol-keyed declared params with string-keyed exploded parts makes the "no overwriting" guarantee produce ambiguous duplicate params instead.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/ruby-client/api.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/ruby-client/api.mustache:187">
P3: The PR description claims exploded-map collisions with other declared parameters are "handled without overwriting", but `query_params[name.to_s] = value` is a plain hash assignment that overwrites any existing entry. If a map key equals another declared query parameter's name, the value depends purely on parameter ordering in the generated method and the earlier entry is silently dropped. Either drop the collision claim or guard the assignment (e.g. `query_params[name.to_s] = value unless query_params.key?(name.to_s)`), keeping the explicit map entries from clobbering declared parameters.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| query_params[:'allowEmpty'] = allow_empty | ||
| query_params[:'language'] = opts[:'language'] if !opts[:'language'].nil? | ||
| # form style explodes an object into one query parameter per entry, keyed by the property name alone | ||
| opts[:'language'].each { |name, value| query_params[name.to_s] = value } if !opts[:'language'].nil? |
There was a problem hiding this comment.
P3: The exploded entries use string keys (query_params[name.to_s]), while every other declared parameter in this method is set with a symbol key (query_params[:'pipe'], query_params[:'context'], etc.). Because Ruby treats :"pipe" and "pipe" as distinct hash keys, a language map entry whose name collides with a declared parameter is not overwritten as the PR description claims - it is serialized in addition to the declared parameter, producing two same-named query parameters on the wire (e.g. the array context plus context=<string>). Use the same key namespace or document the collision behavior; mixing symbol-keyed declared params with string-keyed exploded parts makes the "no overwriting" guarantee produce ambiguous duplicate params instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/ruby-faraday/lib/petstore/api/fake_api.rb, line 1575:
<comment>The exploded entries use string keys (`query_params[name.to_s]`), while every other declared parameter in this method is set with a symbol key (`query_params[:'pipe']`, `query_params[:'context']`, etc.). Because Ruby treats `:"pipe"` and `"pipe"` as distinct hash keys, a `language` map entry whose name collides with a declared parameter is not overwritten as the PR description claims - it is serialized in addition to the declared parameter, producing two same-named query parameters on the wire (e.g. the array `context` plus `context=<string>`). Use the same key namespace or document the collision behavior; mixing symbol-keyed declared params with string-keyed exploded parts makes the "no overwriting" guarantee produce ambiguous duplicate params instead.</comment>
<file context>
@@ -1571,7 +1571,8 @@ def test_query_parameter_collection_format_with_http_info(pipe, ioutil, http, ur
query_params[:'allowEmpty'] = allow_empty
- query_params[:'language'] = opts[:'language'] if !opts[:'language'].nil?
+ # form style explodes an object into one query parameter per entry, keyed by the property name alone
+ opts[:'language'].each { |name, value| query_params[name.to_s] = value } if !opts[:'language'].nil?
# header parameters
</file context>
There was a problem hiding this comment.
You are right that the string and symbol keys do not collide in the hash, but whether both then reach the wire depends on the http library. I checked it against the regenerated samples by calling test_query_parameter_collection_format with language: {"context" => "exploded"} next to the declared context:
- ruby (typhoeus):
...&context%5B0%5D=declared&...&context=exploded, both sent - ruby-httpx:
...&context%5B%5D=declared&...&context=exploded, both sent - ruby-faraday: only
context=exploded.request.params = query_paramscopies into the string-keyedFaraday::Utils::ParamsHash, so the two keys collapse and whichever was assigned later wins.
Sending both matches the go port (#24797), which Adds exploded entries to url.Values. Making faraday do the same would mean changing how the faraday client encodes params, which is out of scope here, and the spec does not define what such a collision should mean. So I have left the code alone and rewritten the "Name collisions" paragraph in the PR description to describe the behaviour per library.
| {{#isExplode}} | ||
| {{^isDeepObject}} | ||
| # form style explodes an object into one query parameter per entry, keyed by the property name alone | ||
| {{{paramName}}}.each { |name, value| query_params[name.to_s] = value } |
There was a problem hiding this comment.
P3: The PR description claims exploded-map collisions with other declared parameters are "handled without overwriting", but query_params[name.to_s] = value is a plain hash assignment that overwrites any existing entry. If a map key equals another declared query parameter's name, the value depends purely on parameter ordering in the generated method and the earlier entry is silently dropped. Either drop the collision claim or guard the assignment (e.g. query_params[name.to_s] = value unless query_params.key?(name.to_s)), keeping the explicit map entries from clobbering declared parameters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/ruby-client/api.mustache, line 187:
<comment>The PR description claims exploded-map collisions with other declared parameters are "handled without overwriting", but `query_params[name.to_s] = value` is a plain hash assignment that overwrites any existing entry. If a map key equals another declared query parameter's name, the value depends purely on parameter ordering in the generated method and the earlier entry is silently dropped. Either drop the collision claim or guard the assignment (e.g. `query_params[name.to_s] = value unless query_params.key?(name.to_s)`), keeping the explicit map entries from clobbering declared parameters.</comment>
<file context>
@@ -180,7 +180,23 @@ module {{moduleName}}
+ {{#isExplode}}
+ {{^isDeepObject}}
+ # form style explodes an object into one query parameter per entry, keyed by the property name alone
+ {{{paramName}}}.each { |name, value| query_params[name.to_s] = value }
+ {{/isDeepObject}}
+ {{#isDeepObject}}
</file context>
There was a problem hiding this comment.
Agreed, the old description was wrong. query_params[name.to_s] = value is a plain assignment. It overwrites an earlier string key: another exploded map with the same entry name, or a string key passed in through opts[:query_params]. In every library the later assignment wins. Declared parameters use symbol keys, so the hash keeps them separately, and the wire result then depends on the library. typhoeus and httpx send both, the same as the go port. faraday collapses them into its string-keyed ParamsHash and the later assignment wins. I checked this against the regenerated ruby, ruby-faraday and ruby-httpx samples.
I did not add the unless query_params.key? guard. It only covers a declared parameter assigned before the map, so it would not make the result the same across parameter orders or libraries, and it would silently drop a value the caller passed on purpose. The PR description now documents the actual behaviour instead of claiming there is no overwrite.
A nil entry went out as k= with typhoeus and as a bare k with faraday and httpx. Optional parameters are already skipped when nil, so skip nil entries too, with Hash#compact. Also shorten the shipped comment, fix the fixture description, which claimed four style/explode combinations where it covers three, and retitle the test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/ruby-client/api.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/ruby-client/api.mustache:187">
P2: A required exploded map passed as `nil` with `client_side_validation` disabled now raises `NoMethodError` before the request is sent. Add the same nil guard used by the optional branch.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/ruby-client/api.mustache:187">
P2: `Hash#compact` requires Ruby 2.4, but `gemRequiredRubyVersion` supports values as low as `>= 1.9`; clients targeting Ruby 1.9–2.3 raise `NoMethodError` for exploded maps. Replace `compact` with an `each` that skips nil values in both new branches, or raise the advertised minimum to 2.4.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| {{#isExplode}} | ||
| {{^isDeepObject}} | ||
| # form style, explode: one query parameter per entry, keyed by the property name; nil is left out | ||
| {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value } |
There was a problem hiding this comment.
P2: A required exploded map passed as nil with client_side_validation disabled now raises NoMethodError before the request is sent. Add the same nil guard used by the optional branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/ruby-client/api.mustache, line 187:
<comment>A required exploded map passed as `nil` with `client_side_validation` disabled now raises `NoMethodError` before the request is sent. Add the same nil guard used by the optional branch.</comment>
<file context>
@@ -180,7 +180,23 @@ module {{moduleName}}
+ {{#isExplode}}
+ {{^isDeepObject}}
+ # form style, explode: one query parameter per entry, keyed by the property name; nil is left out
+ {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value }
+ {{/isDeepObject}}
+ {{#isDeepObject}}
</file context>
| {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value } | |
| {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value } if !{{{paramName}}}.nil? |
There was a problem hiding this comment.
True, but it only arises for a required parameter passed as nil with client_side_validation turned off, which is a caller error the validation exists to catch. With validation on (the default) the call fails earlier with the usual "Missing the required parameter" message. I have left it rather than add a nil guard to the required branch, since a guard there would silently send the request without a required parameter.
| {{#isExplode}} | ||
| {{^isDeepObject}} | ||
| # form style, explode: one query parameter per entry, keyed by the property name; nil is left out | ||
| {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value } |
There was a problem hiding this comment.
P2: Hash#compact requires Ruby 2.4, but gemRequiredRubyVersion supports values as low as >= 1.9; clients targeting Ruby 1.9–2.3 raise NoMethodError for exploded maps. Replace compact with an each that skips nil values in both new branches, or raise the advertised minimum to 2.4.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/ruby-client/api.mustache, line 187:
<comment>`Hash#compact` requires Ruby 2.4, but `gemRequiredRubyVersion` supports values as low as `>= 1.9`; clients targeting Ruby 1.9–2.3 raise `NoMethodError` for exploded maps. Replace `compact` with an `each` that skips nil values in both new branches, or raise the advertised minimum to 2.4.</comment>
<file context>
@@ -180,7 +180,23 @@ module {{moduleName}}
+ {{#isExplode}}
+ {{^isDeepObject}}
+ # form style, explode: one query parameter per entry, keyed by the property name; nil is left out
+ {{{paramName}}}.compact.each { |name, value| query_params[name.to_s] = value }
+ {{/isDeepObject}}
+ {{#isDeepObject}}
</file context>
There was a problem hiding this comment.
The generated client already needs a newer ruby than that: master's base_object.mustache calls attributes.transform_keys(&:to_sym) in every model's build_from_hash, and Hash#transform_keys is Ruby 2.5. So Hash#compact (2.4) does not raise the real floor; the >= 1.9 default of gemRequiredRubyVersion is already out of date on master, which is worth its own fix rather than avoiding 2.4 methods here.
There was a problem hiding this comment.
agreed. will file a pr to update ruby to newer version instead.
|
@wiebren thanks for the PR to improve the Ruby client generator. Worth mentioning there's another generator |
Bug
A query parameter whose schema is an object, with
style/explodeleft at their defaults (form,true), must go on the wire as one parameter per entry, keyed by the property name alone. Therubyclient assigned the whole hash under the parameter name, and every http library then bracketed it.called with
{"category": "books", "createdDate:gte": "2023-01-01"}:category=books&createdDate%3Agte=2023-01-01filter%5Bcategory%5D=books&filter%5BcreatedDate%3Agte%5D=2023-01-01Series
One of six per-language PRs for the same bug: #24797 go, #24802 python, #24803 typescript-fetch, #24867 kotlin, #24868 dart, #24869 ruby. No shared
main/code; each adds the same fixture,3_0/exploded-object-query-param.yaml.Fix
ruby-client/api.mustachemerges an exploded map intoquery_paramsentry by entry at the call site, leaving nil entries out, the way nil optional parameters already are:That fixes typhoeus, faraday and httpx at once, since they all consume the
query_paramshash.deepObjectandexplode: falseparameters, and every other parameter shape, produce the line they did before.Name collisions. An exploded entry uses a string key where declared parameters use symbols, so on a name clash typhoeus and httpx send both, while faraday (whose params hash is string-keyed) keeps the one assigned last.
Verified
A client generated from the fixture, against a server that echoes its raw query string (typhoeus and faraday give the same result):
filter(object, defaults)category=books&createdDate%3Agte=2023-01-01typedFilter(map, defaults)category=books&createdDate%3Agte=2023-01-01filterwith{"k": nil, "category": "books"}category=books(without.compact:k=with typhoeus, a barekwith faraday)deepFilter(style: deepObject)deepFilter%5Bcategory%5D=books&…(as on master, and already correct)flatFilter(explode: false)flatFilter%5Bcategory%5D=books&…(as on master, see Known gaps)RubyClientCodegenTest#testExplodedObjectQueryParameterfails without the fix.Known gaps
explode: falsekeeps its bracket encoding, as on master; the spec wantsflatFilter=category,books,….$ref,isModelrather thanisMap) still go on the wire whole.multiarrays included: the encoding is up to the http library andconfig.params_encoding(bracketed by default,tld%5B0%5D=com), and so is a nil item inside it.PR checklist
./bin/generate-samples.sh bin/configs/ruby*.yaml): 4 files,fake_api.rbin ruby, ruby-autoload, ruby-faraday and ruby-httpx, from the petstore fixture'slanguageparameter (a declared map with the default style).Generated with Claude Code