fix: Deserialize array-typed form parameters to arrays (#58) - #59
Open
shadowhand wants to merge 1 commit into
Open
fix: Deserialize array-typed form parameters to arrays (#58)#59shadowhand wants to merge 1 commit into
shadowhand wants to merge 1 commit into
Conversation
ParameterDeserializer::deserializeForm() decided the shape of a non-exploded value by looking for a comma instead of at the declared schema type, so `?tags=a,b` produced ["a","b"] but `?tags=a` produced the string "a". Against `type: array` the single-value case failed with TypeMismatchError, making an array parameter reject exactly one item while accepting two. Every other style branch -- simple, matrix, label, cookie -- already routes on isArrayType(). Form now does the same: when the schema is array-typed the value goes through splitBySeparator(), which yields a one item list for a lone value and an empty list for an empty one, and the comma heuristic stays as the fallback for parameters that declare no array type. Explode is untouched: exploded values still pass through unsplit, and array inputs from repeated or bracketed keys still implode as before. header_array_type_with_single_value_throws_type_mismatch asserted the old behaviour. Its `minItems: 2` schema still rejects the single value, now as MinItemsError -- "a lone value is not an array" was the wrong reason -- so it is renamed accordingly. QueryParameterEdgeCasesTest::form_array_accepts_any_item_count covers the reported JSON:API `include` case end to end -- two items, one item, and none -- through full request validation.
shadowhand
force-pushed
the
fix/58-form-array-deserialization
branch
from
August 11, 2026 14:04
c7e8c12 to
202056f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #58.
The bug
ParameterDeserializer::deserializeForm()decides whether a non-explodedformvalue is an array by looking for a comma in the string, rather than by looking at
the declared schema type:
So against
type: arraywithexplode: false, a value carrying two itemsdeserializes to a list, but a value carrying one item stays a string and then
fails the
type: arraycheck withTypeMismatchError. The result is an arrayparameter that rejects exactly one item while accepting two. The empty case is
dropped the same way:
?include=yields''rather than[].For a JSON:API
includeparameter that means?include=author,commentsisaccepted and
?include=authoris a 400.Before / after
Running the reproduction from #58 verbatim:
Before
After
The fix
formwas the only style branch using the separator-sniffing heuristic.deserializeSimple(),deserializeMatrix(),deserializeLabel()anddeserializeCookie()all route onisArrayType($param)and pass the valuethrough
splitBySeparator(), which already returns a one element list for a lonevalue and an empty list for an empty one. Form now does the same, and the comma
heuristic stays as the fallback for parameters that declare no array type:
deserializeForm()takes theParameterinstead of justbool $explode, so itcan consult the schema — the same signature the four other type-aware branches
already have.
explode: trueand array-valued input (repeated or bracketed keys) are untouched:exploded values still pass through unsplit, arrays still implode.
Tests
ParameterDeserializerTest— four unit cases on the array-typed form parameter:single value →
['solo'], comma-separated → three elements, empty →[], and atype: ['array', 'null']union to cover theisArrayType()list branch.QueryParameterEdgeCasesTest::form_array_accepts_any_item_countdrives thereported JSON:API
includeshape through full request validation via the publicbuilder, at two items, one item, and none.
I verified these tests actually catch the bug rather than merely describing the
new code: with the
src/change reverted and the tests kept, 4 of the unit andheader cases fail and 2 of the 3 functional cases error with
Expected type "array", but got "string"— while the "two items" case passes,which is precisely the asymmetry the issue reports.
One existing test re-pointed
HeaderValidationTest::header_array_type_with_single_value_throws_type_mismatchasserted the old behaviour. Its schema is
type: array, minItems: 2and the valueis
'solo', so the request is still rejected after the fix — asMinItemsError("Array has 1 items, but minimum is 2"), which is the accurate reason. "A lone
value is not an array" was the wrong one. Renamed to
header_array_type_with_single_value_throws_min_itemsaccordingly.Verification
make tests— 7138 tests, 14729 assertions, 0 failures. (The 2 reporteddeprecations are pre-existing, in unrelated files.)
make psalm— 0 errors.make cs-fix— Fixed 0 of 877 files.make rector— OK.Notes
src/. 4e5baf8 states that after the §12cleanup "all inline
//comments are resolved: only the machine-greppable ADRexemption marker remains", and
grep -rn "^\s*//" src/confirms a comment herewould be the only one. The
isArrayType()call reads the same as it does in thefour sibling branches, and the tests plus the CHANGELOG entry carry the
rationale. Happy to add one if you would prefer it called out in the source.
case they are of interest:
type: string, so?q=a,bdeserializes to['a', 'b']and fails the stringcheck. Same root cause — shape decided without consulting the type — but
fixing it changes behaviour for parameters this issue does not cover.
explode: truewith an array type.?tags=phpagainsttype: arrayalso stays a scalar and fails. This one is entangled with the
explodedefault: OpenAPI specifies
explode: trueforstyle: form, butParameter::$explodedefaults tofalsefor every style, andQueryParameterEdgeCasesTest::qp_02/qp_07are built on that default.Worth its own issue and its own decision.
Checklist
### BREAKING(N/A — see below)make testsgreenmake psalmclean (0 errors)make cs-fixcleanmake rectorcleanOn BC: a spec that declares
type: arrayand relies on a single valuedeserializing to a string would have been failing validation already, so there
is no working behaviour to break.
deserializeForm()isprivate, so itssignature change is not public API.