Skip to content

Conversation

@MarcelOlsen
Copy link
Contributor

@MarcelOlsen MarcelOlsen commented Dec 23, 2025

Fix t.Record validation failure in response schemas

this resolves #1631

Problem

Response validation fails when using t.Record types, returning 422 validation errors even with valid data.

Example:

const app = new Elysia().get('/', () => ({
    list: [{
        toto: { bar: 1 },
        foo: { link: 'first' }
    }],
    one: {
        toto: { bar: 0 },
        foo: { link: 'second' }
    }
}), {
    response: {
        200: t.Object({
            list: t.Array(t.Object({
                toto: t.Object({ bar: t.Integer() }),
                foo: t.Record(t.String(), t.String())
            })),
            one: t.Object({
                toto: t.Object({ bar: t.Integer() }),
                foo: t.Record(t.String(), t.String())
            })
        })
    }
})

// Returns 422 with validation error
// Should return 200 with the data

Root Cause

The bug is in exact-mirror library - optionalsInArray cleanup state was bleeding across sibling structures, causing Record properties to be incorrectly removed.

Solution

Upstream Fix (exact-mirror)

Fixed in elysiajs/exact-mirror#26 by clearing optionalsInArray state after use, preventing pollution across sibling arrays/records.

Compatibility Changes (this PR)

src/schema.ts:

  • Updated createMirror calls to use TAnySchema type casts instead of TSchema (lines 543, 805, 944)
  • Ensures compatibility with exact-mirror's stricter type signature

test/validator/response.test.ts:

  • Added regression test for Record with nested objects (lines 566-593)
  • Ensures the original bug scenario stays fixed

Skip normalization for schemas with patternProperties to prevent Clean from removing valid Record properties
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

Walkthrough

Added an exported hasPatternProperties utility that recursively detects TypeBox patternProperties; updated validator/mirroring/cleaner selection and object normalization paths to conservatively avoid exact mirroring/forced additionalProperties=false when schemas contain patternProperties. Added a nested-Record response test.

Changes

Cohort / File(s) Summary
Schema detection & gating
src/schema.ts
Added export const hasPatternProperties(...) and updated getSchemaValidator and related flows to check for patternProperties before creating mirrors, forcing additionalProperties: false, or choosing normalization/cleaner strategies. Guards added around mirror creation and object reconstruction/merge.
Response validator tests
test/validator/response.test.ts
Added a test case validating nested t.Record/object responses to reproduce/cover the nested-record response scenario.

Sequence Diagram(s)

sequenceDiagram
  rect `#F6FAFF`
    participant Client
    participant Handler as "Request/Response Handler"
    participant SchemaUtil as "schema.ts: hasPatternProperties"
    participant Validator as "getSchemaValidator"
    participant Mirror as "exactMirror"
    participant Cleaner as "cleaner"
  end

  Client->>Handler: send request / expect response
  Handler->>SchemaUtil: inspect schema (hasPatternProperties?)
  SchemaUtil-->>Handler: true / false

  alt schema has patternProperties
    Handler->>Validator: request validator (guarded)
    Validator->>Cleaner: choose cleaner (avoid exactMirror)
    Cleaner-->>Validator: cleaned/validated payload
  else no patternProperties
    Handler->>Validator: request validator
    Validator->>Mirror: create exactMirror for normalization
    Mirror-->>Validator: normalized/mirrored payload
  end

  Validator->>Handler: validation result (ok / errors)
  Handler->>Client: return response or validation error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hopped through types with careful paws,
Sniffed patternProperties in their claws.
Mirrors paused where patterns play —
Clean responses now find their way.
🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly identifies the bug fix: validation failures for t.Record in response schemas, which directly aligns with the main changeset.
Linked Issues check ✅ Passed The PR successfully implements the solution for #1631 by detecting patternProperties (used by t.Record), skipping Clean function application, and adding regression tests that match the reported issue.
Out of Scope Changes check ✅ Passed All changes directly address the patternProperties/Record validation bug; no unrelated modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 23, 2025

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1634

commit: a22a463

Copy link
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

🧹 Nitpick comments (2)
src/schema.ts (1)

802-802: Consider simplifying the condition.

The condition checks both schema.additionalProperties === false and !hasPatternProperties(schema). However, Line 941 (compiled mode) only checks !hasPatternProperties(schema) without the additionalProperties condition.

For consistency and to ensure all schemas with patternProperties skip the Clean function, consider:

