Skip to content

fix: preserve array structure in matchedData with wildcard validation#1356

Open
mahmoodhamdi wants to merge 1 commit into
express-validator:masterfrom
mahmoodhamdi:fix/issue-915-matchedData-array-body
Open

fix: preserve array structure in matchedData with wildcard validation#1356
mahmoodhamdi wants to merge 1 commit into
express-validator:masterfrom
mahmoodhamdi:fix/issue-915-matchedData-array-body

Conversation

@mahmoodhamdi
Copy link
Copy Markdown
Contributor

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:

// body: [{name: 'John Doe'}]
// validation: checkSchema({'*.name': {in: 'body', isString: true}})

matchedData(req, { locations: ['body'] })
// Returns: {'0': {name: 'John Doe'}}
// Expected: [{name: 'John Doe'}]

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.

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,
);

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

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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/matched-data.ts
Comment on lines +54 to +61
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,
);
Comment thread src/matched-data.ts
Comment on lines +54 to +56
const shouldBeArray =
instances.length > 0 &&
instances.every(instance => /^\d+$/.test(_.toPath(instance.path)[0]));
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.

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"];

Copy link
Copy Markdown
Member

@gustavohenke gustavohenke left a comment

Choose a reason for hiding this comment

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

Hey, thanks for the fix - please address the review comments 🙇

Comment thread src/matched-data.ts
Comment on lines +54 to +56
const shouldBeArray =
instances.length > 0 &&
instances.every(instance => /^\d+$/.test(_.toPath(instance.path)[0]));
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.

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"];

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.

matchedData returns key value pair object on array body

3 participants