#617·qs

allowDots corrupts a key when a bracket segment contains a literal dot

Author: afonsojanuCreated Sep 3, 2026Updated Sep 3, 2026

parse() with allowDots true mangles a key whenever one of its bracket segments already contains a literal, unencoded dot.

qs.parse('outer[a.b]=1', { allowDots: true })
// { outer: { 'a[b]': '1' } }

qs.parse('outer[a.b]=1', { allowDots: false })
// { outer: { 'a.b': '1' } }   <- this is the correct shape

The second call is the baseline: without allowDots, a.b stays a single key named a.b. Turning allowDots on should only add dot-notation as an alternative way to write brackets, not change how an existing bracket already containing a dot gets parsed. Instead the key comes out as a[b], which then gets parsed as its own nested object.

This happens regardless of encodeDotInKeys/decodeDotInKeys, so it's not related to those options at all. Reproduces on the currently published 6.16.0.

The cause is in splitKeyIntoSegments in lib/parse.js. When allowDots is on, the very first line runs a global regex over the whole raw key before the actual bracket-depth walk happens:

var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;

That regex has no idea whether a . it's looking at is already sitting inside an existing [...] group. For outer[a.b], it matches the .b and rewrites it to [b], turning the string into outer[a[b]... and because [^.[]+ is greedy and also happily consumes the segment's own closing ], the actual result ends up as outer[a[b]], which the bracket walk then reads as one literal key a[b] nested under outer, rather than the two characters a and . and b staying together as a.b.

A minimal fix would need to make this replace bracket-aware (only convert dots outside of any [...] group), or restructure so dot-splitting only happens on the parent segment before the first [, not on the whole key string.

I'm flagging this rather than opening a PR for it myself since lib/parse.js's dot/bracket key handling already has several PRs open right now (#547, #560, #564, #542, #536) touching adjacent code in the same function, and I didn't want to add another PR into that same area while those are pending review. Happy to have this folded into whichever of those ends up landing, or picked up separately, whatever's easier to manage.