🔎 Suggested simplification
-			if (normalize && schema.additionalProperties === false && !hasPatternProperties(schema)) {
+			if (normalize && !hasPatternProperties(schema)) {

This ensures the Clean function is skipped for all schemas containing patternProperties, regardless of the additionalProperties setting.

test/validator/response.test.ts (1)

567-567: Update placeholder issue number.

The comment references issues/XXXX which appears to be a placeholder. Based on the PR objectives, this should reference issue #1631.

🔎 Suggested fix
-	// https://github.com/elysiajs/elysia/issues/XXXX
+	// https://github.com/elysiajs/elysia/issues/1631
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00ea802 and 0e494af.

📒 Files selected for processing (2)
  • src/schema.ts
  • test/validator/response.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
test/validator/response.test.ts (1)
test/utils.ts (1)
  • req (1-2)
🔇 Additional comments (5)
src/schema.ts (4)

720-721: LGTM: Correct guard for patternProperties.

The check correctly prevents setting additionalProperties = false when the schema contains patternProperties, which would conflict with the Record type's dynamic keys.


727-728: LGTM: Consistent patternProperties guards.

These checks correctly prevent forcing additionalProperties when patternProperties exist in the schema, maintaining compatibility with Record types.

Also applies to: 738-738


941-961: LGTM: Clean function gating in compiled mode.

The condition correctly skips Clean function creation when patternProperties exist, resolving the validation bug for Record schemas.


83-114: Fix correctly addresses exact-mirror's limitation with patternProperties.

The implementation properly prevents exact-mirror's Clean function from incorrectly removing Record properties by skipping normalization when patternProperties are detected. The hasPatternProperties function correctly identifies schemas using pattern-based dynamic keys through recursive traversal, including composition operators like anyOf, allOf, and nested object/array types.

test/validator/response.test.ts (1)

568-706: LGTM: Comprehensive regression test coverage.

The six new tests thoroughly cover Record validation scenarios:

  • Original bug with nested Record in arrays and objects
  • Simple and complex Record structures
  • Negative case (invalid data rejection)
  • Nested Records
  • Backward compatibility for regular Objects with additionalProperties=false

This provides excellent coverage for the bug fix and ensures no regressions.

- Fix schema.not handling (single schema, not array)
- Simplify Clean condition for consistency
- Replace 'as any' with @ts-expect-error in tests
- Remove unnecessary comments
Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (1)
src/schema.ts (1)

130-130: Fix: schema.not is a single schema, not an array.

Line 130 incorrectly treats schema.not as an array by calling .some() on it, which will cause a runtime error. According to JSON Schema specification and TypeBox, not accepts a single schema object, not an array.

This is the same bug that was fixed in hasPatternProperties at line 100. The correct pattern (used at line 329 in hasRef and line 377 in hasTransform) is to call the predicate function directly.

🔎 Proposed fix
-	if (schema.not) return schema.not.some(hasAdditionalProperties)
+	if (schema.not) return hasAdditionalProperties(schema.not)

Based on past review comments and the fix applied to hasPatternProperties.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e494af and 970a604.

📒 Files selected for processing (2)
  • src/schema.ts
  • test/validator/response.test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
test/validator/response.test.ts (1)
test/utils.ts (1)
  • req (1-2)
src/schema.ts (1)
src/type-system/index.ts (1)
  • TypeCheck (685-685)
🔇 Additional comments (5)
test/validator/response.test.ts (2)

566-616: LGTM! Excellent regression test for the original bug.

This test directly validates the fix for the issue where t.Record schemas nested in objects/arrays were causing 422 validation errors. The test covers both list items and nested objects with Record fields, which matches the reported bug scenario.


618-706: LGTM! Comprehensive test coverage for Record validation.

The test suite thoroughly covers:

  • Simple Record validation (618-630)
  • Records with complex object values (632-652)
  • Validation failure scenarios (654-666)
  • Deeply nested Records (668-688)
  • Backward compatibility ensuring regular Objects remain strictly validated (690-706)

The use of @ts-expect-error at lines 657 and 693 is appropriate for testing intentional validation failures and aligns with the PR's code quality improvements.

src/schema.ts (3)

83-114: LGTM! Well-implemented recursive pattern detection.

The hasPatternProperties function correctly:

  • Recursively traverses the schema tree (Import, unions, objects, arrays)
  • Line 100 correctly handles schema.not as a single schema (not an array), which addresses the bug mentioned in past review comments
  • Properly unwraps TypeCheck wrapper schemas

720-720: LGTM! Correct guards to prevent conflict between patternProperties and additionalProperties.

The checks at lines 720, 727-728, and 738 correctly prevent setting additionalProperties: false when patternProperties is present in the schema. This is essential because:

  • patternProperties allows dynamic keys matching a pattern
  • Setting additionalProperties: false would conflict with this behavior
  • These guards are placed at all key locations where additionalProperties is being set

Also applies to: 727-738


802-818: LGTM! Core fix for Record validation - correctly skips Clean when patternProperties present.

These guards (lines 802 and 941-961) implement the core solution described in the PR objectives:

  • Detect schemas containing patternProperties using hasPatternProperties()
  • Skip creating/applying the Clean function from exact-mirror for those schemas
  • This prevents the bug where Record properties (which use patternProperties) were being stripped during response normalization

The fix is applied in both:

  • Dynamic validation mode (line 802)
  • Compiled validation mode (lines 941-961)

This ensures backward compatibility: regular t.Object schemas still get Clean normalization, while schemas with patternProperties preserve their dynamic keys.

Also applies to: 941-961

@MarcelOlsen
Copy link
Contributor Author

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)

src/schema.ts (1)> 130-130: Fix: schema.not is a single schema, not an array.

Line 130 incorrectly treats schema.not as an array by calling .some() on it, which will cause a runtime error. According to JSON Schema specification and TypeBox, not accepts a single schema object, not an array.
This is the same bug that was fixed in hasPatternProperties at line 100. The correct pattern (used at line 329 in hasRef and line 377 in hasTransform) is to call the predicate function directly.

🔎 Proposed fix

-	if (schema.not) return schema.not.some(hasAdditionalProperties)
+	if (schema.not) return hasAdditionalProperties(schema.not)

Based on past review comments and the fix applied to hasPatternProperties.

📜 Review details

This is marked outside of the scope and the change has not been introduced in this PR, so this will not be getting addressed

@zoriya
Copy link
Contributor

zoriya commented Dec 24, 2025

just for information, i have routes that use t.Record without this issue so I'm not sure if this is really the issue.

i also don't think that disabling object cleaning when there's a record anywhere in the object is a correct fix. it should clean the object, just not the record.

@MarcelOlsen
Copy link
Contributor Author

MarcelOlsen commented Dec 25, 2025

Hmm, yeah, now that I think about it this does seem kinda too broad. Also, this might actually be a similar issue to what LASkuma was facing after all. Would you be able to check if this draft PR elysiajs/exact-mirror#26 fixes your issue? It seems to return the correct thing on my end @zoriya:

> curl http://localhost:3003/

{"list":[{"toto":{"bar":1},"foo":{"link":"first"}}],"one":{"toto":{"bar":0},"foo":{"link":"second"}}}%  

Thanks for the feedback, nice catch!

- Update createMirror calls to use TAnySchema instead of TSchema
- Add regression test for Record with nested objects
- Ensures compatibility with exact-mirror's stricter type signature
Copy link
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: 0

🧹 Nitpick comments (1)
src/schema.ts (1)

96-100: someOf is not a valid JSON Schema keyword—this check is dead code.

JSON Schema only defines anyOf, oneOf, allOf, and not. The someOf check on line 98 will never execute since schema.someOf is always undefined. While harmless, consider removing it for clarity.

🔎 Proposed fix
 	if (schema.anyOf) return schema.anyOf.some(hasPatternProperties)
 	if (schema.oneOf) return schema.oneOf.some(hasPatternProperties)
-	if (schema.someOf) return schema.someOf.some(hasPatternProperties)
 	if (schema.allOf) return schema.allOf.some(hasPatternProperties)
 	if (schema.not) return hasPatternProperties(schema.not)

Note: The same issue exists in hasAdditionalProperties (line 128) and hasElysiaMeta (line 236-238), but those are outside this PR's scope.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 970a604 and a22a463.

📒 Files selected for processing (2)
  • src/schema.ts
  • test/validator/response.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/validator/response.test.ts
🔇 Additional comments (5)
src/schema.ts (5)

83-95: Well-structured recursive helper.

The hasPatternProperties function correctly traverses the schema tree, handles TypeCheck wrappers, Import schemas, and composition keywords. The fix for schema.not (treating it as a single schema rather than an array) is correctly applied here.

Also applies to: 101-114


720-721: Correct guards to preserve Record schema semantics.

These checks prevent additionalProperties: false from being forced onto schemas containing patternProperties, which is essential for TypeBox Record schemas to work correctly with dynamic keys.

Also applies to: 727-729, 738-738


802-818: Correct guard for dynamic mode.

Skipping Clean assignment when hasPatternProperties returns true prevents the exact-mirror/Value.Clean from stripping valid Record keys. The fallback to createCleaner on exactMirror failure is preserved.


941-961: Correct guard for compiled mode.

Mirrors the dynamic mode fix, ensuring Clean is not created for schemas containing patternProperties. The different normalize modes (true, 'exactMirror', 'typebox') are all properly guarded.


543-547: Type cast for exact-mirror compatibility.

The TAnySchema cast aligns with the exact-mirror API expectations. Same pattern correctly applied at lines 805 and 944.

@SaltyAom SaltyAom merged commit df9c4cd into elysiajs:main Jan 3, 2026
3 checks passed
@SaltyAom
Copy link
Member

SaltyAom commented Jan 3, 2026

my bad, I accidentally click merge but I think it should not

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.

Request validation errors with specific schema structure

3 participants