Skip to content

fix(fetch): respect OpenAPI default explode: true for array query parameters (#3231)#3308

Open
zeriong wants to merge 8 commits into
orval-labs:masterfrom
zeriong:fix/fetch-default-explode-true-3231
Open

fix(fetch): respect OpenAPI default explode: true for array query parameters (#3231)#3308
zeriong wants to merge 8 commits into
orval-labs:masterfrom
zeriong:fix/fetch-default-explode-true-3231

Conversation

@zeriong
Copy link
Copy Markdown
Contributor

@zeriong zeriong commented May 3, 2026

Summary

  • Use nullish coalescing (parameterObject.explode ?? true) so that undefined defaults to true per the OpenAPI 3.0/3.1 spec instead of being treated as
    falsy.
  • Previously, array query parameters without an explicit explode field were serialized as comma-separated (?ids=1,2,3) instead of repeated key-value pairs
    (?ids=1&ids=2&ids=3).

Breaking Change

Users relying on the implicit comma-separated behavior without setting explode: false will see a change in serialization output. To preserve the old behavior,
explicitly set explode: false on the parameter.

Test plan

  • bun run build — 12 successful
  • bun vitest run — 2303 passed (3 pre-existing failures in resolve-version.test.ts, unrelated)
  • bun run test:snapshots — 3746 passed; 2 snapshot files updated (fetch/parameters, fetch/dateParams)
  • Verified solid-start package already handled this correctly, no other clients affected

Closes #3231

Summary by CodeRabbit

  • Bug Fixes
    • Fixed query-parameter explode behavior so array-valued query parameters follow style-based defaults (form-style defaults to explode).
    • Arrays in query parameters are now serialized as repeated entries when exploded (e.g., q and tag), producing correct URLs and request behavior.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Array-like query parameter explode logic was refactored into shouldExplodeArrayQueryParameter which resolves style (defaulting by in) and enables explode when style === 'form'; the explode-parameter selection now uses this predicate and snapshots updated to treat q and tag as exploded.

Changes

Query Parameter Explosion Default Compliance

Layer / File(s) Summary
Explode predicate & helpers
packages/fetch/src/index.ts
Introduced getDefaultStyle, getDefaultExplode, and isArrayLikeSchema to resolve default style/explode and detect array-like schemas.
Explode predicate
packages/fetch/src/index.ts
Added shouldExplodeArrayQueryParameter which gates explode to in === 'query', array-like schemas, and style === 'form', using explicit explode when present or the style-based default.
Explode filter usage
packages/fetch/src/index.ts
Replaced inline parameterObject.explode check with shouldExplodeArrayQueryParameter when building explodeParameters.
Test Snapshots
tests/__snapshots__/fetch/dateParams/pets/pets.ts, tests/__snapshots__/fetch/parameters/endpoints.ts
Snapshots updated: explodeParameters expanded from ['tag'] to ['q', 'tag'], so array values for q are serialized as repeated query parameters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • orval-labs/orval#3055 — Related changes to explode-aware query serialization in another client integration.

Suggested reviewers

  • melloware

Poem

🐇 I nibble code beneath the moon,
I twitch my nose and hum a tune,
Arrays now bloom as keys repeat,
q and tag dance, each value neat.
Hop—serialization tastes like sweet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing the fetch client to respect OpenAPI's default explode: true for array query parameters.
Linked Issues check ✅ Passed The PR implementation aligns with issue #3231 requirements: it treats undefined explode as true for array query parameters in form style, applies style-based gating, and includes appropriate tests.
Out of Scope Changes check ✅ Passed All changes in the PR are directly scoped to the issue objectives: refactoring explode logic, adding style-based defaulting, and updating test snapshots accordingly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@melloware melloware requested a review from soartec-lab May 3, 2026 12:20
@melloware melloware added the fetch Fetch client related issue label May 3, 2026
@soartec-lab
Copy link
Copy Markdown
Member

@zeriong

In v3.1, the default value is false.

https://swagger.io/specification/

@idiodoneo-chris
Copy link
Copy Markdown

According to the Swagger spec, for query parameters the default is, in fact, true if style is not specified or is set to form. This is the case when FastAPI produces an api spec (and FastAPI does then require exploded parameters in the query which broke with Orval 8).

Here's a typical example produced by FastAPI:

  {
    "/api/v1/interests": {
      "get": {
        "summary": "List all interests",
        "description": "Lists all available interests with localized names.",
        "operationId": "list_interests_endpoint",
        "parameters": [
          {
            "name": "l",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Language codes to prioritize for text",
              "title": "L"
            },
            "description": "Language codes to prioritize for text"
          }
        ],
      }
    }
  }

@soartec-lab
Copy link
Copy Markdown
Member

@idiodoneo-chris
Thank you for your comment. The solution would be to have a process to calculate the value of explode from the values ​​of style and form.

zeriong added 2 commits May 10, 2026 23:52
Use nullish coalescing (parameterObject.explode ?? true) so that
undefined defaults to true per the OpenAPI 3.0/3.1 spec instead of
being treated as falsy.

Closes orval-labs#3231

Signed-off-by: zeriong <jaeryong95@gmail.com>
Signed-off-by: zeriong <jaeryong95@gmail.com>
@zeriong zeriong force-pushed the fix/fetch-default-explode-true-3231 branch from 97ac3f4 to 6cb04e1 Compare May 10, 2026 14:52
@zeriong
Copy link
Copy Markdown
Contributor Author

zeriong commented May 10, 2026

@soartec-lab
Good point on involving!
style. Kept the PR scope but added a guard so default-true only applies when style is form or unspecified — leaves room for future non-form style handling without changing current behavior:

const style = parameterObject.style ?? 'form';

return (
  parameterObject.in === 'query' &&
  isArrayLike &&
  style === 'form' &&
  (parameterObject.explode ?? true)
);

Snapshots unchanged.

Comment thread packages/fetch/src/index.ts Outdated
| undefined) ?? []
).some((s) => resolveSchemaRef(s, context).schema.type === 'array');

