Skip to content

feat: add array index support in nested multipart form parsing#5

Merged
Teyik0 merged 1 commit intomainfrom
claude/nested-image-multipart-support-eF4Rq
Jan 23, 2026
Merged

feat: add array index support in nested multipart form parsing#5
Teyik0 merged 1 commit intomainfrom
claude/nested-image-multipart-support-eF4Rq

Conversation

@Teyik0
Copy link
Owner

@Teyik0 Teyik0 commented Jan 23, 2026

Extended the nested object reconstruction to handle array indices in multipart form keys (e.g., images.create[0], items[1].name).

Changes:

  • Updated setNestedValue() to parse and handle array index notation [N]
  • Arrays are automatically initialized when indices are detected
  • Code generation in web-standard adapter updated for array support
  • Added comprehensive test for complex nested arrays with files

This enables schemas like:
t.Object({ images: t.Object({ create: t.Files(), update: t.Array(t.Object({ id: t.String(), img: t.File() })) }) })

All 49 body validator tests pass.


Summary by cubic

Adds support for array indices in multipart form fields when rebuilding nested bodies (e.g., images.create[0], items[1].name). This unlocks arrays and arrays of objects with files in body schemas without custom parsing.

  • New Features
    • Parse [index] notation in nested keys (e.g., a[0], a[1].b)
    • Auto-initialize arrays and objects during reconstruction
    • Update web-standard adapter codegen and add tests for nested arrays with files

Written for commit c816f5c. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Form data parsing now supports array-based field notation for handling complex nested form structures.
    • Improved handling of nested arrays containing multiple files in form submissions.
    • Enhanced validation for multi-level nested data structures with file uploads.

✏️ Tip: You can customize this high-level summary in your review settings.

Extended the nested object reconstruction to handle array indices in multipart
form keys (e.g., `images.create[0]`, `items[1].name`).

Changes:
- Updated setNestedValue() to parse and handle array index notation [N]
- Arrays are automatically initialized when indices are detected
- Code generation in web-standard adapter updated for array support
- Added comprehensive test for complex nested arrays with files

This enables schemas like:
  t.Object({
    images: t.Object({
      create: t.Files(),
      update: t.Array(t.Object({
        id: t.String(),
        img: t.File()
      }))
    })
  })

All 49 body validator tests pass.
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

The changes extend form data handling and nested value assignment to support array notation (e.g., foo[0], bar[2]) alongside existing dot notation, enabling proper parsing and assignment of complex nested structures with arrays in multipart/form-data and other form contexts.

Changes

