[ruby] fix: explode object query parameters - #24869
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>
| {{#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>
A query parameter whose schema is an object and whose
style/explodeare left at theirdefaults —
style: form,explode: true— must go on the wire as one parameter per entry,keyed by the property name alone. The
rubyclient assigned the whole hash under theparameter name and left the http library to serialize it, which comes out in rails bracket
style, while
csharp,javaandphpgot it right.Given:
called with
{"tld": "com", "createdDate:gte": "2023-01-01"}:tld=com&createdDate%3Agte=2023-01-01filter%5Btld%5D=com&filter%5BcreatedDate%3Agte%5D=2023-01-01The cause
ruby-client/api.mustache— the generated api put the hash intoquery_paramswhole(
query_params[:'filter'] = opts[:'filter']), and no style or explode ever reaches theruntime: typhoeus, faraday and httpx all encode a nested hash in bracket style.
The fix merges an exploded map into
query_paramsentry by entry at the call site —— which fixes all three http libraries at once, since they all consume the
query_paramshash the api builds.
deepObjectandexplode: falseparameters keep the exact line theyproduced before, and so does every other parameter shape (
build_collection_paramforarrays,
to_jsonfor json-content parameters).Exploded entries use string keys where spec-declared parameters use symbol keys, so an entry
whose name collides with a declared sibling parameter does not overwrite it — both go on the
wire. There is no equivalent of the python collection-format lookup here
(
build_collection_paramis only ever emitted for declared array parameters), so theruntime-name collision that bit #24802 has no ruby counterpart.
Verified on the wire, not just asserted
The client was generated from the new fixture and pointed at a server that echoes its own raw
request line back (typhoeus, the default library):
filter(object, defaults)filter%5Btld%5D=com&filter%5BcreatedDate%3Agte%5D=2023-01-01tld=com&createdDate%3Agte=2023-01-01typedFilter(map, defaults)tld=com&createdDate%3Agte=2023-01-01deepFilter(style: deepObject)deepFilter%5Btld%5D=com&…flatFilter(explode: false)flatFilter%5Btld%5D=com&…The
deepFilterrow is worth a note: the accidental bracket encoding is the declareddeepObject format, so deepObject parameters were already correct here and stay untouched.
Known gaps, called out deliberately
explode: falsekeeps its previous bracket encoding. The spec asks forflatFilter=tld,com,createdDate:gte,2023-01-01; it was bracketed before and is bracketedafter, byte for byte — same scoping as the sibling PRs, left for a follow-up to keep this
reviewable.
Declared object models. A
$refed object model as a query parameter isisModel, notisMap, so it still goes on the wire whole. Pre-existing, and the same gap the sibling PRsdocument. Left for a follow-up.
Tests
modules/openapi-generator/src/test/resources/3_0/exploded-object-query-param.yamlcoversthe four combinations that decide the wire format:
RubyClientCodegenTest#testExplodedObjectQueryParameterIt fails without the fix (verified by stashing only the template change). All 26 tests in
RubyClientCodegenTestpass.PR checklist
./bin/generate-samples.shforbin/configs/ruby*.yaml;./bin/utils/export_docs_generators.shproduced no diff).4 sample files changed, all
fake_api.rb(ruby, ruby-autoload, ruby-faraday,ruby-httpx), from the petstore fixture's
languageparameter — a declared map with thedefault style, the same parameter that changed in the python sibling.
Generated with Claude Code
Summary by cubic
Fixes the Ruby client generator so object query parameters with default
style: formandexplode: truego on the wire as one parameter per entry, keyed by the property name alone, instead of the whole hash under the parameter name (which produced bracket-style encoding likefilter[tld]=com).Notes
deepObjectandexplode: falseparameters keep their previous wire format, byte for byte.explode: falseand$refed object models still serialize the old way; aligning them with the spec is left for follow-ups.query_paramshash the generated api builds.Written for commit cd9d7a5. Summary will update on new commits.