Skip to content

fix(mock): detect $ref in single-element allOf/oneOf/anyOf array items - #3421

Merged
melloware merged 1 commit into
orval-labs:masterfrom
jakiestfu:fix/array-items-ref-recursion-allof
May 22, 2026
Merged

fix(mock): detect $ref in single-element allOf/oneOf/anyOf array items#3421
melloware merged 1 commit into
orval-labs:masterfrom
jakiestfu:fix/array-items-ref-recursion-allof

Conversation

@jakiestfu

@jakiestfu jakiestfu commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds extractItemsRef helper to detect $ref whether direct on item.items or wrapped in a single-element allOf/oneOf/anyOf composition
  • Fixes recursion guard so self-referential array schemas emit [] instead of undefined[]
  • Normalizes wrapped items to { $ref } before resolveMockValue, preventing enum arrays from double-wrapping as SomeEnum[][]
  • Only single-element compositions are unwrapped; multi-element compositions still flow through the combine path

Reproduction

Self-referential schema producing undefined[]

components:
  schemas:
    Category:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        subcategories:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/Category'

Before fix: The recursion guard didn't fire because it only checked item.items.$ref (which is undefined when wrapped in allOf). The code recursed into combineSchemasMock, which eventually skipped the already-visited ref but left the parent array case generating Array.from(...).map(() => undefined) — producing undefined[].

After fix: extractItemsRef unwraps the single-element allOf and the recursion guard correctly returns [].

Enum array double-wrapping as PetStatus[][]

components:
  schemas:
    PetStatus:
      type: string
      enum: [available, pending, sold]
    Pet:
      type: object
      properties:
        name:
          type: string
        previousStatuses:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/PetStatus'

Before fix: The allOf-wrapped items went through the composition path, so the outer array case never received the enums flag back from resolveMockValue. It wrapped the already-correct faker.helpers.arrayElements(Object.values(PetStatus)) (which itself returns PetStatus[]) in an extra Array.from(...).map(...), yielding PetStatus[][].

After fix: The wrapped items are normalized to { $ref: '#/components/schemas/PetStatus' } before calling resolveMockValue, which resolves the enum through the normal $ref path, properly propagates the enums flag, and the array case returns the value directly without double-wrapping.

Test plan

  • Unit tests added for recursion guard with direct $ref, allOf, oneOf, anyOf wrappers
  • Negative test: multi-element allOf is not unwrapped
  • All existing tests pass (npx vitest run packages/mock/src/faker/getters/scalar.test.ts — 40 tests)

Summary by CodeRabbit

  • Bug Fixes

    • Improved mock generation for array schemas with circular references. Now detects references even when wrapped by single-element composed schemas (allOf/oneOf/anyOf), returns an empty array for circular cases, and avoids incorrect short-circuits that could produce wrong mocks.
  • Tests

    • Added tests covering array reference extraction and recursion prevention, including edge cases with nested composed schemas.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2bccdf83-8ae0-4c12-88d6-bcf1e83a7cb2

📥 Commits

Reviewing files that changed from the base of the PR and between b2862a4 and b62be12.

📒 Files selected for processing (2)
  • packages/mock/src/faker/getters/scalar.test.ts
  • packages/mock/src/faker/getters/scalar.ts

📝 Walkthrough

Walkthrough

Extracts $ref from array items (including single-element composed wrappers), uses the extracted ref for recursion short-circuits, normalizes items to a direct $ref before mock resolution, removes one ESLint suppression, and adds tests for circular and multi-element composed schemas.

Changes

Array Item $ref Extraction

Layer / File(s) Summary
$ref extraction helper and array items refactoring
packages/mock/src/faker/getters/scalar.ts
Added extractItemsRef to detect $ref in direct or single-element allOf/oneOf/anyOf wrappers; removed an obsolete @typescript-eslint/no-unsafe-call suppression; updated array branch to use extracted $ref for recursion guards and to normalize items to a direct $ref before calling resolveMockValue.
Circular reference test coverage
packages/mock/src/faker/getters/scalar.test.ts
Added Vitest cases asserting that arrays whose items are circular refs (direct $ref or single-element composed wrappers) return [], and that allOf with multiple elements does not prematurely short-circuit.

Sequence Diagram

sequenceDiagram
  participant getMockScalar
  participant extractItemsRef
  participant resolveMockValue
  getMockScalar->>extractItemsRef: extract $ref from items (direct or single-element composed)
  extractItemsRef-->>getMockScalar: return itemsRef or undefined
  alt itemsRef is visited
    getMockScalar->>getMockScalar: return [] (short-circuit)
  else itemsRef not visited
    getMockScalar->>resolveMockValue: pass normalized items (direct $ref when applicable)
    resolveMockValue-->>getMockScalar: return mock value
  end
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • orval-labs/orval#3350: Related fixes addressing circular $ref handling and recursion skipping in composed schema resolution.

Suggested labels

mock, bug

Suggested reviewers

  • melloware

"I nibbled through refs in curly stacks,
unwrapped the nests of allOf tracks,
guarded recursion with a careful hop,
mocked arrays now stop when cycles stop.
— a rabbit, twitching whiskers and a code-top"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 clearly and specifically describes the main change: detecting $ref in single-element allOf/oneOf/anyOf array items, which is the core fix applied in this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
packages/mock/src/faker/getters/scalar.test.ts (1)

