Skip to content

fix(internal): encode nested multipart fields with bracket notation - #339

Closed
leoplct wants to merge 1 commit into
openai:mainfrom
leoplct:fix/multipart-deepobject-encoding
Closed

fix(internal): encode nested multipart fields with bracket notation#339
leoplct wants to merge 1 commit into
openai:mainfrom
leoplct:fix/multipart-deepobject-encoding

Conversation

@leoplct

@leoplct leoplct commented Aug 6, 2026

Copy link
Copy Markdown

The OpenAI API expects nested fields in multipart/form-data bodies to be flattened deepObject-style (e.g. expires_after[anchor]=created_at), but nested hashes were serialized as a single JSON part, which the API rejects with "Additional properties are not allowed ('expires_after' was unexpected)".

Flatten nested hashes and arrays inside multipart bodies using bracket notation, matching openai-python's _serialize_multipartform (qs "brackets" array format). Top-level primitive arrays keep the existing repeated-key encoding.

Fixes #212

The OpenAI API expects nested fields in multipart/form-data bodies to be
flattened deepObject-style (e.g. `expires_after[anchor]=created_at`),
but nested hashes were serialized as a single JSON part, which the API
rejects with "Additional properties are not allowed ('expires_after'
was unexpected)".

Flatten nested hashes and arrays inside multipart bodies using bracket
notation, matching openai-python's `_serialize_multipartform`
(qs "brackets" array format). Top-level primitive arrays keep the
existing repeated-key encoding.

Fixes openai#212

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc2s6tXUGssbrFoafV34Rj
@leoplct
leoplct requested a review from a team as a code owner August 6, 2026 12:46
@jbeckwith-oai jbeckwith-oai added the generator Touches generated SDK files label Aug 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the clear reproduction and for grounding the approach in openai-python’s global brackets format. The recursive nested-object work here is an important part of the complete fix.

We’ve incorporated that behavior into #348 and combined it with the missing bracket names for top-level primitive and file arrays, plus scalar compatibility and broader wire-format coverage. I’m closing this PR in favor of #348 so the consolidated implementation can be reviewed and landed as one change. Appreciate the thoughtful work here.

jbeckwith-oai added a commit to fallintoplace/openai-ruby that referenced this pull request Aug 10, 2026
## Summary

- encode multipart arrays recursively as `field[]`, including both
primitive and file arrays
- flatten nested multipart objects with bracket notation, including
arrays of nested objects
- preserve scalar field names and streaming file behavior
- escape both `name` and `filename` `Content-Disposition` parameters
- keep the fix entirely in the openai-ruby runtime; no generated model,
OpenAPI, or Castiron changes are needed

## Root cause

The shared multipart writer repeated top-level primitive and file arrays
using the scalar field name and serialized nested hashes as JSON parts.
The API's form parser expects the same global brackets format used by
openai-python and by the transformed OpenAPI examples.

Before:

- `timestamp_granularities=word` and `timestamp_granularities=segment`
- duplicate scalar `image` file parts
- one JSON `expires_after` part

After:

- `timestamp_granularities[]=word` and
`timestamp_granularities[]=segment`
- duplicate `image[]` file parts, while a single image remains `image`
- `expires_after[anchor]=created_at` and `expires_after[seconds]=3600`

The implementation replaces the old top-level array special case with
one recursive field writer. It preserves insertion order, array order,
duplicate values, and streaming file contents without materializing a
flattened body.

This consolidates and supersedes openai#249, openai#311, and openai#339.

Fixes openai#201.
Fixes openai#212.
Fixes openai#253.

## Validation

- `mise exec ruby@4.0.6 -- bundle exec ruby -Itest
test/openai/internal/util_test.rb` — 45 runs, 178 assertions
- `mise exec ruby@4.0.6 -- ./scripts/test` — 522 runs, 1,709 assertions
- `mise exec ruby@4.0.6 -- bundle exec rake lint` — 2,596 files, no
RuboCop offenses; Sorbet clean; 1,211 RBS files valid
- `mise exec ruby@4.0.6 -- bundle exec rake build:gem` — built
`openai-0.78.0.gem`
- RBI and RBS formatting plus `git diff --check`
- thermo-nuclear maintainability review and a separate
correctness/readability/architecture/security/performance review,
repeated after findings until clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

generator Touches generated SDK files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Files - expires_after was unexpected

3 participants