[Fix] stringify: stop re-encoding structural separator dots when encodeDotInKeys is set - #564
Open
spokodev wants to merge 1 commit into
Open
Conversation
…ncodeDotInKeys` is set
With `allowDots` + `encodeDotInKeys`, the nesting-separator dots between
keys were being percent-encoded on each level of recursion, because the
encoding ran over the accumulated key path (`prefix`) rather than the
individual key segment.
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true })
// before: 'a%252Eb.c=d' (a->b separator encoded; deeper nesting
// clobbers every separator but the last)
// after: 'a.b.c=d' (no key has a literal dot, so it is a no-op)
This broke the documented `stringify` -> `parse` round-trip:
qs.parse('a%252Eb.c=d', { allowDots: true, decodeDotInKeys: true })
// before: { 'a.b': { c: 'd' } }
// after: { a: { b: { c: 'd' } } }
`encodeDotInKeys` is meant to encode literal dots inside a key, not the
structural `.` that `allowDots` inserts between keys. Encode the dot once
per key segment in the top-level driver (matching how nested child keys
are already encoded) instead of replaying the replace over the inherited
prefix.
spokodev
force-pushed
the
fix/encodedotinkeys-nesting-separators
branch
from
June 29, 2026 09:34
25239e7 to
78db44d
Compare
Contributor
Author
|
Rebased onto main. The top-level-key portion of this is now on main via 708fade (it adds the same |
ljharb
added a commit
that referenced
this pull request
Jul 2, 2026
…vior
With `allowDots` + `encodeDotInKeys`,
stringify currently over-encodes the structural `.` separators between nested keys
(e.g. { a: { b: { c: 'd' } } } -> a%252Eb.c=d),
which breaks the stringify -> parse round-trip.
Related: #564
ljharb
force-pushed
the
fix/encodedotinkeys-nesting-separators
branch
from
July 2, 2026 15:41
78db44d to
662cd48
Compare
Owner
|
I added tests for the current behavior to main, and rebased this PR. |
Contributor
Author
|
Thanks for rebasing. The remaining change still narrows to the |
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.
With
allowDots+encodeDotInKeys,stringifypercent-encodes the structural nesting-separator dots, not just the literal dots inside a key. That breaks the documentedstringify->parseround-trip for any nested object whose keys do not contain a literal dot.Repro
Deeper nesting clobbers every separator except the last:
The control case (
{ allowDots: true }alone) round-trips correctly, so the regression is specific toencodeDotInKeys.What the option means
Per the README,
encodeDotInKeysencodes the dots that appear in the keys of an object:The dot in
name.objis part of the key and must be encoded; the dot beforefirst/lastis the separatorallowDotsinserts and must stay literal. The bug encodes both.Root cause
lib/stringify.js:prefixis the accumulated key path, so this replace runs again at every level of recursion and re-encodes separator dots that were inherited from parent levels. The literal dots inside an individual key are already handled per segment a few lines down (encodedKey = ... String(key).replace(...)), so the prefix-wide replace is the source of the over-encoding.Fix
Encode the dots once per key segment in the top-level driver (the same place and the same way nested child keys are already encoded), and let
encodedPrefixbe the plain accumulated path. Two lines inlib/stringify.js.Relationship to #562
This is a different defect from the open #562, which fixes the opposite failure: a top-level key that contains a literal dot and has a primitive value was under-encoded (
{ 'a.b': 'c' }produceda.b=cinstead ofa%252Eb=c). #562 never touches theencodedPrefixline, so applying it leaves this over-encoding bug present:The two fixes share one line (moving the per-segment encode into the driver), because removing the prefix-wide replace requires the top-level key's own literal dots to be encoded there instead, otherwise the README
name.objexample regresses. The distinct defect addressed here is the prefix-wide re-encoding on the recursion path.Verification
stringifytest for the nested separators plus a round-trip assertion; it fails on current HEAD (red) and passes with the fix (green).1016assertions pass.npm run readmepasses, so every documentedencodeDotInKeysexample still produces its documented output (thename.objliteral-dot cases are unchanged).npm run lintclean (no new warnings on the changed lines).