Skip to content

fix: handle Union with objects in query array params#1830

Open
eL1fe wants to merge 2 commits intoelysiajs:mainfrom
eL1fe:fix/union-query-multiple-objects
Open

fix: handle Union with objects in query array params#1830
eL1fe wants to merge 2 commits intoelysiajs:mainfrom
eL1fe:fix/union-query-multiple-objects

Conversation

@eL1fe
Copy link
Copy Markdown

@eL1fe eL1fe commented Mar 30, 2026

Summary

When using t.Union([t.Object({a}), t.Object({b})]) inside a query array schema, only the first object variant worked. The second always failed with a validation error.

Problem

const schema = t.Object({
  data: t.Array(t.Union([
    t.Number(),
    t.Object({ a: t.String() }),
    t.Object({ b: t.String() }), // this variant always failed
  ]))
})

Two root causes:

  1. ArrayQuery decode used value.split(',') which produced string items like '{"b":"world"}' that TypeBox couldn't match against Object union variants
  2. Multi-param format (data=1&data={"b":"world"}) built a string array that failed Check before Decode could run

Changes

  • src/type-system/index.ts: ArrayQuery decode now tries JSON.parse('[' + value + ']') before falling back to naive split, and parses JSON-like single items via tryParseItem
  • src/parse-query.ts: multi-param ArrayQuery values are joined as comma-separated string instead of building a string array, so Decode handles both formats uniformly

Test plan

  • Comma format with first/second object variant both pass
  • Multi-param format with first/second object variant both pass
  • All query validation tests pass (56 pass)
  • Full test suite passes (1525 pass, 0 fail)

Fixes #1829

Summary by CodeRabbit

  • Bug Fixes
    • Improved query parameter parsing for arrays to better handle bracketed and malformed inputs, with safer parsing and fallback behavior.
    • Enhanced handling of repeated multi-value parameters by normalizing them into comma-delimited values.
    • More robust splitting of comma-delimited query strings that respects nested structures and quoted segments.

When using t.Union([t.Object({a}), t.Object({b})]) inside a query
array, the second object variant always failed validation.

Two issues:
1. ArrayQuery decode used naive split(',') which produced string items
   that TypeBox couldn't match against Object union variants. Now tries
   JSON.parse('[...]') first to properly parse objects.

2. Multi-param format (data=1&data={"b":"world"}) created a string
   array that failed Check before Decode could run. Now joins
   multi-param values as comma-separated string so ArrayQuery
   Decode handles both formats uniformly.

Fixes elysiajs#1829
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Walkthrough

Array query parsing was tightened: bracketed values now attempt JSON.parse with a safe fallback; comma-delimited single-value arrays are parsed with nesting- and quote-aware splitting and per-item JSON parsing to better handle objects/arrays in query strings.

Changes