const style = parameterObject.style ?? 'form';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note that the default value may change depending on the type of parameter.

Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of in): for "query" - "form"; for "path" - "simple"; for "header" - "simple"; for "cookie" - "form".

https://swagger.io/specification/

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.

@soartec-lab
Thanks for check, please one more review!

zeriong added a commit to zeriong/orval that referenced this pull request May 11, 2026
Address review feedback on orval-labs#3308: the OpenAPI spec defines
and  defaults per  value (query/cookie -> form/true,
path/header -> simple/false). Extract ,
, , and
 so the spec mapping is explicit
and array-like schema detection guards against malformed
oneOf/anyOf/allOf. No behavior change; snapshots unchanged.

Signed-off-by: zeriong <jaeryong95@gmail.com>
Address review feedback on orval-labs#3308: the OpenAPI spec defines style and
explode defaults per "in" value (query/cookie -> form/true,
path/header -> simple/false). Extract getDefaultStyle,
getDefaultExplode, isArrayLikeSchema, and
shouldExplodeArrayQueryParameter so the spec mapping is explicit and
array-like schema detection guards against malformed
oneOf/anyOf/allOf. No behavior change; snapshots unchanged.

Signed-off-by: zeriong <jaeryong95@gmail.com>
@zeriong zeriong force-pushed the fix/fetch-default-explode-true-3231 branch from 6812509 to 0b338b7 Compare May 11, 2026 11:45
@zeriong zeriong requested a review from soartec-lab May 12, 2026 11:57
@soartec-lab
Copy link
Copy Markdown
Member

@zeriong
I apologize, but I'm spending too much time on this, so please give me a little more time to review it.
It appears that the AI ​​has made a change of direction midway through the process and is performing unnecessary and redundant processing.

https://github.com/orval-labs/orval/blob/master/CONTRIBUTING.md#a-note-about-ai

@zeriong
Copy link
Copy Markdown
Contributor Author

zeriong commented May 12, 2026

@soartec-lab

Apologies — I think I misinterpreted your earlier note about per-in defaults as a hint to refactor further, but rereading it, it was just clarifying the spec. The extra helpers in the latest commit are out of scope for #3231 and don't change behavior.

Shall I revert to the 2nd commit (6cb04e1) with just explode ?? true and the style === 'form' guard?

I'll also be more careful in reviewing AI-assisted output before pushing. Thanks for your patience.

parameterObject.in === 'query' && isArrayLike && parameterObject.explode
);
});
const explodeParameters = parameterObjects.filter((parameterObject) =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the significance of extracting this function? It just looks like the code has been moved.

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.

Fair point — it's a single-use extraction that just relocates the original inline filter without reducing complexity. I'll fold the logic back into the .filter() callback.

}

const style = parameterObject.style ?? getDefaultStyle(parameterObject.in);
// Only `form` style supports explode semantics in a way orval currently
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/ Only form style supports explode semantics in a way orval currently
// emits (repeated key=value pairs). Other styles (spaceDelimited,
// pipeDelimited, deepObject) are intentionally not exploded here.

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.

@soartec-lab
I think the annotation is simply copied, is there any other meaning?


// Per OpenAPI 3.0/3.1, the default `explode` is `true` when `style` is `form`,
// otherwise `false`. https://swagger.io/specification/
const getDefaultExplode = (style: string): boolean => style === 'form';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly, what is the significance of extracting this variable?

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.

Agreed. By the time this is reached, style === 'form' is already guaranteed by the guard above, so getDefaultExplode(style) always returns true. The extraction adds nothing — I'll inline it to parameterObject.explode ?? true.

// Per OpenAPI 3.0/3.1, the default `style` depends on `in`:
// query, cookie -> "form"; path, header -> "simple".
// https://swagger.io/specification/
const getDefaultStyle = (parameterIn: string | undefined): string => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems unlikely that the default value will be reached. I thought a ternary operator would suffice.

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.

You're right. getDefaultStyle is only ever called from shouldExplodeArrayQueryParameter, which already returns early when in !== 'query'. So it's always called with 'query' and always returns 'form' — the cookie/path/header/default branches are unreachable here. I'll drop the switch entirely and just default style to 'form'.

@zeriong
Copy link
Copy Markdown
Contributor Author

zeriong commented May 27, 2026

@soartec-lab
Thanks for the thorough review — you're right on all four points, and I want to be transparent about how this happened. After you cited the spec's per-in style/explode defaults on May 11, I read it as a request to encode that full table explicitly, and added getDefaultStyle / getDefaultExplode / a named predicate to do so. The intent was spec fidelity, but I missed that this code path is query- and form-only by construction (orval's explodeParameters mechanism only emits repeated key=value, which is form-style query behavior). So the per-in and non-form branches I added are not just unused — they're unreachable and untestable through the actual generation path, which makes them a liability rather than future-proofing. I'll collapse these back to the minimal form from 6cb04e1 (inline array-like check, a style ?? 'form' / style === 'form' guard, and explode ?? true); behavior and snapshots are identical. I'll keep isArrayLikeSchema since it dedups the three repeated oneOf/anyOf/allOf blocks, but happy to inline that too if you'd prefer the PR to stay strictly at 6cb04e1.

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

Labels

fetch Fetch client related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fetch client does not respect OpenAPI default explode: true for array query parameters

4 participants