665-744: ⚡ Quick win

Add one regression test for single-element wrapper + sibling keywords.

Please add a case like items: { allOf: [{ $ref: ... }], nullable: true } so wrapper normalization doesn’t silently strip sibling semantics in future edits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mock/src/faker/getters/scalar.test.ts` around lines 665 - 744, Add a
regression test in the existing describe block for getMockScalar that ensures
single-element wrapper plus sibling keywords are preserved: copy the pattern
used for the "returns [] when items is allOf with a single circular $ref" test
but make items = { allOf: [{ $ref: '`#/components/schemas/Foo`' }], nullable: true
} (using the same baseArg and item.name) and assert
expect(result.value).toBe('[]'); to ensure wrapper normalization doesn't strip
sibling semantics; place this next to the other single-element wrapper tests
referencing getMockScalar and baseArg.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mock/src/faker/getters/scalar.ts`:
- Around line 272-273: The code collapses a wrapped single-element items schema
to { $ref: itemsRef } and thus drops any sibling keywords (e.g., nullable,
default, constraints); change the assignment that builds resolvedItems to merge
the $ref into the original item.items instead of replacing it (e.g., produce a
new object that spreads item.items and adds $ref when itemsRef exists and
item.items lacks $ref). Apply the same merge fix to the analogous occurrence at
the other spot (the similar resolvedAdditionalItems/resolvedItems creation near
the second reference) so sibling keys are preserved.

---

Nitpick comments:
In `@packages/mock/src/faker/getters/scalar.test.ts`:
- Around line 665-744: Add a regression test in the existing describe block for
getMockScalar that ensures single-element wrapper plus sibling keywords are
preserved: copy the pattern used for the "returns [] when items is allOf with a
single circular $ref" test but make items = { allOf: [{ $ref:
'`#/components/schemas/Foo`' }], nullable: true } (using the same baseArg and
item.name) and assert expect(result.value).toBe('[]'); to ensure wrapper
normalization doesn't strip sibling semantics; place this next to the other
single-element wrapper tests referencing getMockScalar and baseArg.
🪄 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: a2a1fcf3-6520-4dd8-8858-8c7a4a49c7b1

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb7494 and 0b29c9e.

📒 Files selected for processing (2)
  • packages/mock/src/faker/getters/scalar.test.ts
  • packages/mock/src/faker/getters/scalar.ts

Comment thread packages/mock/src/faker/getters/scalar.ts
@jakiestfu
jakiestfu force-pushed the fix/array-items-ref-recursion-allof branch from 0b29c9e to b2862a4 Compare May 21, 2026 22:13
@jakiestfu
jakiestfu marked this pull request as draft May 21, 2026 22:21
@jakiestfu
jakiestfu force-pushed the fix/array-items-ref-recursion-allof branch 2 times, most recently from bd22b10 to 011df32 Compare May 21, 2026 22:35
@jakiestfu
jakiestfu marked this pull request as ready for review May 21, 2026 22:37
The recursion guard in the array case only checked for a direct `$ref`
on `item.items`, missing the common pattern where specs wrap the
reference in a single-element composition (e.g. `items: { allOf:
[{ $ref }] }`). This caused self-referential schemas to produce
`undefined[]` and enum arrays to double-wrap as `SomeEnum[][]`.

Add `extractItemsRef` helper that returns the underlying `$ref` whether
direct or wrapped in a single-element allOf/oneOf/anyOf, then normalize
the items before passing to `resolveMockValue`. Only single-element
compositions are unwrapped; multi-element compositions still flow
through the combine path.

Includes unit tests for the recursion guard across all composition
wrappers and a negative test for multi-element compositions.
@jakiestfu
jakiestfu force-pushed the fix/array-items-ref-recursion-allof branch from 011df32 to b62be12 Compare May 21, 2026 22:37
@jakiestfu

Copy link
Copy Markdown
Contributor Author

@melloware I think this is one of the last bugs that is preventing us at Turo from using the faker mocks. We might be interested in exploring support for generating mocks for models too, not just operations, that can come later.

@melloware

Copy link
Copy Markdown
Collaborator

Awesome I will check it tomorrow!

@melloware melloware added the mock Related to mock generation label May 21, 2026
@melloware

Copy link
Copy Markdown
Collaborator

@jakiestfu did you want to filter the issues and look for label=mock to see if this fixes any open issues or if you want to fix any of those open issues before the next release??

@jakiestfu

Copy link
Copy Markdown
Contributor Author

@melloware I will look to see if this solves any open issues and follow up with them.

Unfortunately, I likely will not be able to invest much time in tackling general orval issues unless they directly impact our ability to use the library, apologies.

@melloware melloware added this to the 8.12.3 milestone May 22, 2026
@melloware
melloware merged commit 4809bce into orval-labs:master May 22, 2026
5 checks passed
@melloware

Copy link
Copy Markdown
Collaborator

8.12.3 is published

@jakiestfu

Copy link
Copy Markdown
Contributor Author

8.12.3 is published

So fast, @melloware, thank you so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mock Related to mock generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants