How Lodash _.set Parses Bracket and Dot Notation

This article explains how Lodash's _.set method internally parses string paths containing bracket and dot notations into object keys. When manipulating nested data structures, Lodash must accurately distinguish between property names separated by periods and indices or property names enclosed in brackets. Below is an examination of the internal path-casting pipeline, the regular expression engine Lodash uses, and how both syntax styles are normalized into a unified array of keys.

The Normalization Pipeline: castPath and stringToPath

When _.set(object, path, value) is called, the library first ensures the path argument is converted into an array of individual keys. This is handled by an internal utility function called castPath.

  1. If the provided path is already an array, castPath returns it directly.
  2. If the path is a symbol or a key that already exists directly on the target object, it is treated as a single literal key.
  3. Otherwise, the string is passed to stringToPath, a memoized utility responsible for converting path strings into arrays of property names.

By converting all representations into an array early, _.set avoids having to determine whether an access operation used dots or brackets during the actual object traversal and assignment steps.

The Unified Parsing Engine: rePropName

Rather than using separate parsers or conditional logic branches to detect dot notation versus bracket notation, Lodash relies on a single, comprehensive regular expression named rePropName.

In the Lodash source code, rePropName is structured to match valid path tokens sequentially. It matches three distinct syntactic forms:

const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;

This pattern matches tokens using an alternation (|) strategy:

1. Dot Notation Segments ([^.[\]]+)

This branch matches any sequence of characters that does not contain a dot or opening/closing brackets. In a path like a.b.c, it sequentially matches a, b, and c. The dots act merely as delimiters excluded by the character class.

2. Bracket Notation Segments (\[(?:... )\])

This branch captures keys enclosed in brackets and differentiates the content inside:

When this branch matches, the capture groups isolate the inner content, stripping away the surrounding brackets and quotation marks.

3. Empty Tokens and Consecutive Delimiters

The final branch uses a lookahead ((?=(?:\.|\[\])(?:\.|\[\]|$))) to capture empty string keys resulting from consecutive dots (e.g., a..b) or empty brackets (e.g., a[]).

Constructing the Key Array

The stringToPath function executes rePropName across the input string using String.prototype.replace:

const result = [];
if (string.charCodeAt(0) === 46 /* . */) {
  result.push('');
}
string.replace(rePropName, (match, expression, quote, subString) => {
  let key = match;
  if (quote) {
    key = subString.replace(reEscapeChar, '$1');
  } else if (expression) {
    key = expression.trim();
  }
  result.push(key);
});
return result;

During this execution:

For example, whether the input is 'user.posts[0].title' or 'user["posts"]["0"].title', the resulting array produced by stringToPath is identical:

['user', 'posts', '0', 'title']

Traversal and Type Determination

Once stringToPath produces the key array, _.set iterates through the segments using baseSet. At each step:

Lodash does not rely on bracket notation itself to decide whether to create an Array or an Object; instead, the bracket syntax is purely a string-tokenization mechanism that resolves to the same normalized key representation as dot notation.