Skip to content

feat: add a form-urlencoded decoder with bracketed nesting - #24

Merged
hughgrigg merged 1 commit into
mainfrom
feat/form-decoder
Sep 2, 2026
Merged

feat: add a form-urlencoded decoder with bracketed nesting#24
hughgrigg merged 1 commit into
mainfrom
feat/form-decoder

Conversation

@hughgrigg

Copy link
Copy Markdown
Contributor

Stacked on #23, which adds the decode hook this fills in.

application/x-www-form-urlencoded is a wire format, and not one service's dialect. Stripe, Rails
and PHP applications all speak it, all three write nesting the same way, and none of them could be
simulated without a decoder for it. It belongs beside JSON for the same reason JSON is here.

line_items[0][price_data][unit_amount]=250&line_items[0][quantity]=1&expand[]=customer
{
  "line_items": [{ "price_data": { "unit_amount": "250" }, "quantity": "1" }],
  "expand": ["customer"]
}

Decisions

Every leaf stays a string. A form body carries no types, and guessing at them would make
quantity=1 and postcode=01234 disagree about what a digit is. The resource's own creation
behaviour converts what it needs, where the target shape is known.

Digits order the entries and do not position them. a[0], a[5] and a[9] give three elements
and never a sparse array of ten. A real encoder counts from zero, where the two readings agree, and
this needs no cap on how large an index a hostile body may claim.

Four inputs throw a SyntaxError rather than being guessed at.

Body Refused because
name=a&name=b Which value wins is a guess
a=1&a[b]=2 a would hold a value and more keys at once
a[][b]=1 An empty bracket appends, and only the last part can
a[b, [a] Brackets cannot be read out of the key

That follows the existing rule that decode failures stay loud. A real encoder emits well-formed
keys, so these are hostile or mistaken input, and a simulation quietly corrupted by them would be
worse than one that stops.

The content-type header is ignored. What a body claims to be and what it holds are two facts,
and choosing the decoder by hand has already settled the first.

Notes

A __proto__ key lands as an own data property and reaches no prototype, which Object.fromEntries
gives for free. There is a test pinning it.

The parser is a page of code and should stay one. Sparse arrays, dotted keys and qs compatibility
flags are each a decision a reader would have to hold in their head, and none is needed to simulate
an API.

Loading
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.

1 participant