Skip to content

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

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

[rust] fix: map-typed query parameters do not compile (.to_string() on a HashMap)#24896
wiebren wants to merge 1 commit 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 query path in several template branches,
which call .to_string() on it. HashMap implements neither Display nor ToString, so
the generated 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

The holes, from a spec with one required and one optional map parameter:

branch reqwest reqwest-trait
required, non-nullable .to_string() — broken .to_string() — broken
required, nullable, non-deepObject non-primitive path, JSON-encoded — ok .to_string() fallthrough — broken
optional, non-deepObject non-primitive path, JSON-encoded — ok .to_string() fallthrough — broken
deepObject + explode handled (parse_deep_object / per-entry) handled

This was noted as a pre-existing, out-of-scope breakage in #24866's PR body; this is the
follow-up. Found generating a client from a production registry API whose listing filters
are map-typed.

The fix

Add an {{#isMap}} split at each hole, serializing the map as one json-encoded parameter
with serde_json::to_string — exactly how reqwest's optional branch already handles
non-primitive parameters, so the optional-vs-required and reqwest-vs-reqwest-trait wire
behaviors now agree. No behavior change for any parameter that compiled before: a required
free-form map (HashMap<String, serde_json::Value>) moves from a broken .to_string() to
the same JSON text, and serde_json::Value's Display was that JSON already.

Tests

RustClientCodegenTest#testMapQueryParamsSerializeAsJson generates the new
3_0/rust/map-query-params.yaml fixture with both libraries and asserts the json-encoded
form with no .to_string() left on either parameter. Fails without the template changes
(verified by stashing only the templates).

Verified by cargo build of clients generated from the fixture: both libraries compile
clean with the fix and fail with E0599 without it.

PR checklist


Generated with Claude Code


Summary by cubic

Fixes Rust client codegen for map-typed query parameters, which previously emitted .to_string() on a HashMap (which doesn't implement Display or ToString), causing compilation errors in both reqwest and reqwest-trait libraries for required non-nullable maps, and optional/nullable maps in reqwest-trait. Now these 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.

Written for commit 58b99b6. Summary will update on new commits.

Review in cubic

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.

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