Cohort / File(s) Summary
Query parsing logic
src/parse-query.ts
When an expected-array query value starts with [ the code now tries JSON.parse inside a try/catch and falls back to slicing+split on failure. For non-bracketed array params, multi-parameter values are concatenated into a single comma-delimited string instead of being accumulated as arrays.
ArrayQuery decoding
src/type-system/index.ts
Reworked ArrayQuery.decode to: attempt JSON array parse for comma-containing inputs; add a tryParseItem helper that JSON.parses items starting with {/[ and otherwise treats them as raw strings; use a comma-splitter that ignores commas inside brackets/quotes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

Brackets and commas danced in query-land, baka~ (¬‿¬)♡
JSON tries first, then splits with care, nyaa~ (≧◡≦)♡
Objects hiding in commas no more, how cute~ (^▽^)♡
Parsers tightened, fallbacks lined up, don't be dramatic~ (¬_¬)♡
Now unions behave — you're welcome, messy queries~ (`∇´)♡

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: handle Union with objects in query array params' accurately reflects the main change: fixing Union schema validation with multiple Object variants in query arrays.
Linked Issues check ✅ Passed Changes directly address #1829 by fixing ArrayQuery decode to preserve JSON structure and multi-param formats so TypeBox union matching succeeds for all Object variants.
Out of Scope Changes check ✅ Passed All changes in parse-query.ts and type-system/index.ts are directly scoped to fixing Union with objects in query array params; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/parse-query.ts (1)

93-111: ⚠️ Potential issue | 🟠 Major

Stop leaving garbage mixed arrays, you dummy ♡~~

Your code has a nasty inconsistency that's gonna bite you~

When finalValue starts with [, you parse it into a real array (line 95). Then at line 106, if currentValue is already a string, you do unshift() and create a disgusting mixed-type array like ["{\"a\":\"x\"}",{"b":"y"}].

But in the else branch (line 111), you just concatenate strings normally. This means the same parameter appearing in different orders produces completely different shapes—sometimes actual arrays, sometimes stringified messes. And when you stringify that mixed array? Haha, the objects turn into [object Object] strings. How embarrassing~ (¬‿¬)

Just normalize everything to comma-separated strings from the start, like you're already doing in the non-bracket branch. Don't store both arrays AND strings for the same parameter, you're making this harder than it needs to be ♡

The suggested fix below keeps it uniform—always strings, always consistent:

Here's how to stop being so messy~
+		const normalizeArrayValue = (value: string) => {
+			if (value.charCodeAt(0) !== 91) return value
+
+			try {
+				return (JSON.parse(value) as unknown[])
+					.map((item) => JSON.stringify(item))
+					.join(',')
+			} catch {
+				return value.slice(1, -1)
+			}
+		}
+
 		if (array && array?.[finalKey]) {
-			if (finalValue.charCodeAt(0) === 91) {
-				try {
-					finalValue = JSON.parse(finalValue) as any
-				} catch {
-					finalValue = finalValue.slice(1, -1).split(',') as any
-				}
-
-				if (currentValue === undefined) result[finalKey] = finalValue
-				else if (Array.isArray(currentValue))
-					currentValue.push(...finalValue)
-				else {
-					result[finalKey] = finalValue
-					result[finalKey].unshift(currentValue)
-				}
-			} else {
-				// join multi-param values as comma-separated so ArrayQuery
-				// Decode handles them uniformly (same as comma format)
-				if (currentValue === undefined) result[finalKey] = finalValue
-				else result[finalKey] = currentValue + ',' + finalValue
-			}
+			const normalized = normalizeArrayValue(finalValue)
+
+			if (currentValue === undefined) result[finalKey] = normalized as any
+			else result[finalKey] = `${currentValue},${normalized}` as any
 		} else {
 			result[finalKey] = finalValue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parse-query.ts` around lines 93 - 111, The code in parse-query.ts mixes
real arrays and strings for the same query param (variables finalValue,
currentValue, finalKey, and result) when finalValue starts with '['; to fix,
always normalize finalValue to a comma-separated string before
assigning/merging: if JSON.parse succeeds and returns an array, convert it to a
string via array.join(','); if JSON.parse fails and you split, join the split
pieces into a single comma string; then use the same merge logic used in the
non-bracket branch (if currentValue undefined set result[finalKey] to that
string, else set result[finalKey] = currentValue + ',' + normalizedFinalValue)
and remove branches that create mixed-type arrays (e.g., the
Array.isArray(currentValue) unshift/push paths) so result values are always
strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/type-system/index.ts`:
- Around line 437-445: In the decode function, the fallback that currently does
value.split(',') must be replaced so it splits only at top-level commas
(respecting {}, [], and quoted strings) and then applies the existing
tryParseItem logic to each token; update the block inside the catch of
compiler.Decode(JSON.parse(`[${value}]`)) to call a top-level-aware splitter
(e.g., splitTopLevel) on value, map tryParseItem over the resulting tokens, and
pass that array into compiler.Decode so structured values and quoted strings are
preserved (references: function decode, helper tryParseItem, and add/use
splitTopLevel for top-level splitting).

---

Outside diff comments:
In `@src/parse-query.ts`:
- Around line 93-111: The code in parse-query.ts mixes real arrays and strings
for the same query param (variables finalValue, currentValue, finalKey, and
result) when finalValue starts with '['; to fix, always normalize finalValue to
a comma-separated string before assigning/merging: if JSON.parse succeeds and
returns an array, convert it to a string via array.join(','); if JSON.parse
fails and you split, join the split pieces into a single comma string; then use
the same merge logic used in the non-bracket branch (if currentValue undefined
set result[finalKey] to that string, else set result[finalKey] = currentValue +
',' + normalizedFinalValue) and remove branches that create mixed-type arrays
(e.g., the Array.isArray(currentValue) unshift/push paths) so result values are
always strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b48ec7ff-e8e2-4c8b-afbe-3cb93b14a342

📥 Commits

Reviewing files that changed from the base of the PR and between 56310be and 45a7293.

📒 Files selected for processing (2)
  • src/parse-query.ts
  • src/type-system/index.ts

The naive split(',') fallback sliced through JSON objects with multiple
keys like {"b":"world","c":1}. Added splitTopLevel that respects
{} [] nesting and quoted strings, and applies tryParseItem to each
token so structured values survive the fallback path.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/type-system/index.ts (1)

446-450: Escaped backslash edge case in quote detection, dummy~ (´∀`)♡

Your escape check only looks at the single preceding character, but what if someone has \\\" (escaped backslash followed by a quote)? The \\ is the escape sequence, so the " should actually END the string, not be treated as escaped~

Example: {"a":"test\\"} - the trailing " would be incorrectly considered escaped because it's preceded by \, but that \ is part of \\ (escaped backslash) ξ・∀・ξ

Buuut~ this is such a rare edge case for query params that I'll let it slide, you lucky baka ♡

✨ More robust escape handling (if you want to be a good developer for once~)
 if (inStr) {
-    if (ch === inStr && value.charCodeAt(i - 1) !== 92)
-        inStr = 0
+    if (ch === inStr) {
+        // Count consecutive backslashes before quote
+        let backslashes = 0
+        for (let j = i - 1; j >= 0 && value.charCodeAt(j) === 92; j--) {
+            backslashes++
+        }
+        // Even number of backslashes means quote is NOT escaped
+        if (backslashes % 2 === 0) inStr = 0
+    }
     continue
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/type-system/index.ts` around lines 446 - 450, The quote-termination check
incorrectly treats any single preceding backslash as an escape (using
value.charCodeAt(i - 1) !== 92), which fails for sequences like \\\"; update the
logic around variables inStr, ch, value and index i so you count the number of
consecutive backslashes immediately before the quote and only treat the quote as
escaped when that count is odd; replace the single-char check with a loop or
backward scan that computes backslashCount and then end the string (set inStr =
0) only if backslashCount % 2 === 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/type-system/index.ts`:
- Around line 446-450: The quote-termination check incorrectly treats any single
preceding backslash as an escape (using value.charCodeAt(i - 1) !== 92), which
fails for sequences like \\\"; update the logic around variables inStr, ch,
value and index i so you count the number of consecutive backslashes immediately
before the quote and only treat the quote as escaped when that count is odd;
replace the single-char check with a loop or backward scan that computes
backslashCount and then end the string (set inStr = 0) only if backslashCount %
2 === 0.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebad7910-2125-42da-b780-37fcecdf5e36

📥 Commits

Reviewing files that changed from the base of the PR and between 45a7293 and c44d130.

📒 Files selected for processing (1)
  • src/type-system/index.ts

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.

Weird behavior on Union schema with multiple Object on query

1 participant