Skip to content

[rust] fix: map-typed query parameters do not compile (.to_string() on a HashMap) - #24896

Open
wiebren wants to merge 3 commits into
OpenAPITools:masterfrom
wiebren:fix/rust-reqwest-trait-map-query-params
Open

wiebren wants to merge 3 commits into
OpenAPITools:masterfrom
wiebren:fix/rust-reqwest-trait-map-query-params

Conversation

@wiebren

@wiebren wiebren commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

A map-typed query parameter (type: object with typed additionalProperties, generated as HashMap<String, T>) falls through to the scalar path in several template branches, which call .to_string() on it. HashMap has no Display, so the crate does not compile:

error[E0599]: `HashMap<std::string::String, std::string::String>` doesn't implement `std::fmt::Display`
error[E0599]: the method `to_string` exists for reference `&HashMap<std::string::String, i32>`,
              but its trait bounds were not satisfied
branch reqwest reqwest-trait
required, non-nullable .to_string(): broken .to_string(): broken
required, nullable, non-deepObject JSON-encoded: ok .to_string() fallthrough: broken
optional, non-deepObject JSON-encoded: ok .to_string() fallthrough: broken
deepObject + explode handled handled

Fix

An {{#isMap}} split at each hole serializes the map with serde_json::to_string, as reqwest's optional non-primitive branch already does. No wire change for anything that compiled before: isMap also covers a bare free-form object (serde_json::Value), which moves from .to_string() to serde_json::to_string(..)?, and those produce the same JSON.

Verification

  • RustClientCodegenTest#testMapQueryParamsSerializeAsJson runs the fixture through both libraries and fails without the templates.
  • cargo build is clean with the fix and fails with E0599 without it.
  • Samples: zero diffs.

Known gaps

A required non-nullable style: deepObject map is sent as one JSON parameter. That branch never had deepObject handling (for maps it did not compile before); deepObject serialization is #24899's scope.

Related

Follow-up to #24866.

PR checklist


Summary by cubic

Fixes Rust client codegen for map-typed query parameters, which previously emitted .to_string() on a HashMap (it has no Display/ToString) and failed to compile in reqwest and reqwest-trait. Maps now serialize as a single JSON-encoded query parameter via serde_json::to_string, matching how non-primitive parameters are already handled. No behavior change for parameters that compiled before.

  • Adds a test fixture and regression test covering required and optional map query params in both libraries.
  • A required non-nullable style: deepObject map still sends as one JSON parameter; deepObject serialization remains out of scope.

Written for commit 5fb96ef. Summary will update on new commits.

Review in cubic


Generated with Claude Code

HashMap implements neither Display nor ToString, so the generated client
did not compile (E0599) wherever a map-typed query parameter fell through
to the scalar path: a required non-nullable map in both reqwest and
reqwest-trait, and the nullable/optional non-deepObject fallthroughs in
reqwest-trait. Serialize the map as one json-encoded parameter with
serde_json::to_string, exactly how reqwest's optional branch already
handles non-primitive parameters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 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/rust/reqwest/api.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache:170">
P3: A required, non-nullable map declared with `style: deepObject` falls into this `{{#isMap}}` branch and is serialized as a single JSON-encoded parameter (`labels={...}`) instead of the deep-object `labels[key]=value` form. The template only implements deepObject spreading inside the `{{#isNullable}}` branch, so required non-nullable deep-object maps bypass it. Previously this path emitted `.to_string()` and did not compile, so this is an incomplete fix, not a regression, but the wire format is now silently wrong for that style.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

{{^isArray}}
{{^isNullable}}
{{#isMap}}
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(&{{{vendorExtensions.x-rust-param-identifier}}})?)]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: A required, non-nullable map declared with style: deepObject falls into this {{#isMap}} branch and is serialized as a single JSON-encoded parameter (labels={...}) instead of the deep-object labels[key]=value form. The template only implements deepObject spreading inside the {{#isNullable}} branch, so required non-nullable deep-object maps bypass it. Previously this path emitted .to_string() and did not compile, so this is an incomplete fix, not a regression, but the wire format is now silently wrong for that style.

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/rust/reqwest/api.mustache, line 170:

<comment>A required, non-nullable map declared with `style: deepObject` falls into this `{{#isMap}}` branch and is serialized as a single JSON-encoded parameter (`labels={...}`) instead of the deep-object `labels[key]=value` form. The template only implements deepObject spreading inside the `{{#isNullable}}` branch, so required non-nullable deep-object maps bypass it. Previously this path emitted `.to_string()` and did not compile, so this is an incomplete fix, not a regression, but the wire format is now silently wrong for that style.</comment>

<file context>
@@ -166,7 +166,12 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
     {{^isArray}}
     {{^isNullable}}
+    {{#isMap}}
+    req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(&{{{vendorExtensions.x-rust-param-identifier}}})?)]);
+    {{/isMap}}
+    {{^isMap}}
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate, and worth separating from this PR's scope. That branch - required, non-nullable - has never had any isDeepObject handling at all: unlike the nullable and optional paths just below it, it goes straight to the scalar form for every type. So a required non-nullable style: deepObject map was not previously exploded either; it did not compile at all, which is what this PR fixes. The json encoding it now produces is the same shape the other libraries' non-primitive parameters use, so nothing regresses - but you are right that it is not the declared style.

The deepObject format work lives in #24899, which routes exploded deepObject maps through parse_deep_object (both the free-form and the typed-map shape). Extending that to the required non-nullable branch is a small addition and I am happy to push it - I would suggest doing it there rather than here, so all deepObject encoding stays in one PR and these two do not both edit the same template lines. Say which you prefer and I will add it.

`serde_json::to_string(&` matched any call in the file; match on the
`labels` key instead, which both libraries emit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ait-map-query-params

# Conflicts:
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant