Skip to content

fix!: explode form-urlencoded body array properties by default#195

Closed
t-unit wants to merge 4 commits into
mainfrom
form-body-array-explode
Closed

fix!: explode form-urlencoded body array properties by default#195
t-unit wants to merge 4 commits into
mainfrom
form-body-array-explode

Conversation

@t-unit

@t-unit t-unit commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Problem

For an application/x-www-form-urlencoded request body whose object schema has an array property, the generated client always sent the comma-joined form:

q=hello&tags=urgent,open

OAS 3.x defines per-property serialization for urlencoded bodies via the Encoding Object, which follows query-parameter defaults: style: form, explode: true. RFC 6570 form expansion ({?tags*}) serializes that as repeated keys:

q=hello&tags=urgent&tags=open

The comma-joined output is the explode: false form, so a conformant server parsed the array as a single element. Additionally, an explicit per-property explode in the spec's Encoding Object was parsed but silently ignored.

Fix

  • Generated object/allOf toForm now delivers array elements with boundaries intact (a Map<String, List<String>> alongside the existing property map) instead of pre-flattening them, and the runtime form encoder emits one name=value entry per element when the property's effective explode is true.
  • Per-property explode from the Encoding Object is threaded through (FormFieldEncoding); absent an Encoding Object the OAS default true applies. An explicit explode: false keeps the comma-joined single entry.
  • Element boundaries stay lossless in combination with allowReserved, where element values may contain literal commas (tags=a,b&tags=c remains two entries).
  • Empty arrays are omitted from the exploded output per RFC 6570; a single-empty-string element emits one tags= entry. If the runtime is asked to explode a property without element data (an encoder invariant violation), it throws an EncodingException naming the property instead of silently dropping the field.
  • Nullability of nested allOf member fields (including write-only/read-only widening) is derived from the same predicate used for field type generation, so guarded access is emitted wherever the field type is nullable.
  • Decoding needed no changes: the object decoder already merges repeated keys for list properties.
  • docs/uri_encoding.md updated to describe the new default behavior.

Breaking change

The wire format of form-urlencoded bodies with array properties changes from tags=a,b to tags=a&tags=b unless the spec explicitly sets explode: false. This matches the OAS default; specs relying on the previous behavior should declare explode: false in the Encoding Object.

Tests

  • tonik_util: repeated-entry explode, explode=false comma-join, empty list vs [''], element percent-encoding, allowReserved with comma-containing elements, missing-element-data throw — all with literal expected values.
  • tonik_generate: full-body emission tests for the class path (required, optional null-guarded, nullable-element, immutable-collection) and the allOf path (object member with array property, direct list member, nullable member, duplicate-key last-wins, write-only null-guard).
  • integration_test/form_urlencoded: repeated-key wire assertions, explicit explode: false scenario, comma-in-element allowReserved scenario, round-trip.

Array properties of object schemas in application/x-www-form-urlencoded
request bodies were always comma-joined (tags=a,b). Per OAS 3.x the
Encoding Object defaults to style: form, explode: true for such
properties, which RFC 6570 form expansion serializes as repeated keys
(tags=a&tags=b). Element boundaries are now threaded through generated
toForm so exploded entries stay lossless even when allowReserved keeps
literal commas in element values; an explicit explode: false in the
spec preserves the comma-joined form.
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.79%. Comparing base (354c998) to head (dd6e71e).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #195      +/-   ##
==========================================
+ Coverage   92.74%   92.79%   +0.05%     
==========================================
  Files         174      175       +1     
  Lines       17765    17900     +135     
==========================================
+ Hits        16476    16611     +135     
  Misses       1289     1289              
Files with missing lines Coverage Δ
...tonik_generate/lib/src/model/all_of_generator.dart 94.57% <100.00%> (+0.09%) ⬆️
.../tonik_generate/lib/src/model/class_generator.dart 96.28% <100.00%> (+0.03%) ⬆️
...e/lib/src/util/form_exploded_values_generator.dart 100.00% <100.00%> (ø)
...b/src/util/to_form_value_expression_generator.dart 100.00% <100.00%> (ø)
...enerate/lib/src/util/type_reference_generator.dart 97.47% <ø> (ø)
...ges/tonik_parse/lib/src/request_body_importer.dart 98.31% <100.00%> (+0.06%) ⬆️
...util/lib/src/encoding/form_encoder_extensions.dart 100.00% <100.00%> (ø)
...nik_util/lib/src/encoding/form_field_encoding.dart 80.00% <ø> (ø)
Files with missing lines Coverage Δ
...tonik_generate/lib/src/model/all_of_generator.dart 94.57% <100.00%> (+0.09%) ⬆️
.../tonik_generate/lib/src/model/class_generator.dart 96.28% <100.00%> (+0.03%) ⬆️
...e/lib/src/util/form_exploded_values_generator.dart 100.00% <100.00%> (ø)
...b/src/util/to_form_value_expression_generator.dart 100.00% <100.00%> (ø)
...enerate/lib/src/util/type_reference_generator.dart 97.47% <ø> (ø)
...ges/tonik_parse/lib/src/request_body_importer.dart 98.31% <100.00%> (+0.06%) ⬆️
...util/lib/src/encoding/form_encoder_extensions.dart 100.00% <100.00%> (ø)
...nik_util/lib/src/encoding/form_field_encoding.dart 80.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

t-unit added 3 commits July 5, 2026 14:41
Default the per-property explode to true only for form or absent style,
per the Encoding Object; spaceDelimited/pipeDelimited arrays keep their
previous comma-joined output instead of newly exploding. Duplicate raw
names across allOf members now emit exploded elements from the same
member whose value wins the parameterProperties merge at runtime,
including when later nullable members are null. The exploded-form-array
predicate is shared across all three emission sites so alias-wrapped
arrays are handled identically everywhere, and the missing-exploded-
values exception now names the parameter and points at generator or
runtime drift.
Split the form array binding's null test into a member guard and a leaf
guard so the exploded-elements channel selects the same winner as the
runtime parameterProperties merge: an absent composition member falls
through to an earlier duplicate-key candidate, while a present member
whose array value is null wins with zero entries, matching the empty
string it writes into the merged map. Document the form-body limits
(oneOf/anyOf bodies, non-form styles, additionalProperties, null
arrays) in the URI encoding guide, drop a dead read-only guard in the
allOf binding collector, and omit the empty parameter-name segment from
the exploded-values drift exception.
Gate null-aware access and force-unwraps on each link's immediate
nullability instead of path-accumulated nullability, so generated
guards carry no null-aware operators on non-nullable receivers and no
unnecessary non-null assertions. Warn at parse time when a form-body
array declares spaceDelimited or pipeDelimited without explode: true,
since only the form style is supported and the array is comma-joined.
The exploded-values drift exception now names both possible causes
instead of ruling out caller input, and the encoder documents the
deliberate one-way tolerance for exploded values without descriptors.
@t-unit t-unit closed this Jul 5, 2026
@t-unit t-unit deleted the form-body-array-explode branch July 5, 2026 17:36
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