fix: preserve array structure in matchedData with wildcard validation#1356
fix: preserve array structure in matchedData with wildcard validation#1356mahmoodhamdi wants to merge 1 commit into
Conversation
When the request body is an array and validated with wildcards like
'*.name', matchedData() returns an object with numeric string keys
instead of an array. This happens because the reduce accumulator is
always initialized as {}, causing lodash's _.set() to create object
properties instead of array elements.
Detect when all top-level path segments are numeric indices and
initialize the accumulator as [] so lodash correctly builds an array.
Fixes express-validator#915
There was a problem hiding this comment.
Pull request overview
This PR fixes matchedData() so that when validated paths target a root array via wildcards (e.g. *.name in body), the extracted result preserves an array shape instead of returning an object with numeric string keys (fixing #915).
Changes:
- Collect matched field instances first, then choose
[]vs{}as the reduce accumulator based on whether all top-level path segments are numeric. - Add a regression test covering array bodies validated with wildcard paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/matched-data.ts |
Infers when the output root should be an array and initializes the accumulator accordingly before _.set() populates it. |
src/matched-data.spec.ts |
Adds a test ensuring matchedData() returns an array for array bodies validated with wildcards. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const shouldBeArray = | ||
| instances.length > 0 && | ||
| instances.every(instance => /^\d+$/.test(_.toPath(instance.path)[0])); | ||
|
|
||
| return instances.reduce( | ||
| (state, instance) => _.set(state, instance.path, instance.value), | ||
| (shouldBeArray ? [] : {}) as T, | ||
| ); |
| const shouldBeArray = | ||
| instances.length > 0 && | ||
| instances.every(instance => /^\d+$/.test(_.toPath(instance.path)[0])); |
There was a problem hiding this comment.
Checking the source location is important for another reason too, which is not converting an intentional object with numeric keys to an array, e.g. this
req.body = { "0": "foo" }shouldn't become
req.body = ["foo"];
gustavohenke
left a comment
There was a problem hiding this comment.
Hey, thanks for the fix - please address the review comments 🙇
| const shouldBeArray = | ||
| instances.length > 0 && | ||
| instances.every(instance => /^\d+$/.test(_.toPath(instance.path)[0])); |
There was a problem hiding this comment.
Checking the source location is important for another reason too, which is not converting an intentional object with numeric keys to an array, e.g. this
req.body = { "0": "foo" }shouldn't become
req.body = ["foo"];
Problem
When the request body is an array and validated using wildcards,
matchedData()returns an object with numeric string keys instead of preserving the array structure:The root cause is in
matchedData()where the reduce accumulator is always initialized as{}. When lodash's_.set()receives an object with numeric paths like[0].name, it creates object properties with string keys instead of array elements.Solution
Before running the reduce, check if all top-level path segments are numeric indices. If so, initialize the accumulator as
[]instead of{}, which lets lodash correctly build an array structure.Non-numeric top-level paths (like
foo,bar.baz) are unaffected — they continue to produce a plain object.Testing
Added a test that validates an array body with wildcards and verifies the result is an array. All 312 tests pass.
Fixes #915