Cohort / File(s) Summary
Form Data Array Notation Parsing
src/adapter/web-standard/index.ts
Extended form data parsing to detect keys containing . or [ characters; added array-aware path traversal that initializes arrays when missing, ensures array elements exist, and navigates into specific indices; applies value assignment logic within array-aware paths
Nested Value Assignment
src/dynamic-handle.ts
Introduced support for array-index paths (e.g., foo[0]) in the setNestedValue function; detects array notation in keys, initializes arrays and objects as needed, and places values at correct array indices; extended conditional checks to route both dot and bracket notation through nested assignment
Test Coverage
test/validator/body.test.ts
Added test case verifying complex nested structures with files: includes nested images object with create (array of files) and update (array of objects with id, img file, altText); validates proper parsing and response transformation with size calculations

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Arrays now nested, brackets understood,
Form data flows through arrays like it should!
From [0] to [1], the path takes flight,
Complex structures handled just right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for array index notation in nested multipart form data parsing, which is the core focus of all three modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/adapter/web-standard/index.ts">

<violation number="1" location="src/adapter/web-standard/index.ts:64">
P1: Potential prototype pollution vulnerability: The array key handling doesn't sanitize dangerous property names like `__proto__`, `constructor`, or `prototype`. A malicious form key like `__proto__[0]` or `items.__proto__[0]` could manipulate object prototypes. Consider adding a blocklist check before processing keys.</violation>
</file>

<file name="src/dynamic-handle.ts">

<violation number="1" location="src/dynamic-handle.ts:43">
P2: Missing bounds check on array index from user input. Consider adding a maximum index limit (e.g., `if (index > 10000) return` or throw an error) to prevent potential DoS via sparse arrays with extremely large indices like `items[999999999999]`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

`if(arrayMatch){` +
`const arrayKey=arrayMatch[1]\n` +
`const index=parseInt(arrayMatch[2],10)\n` +
`if(!(arrayKey in current))current[arrayKey]=[]\n` +
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 23, 2026

Choose a reason for hiding this comment

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

P1: Potential prototype pollution vulnerability: The array key handling doesn't sanitize dangerous property names like __proto__, constructor, or prototype. A malicious form key like __proto__[0] or items.__proto__[0] could manipulate object prototypes. Consider adding a blocklist check before processing keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/adapter/web-standard/index.ts, line 64:

<comment>Potential prototype pollution vulnerability: The array key handling doesn't sanitize dangerous property names like `__proto__`, `constructor`, or `prototype`. A malicious form key like `__proto__[0]` or `items.__proto__[0]` could manipulate object prototypes. Consider adding a blocklist check before processing keys.</comment>

<file context>
@@ -52,16 +52,35 @@ export const WebStandardAdapter: ElysiaAdapter = {
+					`if(arrayMatch){` +
+					`const arrayKey=arrayMatch[1]\n` +
+					`const index=parseInt(arrayMatch[2],10)\n` +
+					`if(!(arrayKey in current))current[arrayKey]=[]\n` +
+					`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
+					`if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
</file context>
Fix with Cubic


if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 23, 2026

Choose a reason for hiding this comment

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

P2: Missing bounds check on array index from user input. Consider adding a maximum index limit (e.g., if (index > 10000) return or throw an error) to prevent potential DoS via sparse arrays with extremely large indices like items[999999999999].

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/dynamic-handle.ts, line 43:

<comment>Missing bounds check on array index from user input. Consider adding a maximum index limit (e.g., `if (index > 10000) return` or throw an error) to prevent potential DoS via sparse arrays with extremely large indices like `items[999999999999]`.</comment>

<file context>
@@ -29,17 +29,50 @@ const setNestedValue = (
+
+		if (arrayMatch) {
+			const [, arrayKey, indexStr] = arrayMatch
+			const index = parseInt(indexStr, 10)
+
+			// Initialize array if needed
</file context>
Fix with Cubic

Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@src/adapter/web-standard/index.ts`:
- Around line 55-83: The traversal that builds nested objects/arrays (using
variables keys, lastKey, current, arrayMatch/lastArrayMatch, arrayKey, index,
finalValue) can break when an existing array element is a primitive; before
descending into current[arrayKey][index] or assigning current[k] as the new
current, ensure the target is an object (typeof === 'object' && current !==
null) and if it is missing or not an object replace it with {} (e.g., when
handling arrayMatch and lastArrayMatch check if current[arrayKey][index] exists
and is an object, otherwise set current[arrayKey][index]={}; similarly when
handling non-array keys ensure current[k] is an object before assigning
current=current[k]). This prevents primitive values from being treated as
containers during later dot/array-index traversals.

In `@src/dynamic-handle.ts`:
- Around line 32-75: The code crashes when an array slot contains a primitive
and later code tries to descend into it (e.g., items[0] = 1 then items[0].id);
in the keys loop and before descending into an array element you must coerce
non-objects to {}. Update the array branch where you do if
(!current[arrayKey][index]) current[arrayKey][index] = {} to instead check
whether typeof current[arrayKey][index] !== 'object' || current[arrayKey][index]
=== null (and set it to {} in that case), and apply the same defensive
coerce-before-descend logic for any place that assumes current[arrayKey][index]
is an object (the array handling in the main keys loop and before descending
into lastKey when arrayMatch is present).

Comment on lines +55 to +83
`if(key.includes('.')||key.includes('[')){` +
`const keys=key.split('.')\n` +
`const lastKey=keys.pop()\n` +
`let current=c.body\n` +
`for(const k of keys){` +
`const arrayMatch=k.match(/^(.+)\\[(\\d+)\\]$/)\n` +
`if(arrayMatch){` +
`const arrayKey=arrayMatch[1]\n` +
`const index=parseInt(arrayMatch[2],10)\n` +
`if(!(arrayKey in current))current[arrayKey]=[]\n` +
`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
`if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
`current=current[arrayKey][index]` +
`}else{` +
`if(!(k in current)||typeof current[k]!=='object'||current[k]===null)` +
`current[k]={}\n` +
`current=current[k]` +
`}` +
`}\n` +
`const lastArrayMatch=lastKey.match(/^(.+)\\[(\\d+)\\]$/)\n` +
`if(lastArrayMatch){` +
`const arrayKey=lastArrayMatch[1]\n` +
`const index=parseInt(lastArrayMatch[2],10)\n` +
`if(!(arrayKey in current))current[arrayKey]=[]\n` +
`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
`current[arrayKey][index]=finalValue` +
`}else{` +
`current[lastKey]=finalValue` +
`}` +
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden array-index traversal against primitives.

If a prior field sets arrayKey[index] to a primitive (e.g., items[0]=foo) and a later field uses items[0].id, current becomes a primitive and the next assignment throws. Consider ensuring the element is an object before descending.

🐛 Proposed fix
- `if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
+ `if(!current[arrayKey][index]||typeof current[arrayKey][index]!=='object'||current[arrayKey][index]===null)current[arrayKey][index]={}\n` +
🤖 Prompt for AI Agents
In `@src/adapter/web-standard/index.ts` around lines 55 - 83, The traversal that
builds nested objects/arrays (using variables keys, lastKey, current,
arrayMatch/lastArrayMatch, arrayKey, index, finalValue) can break when an
existing array element is a primitive; before descending into
current[arrayKey][index] or assigning current[k] as the new current, ensure the
target is an object (typeof === 'object' && current !== null) and if it is
missing or not an object replace it with {} (e.g., when handling arrayMatch and
lastArrayMatch check if current[arrayKey][index] exists and is an object,
otherwise set current[arrayKey][index]={}; similarly when handling non-array
keys ensure current[k] is an object before assigning current=current[k]). This
prevents primitive values from being treated as containers during later
dot/array-index traversals.

Comment on lines +32 to +75
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!

let current = obj
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)

if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)

// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []

// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []

// Initialize object at index if needed
if (!current[arrayKey][index]) current[arrayKey][index] = {}

current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}

current[lastKey] = value
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)

if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []

current[arrayKey][index] = value
} else {
current[lastKey] = value
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent crashes when array elements are non-objects.

If an array index already contains a primitive and a later path descends further (e.g., items[0]=1 then items[0].id), this will throw at assignment. Guard by coercing non-objects to {} before descending.

🐛 Proposed fix
-			if (!current[arrayKey][index]) current[arrayKey][index] = {}
+			if (
+				!current[arrayKey][index] ||
+				typeof current[arrayKey][index] !== 'object' ||
+				current[arrayKey][index] === null
+			)
+				current[arrayKey][index] = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!
let current = obj
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []
// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
// Initialize object at index if needed
if (!current[arrayKey][index]) current[arrayKey][index] = {}
current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}
current[lastKey] = value
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
current[arrayKey][index] = value
} else {
current[lastKey] = value
}
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!
let current = obj
for (const key of keys) {
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []
// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
// Initialize object at index if needed
if (
!current[arrayKey][index] ||
typeof current[arrayKey][index] !== 'object' ||
current[arrayKey][index] === null
)
current[arrayKey][index] = {}
current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
current[arrayKey][index] = value
} else {
current[lastKey] = value
}
🤖 Prompt for AI Agents
In `@src/dynamic-handle.ts` around lines 32 - 75, The code crashes when an array
slot contains a primitive and later code tries to descend into it (e.g.,
items[0] = 1 then items[0].id); in the keys loop and before descending into an
array element you must coerce non-objects to {}. Update the array branch where
you do if (!current[arrayKey][index]) current[arrayKey][index] = {} to instead
check whether typeof current[arrayKey][index] !== 'object' ||
current[arrayKey][index] === null (and set it to {} in that case), and apply the
same defensive coerce-before-descend logic for any place that assumes
current[arrayKey][index] is an object (the array handling in the main keys loop
and before descending into lastKey when arrayMatch is present).

@Teyik0 Teyik0 merged commit c217198 into main Jan 23, 2026
2 checks passed
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.

2 participants