Skip to content

fix(mock): avoid double-wrapping null branch for required nullable scalars - #3501

Merged
melloware merged 1 commit into
orval-labs:masterfrom
wadakatu:fix/mock-3484-double-null-wrap
May 31, 2026
Merged

fix(mock): avoid double-wrapping null branch for required nullable scalars#3501
melloware merged 1 commit into
orval-labs:masterfrom
wadakatu:fix/mock-3484-double-null-wrap

Conversation

@wadakatu

@wadakatu wadakatu commented May 31, 2026

Copy link
Copy Markdown
Contributor

What

Fixes the double-wrapped null branch in faker/MSW mocks for required nullable scalar properties.

A required property typed as an OpenAPI 3.1 nullable union (type: [<scalar>, 'null']) — the same shape an OpenAPI 3.0 nullable: true scalar is upgraded to by @scalar/openapi-parser before mock generation — was wrapped with a null branch twice, pushing null to ~75% instead of ~50%:

// before
tag: faker.helpers.arrayElement([
  faker.helpers.arrayElement([faker.string.alpha(), null]),
  null,
])
// after
tag: faker.helpers.arrayElement([faker.string.alpha(), null])

Why

Two layers each detected the null union independently and each added a branch:

  • packages/mock/src/faker/getters/scalar.ts wraps the leaf value via getNullable → inner arrayElement([value, null]).
  • packages/mock/src/faker/getters/object.ts re-checks the property type and wraps the already-wrapped value again → outer arrayElement([..., null]).

How

Let the scalar getter own the null branch. It now flags the returned MockDefinition with a new optional nullWrapped field when it has already applied getNullable, and the object property layer skips its own wrap when that flag is set. Boolean (and number enum/const, where the scalar getter does not keep the wrap) stay bare, so the object layer still contributes their single null branch — no behavior change for those.

Only the required path is changed; the optional nullable branch (arrayElement([value, undefined]) / arrayElement([value, null]) for omit) is intentionally untouched, as its value | null | undefined nesting is a valid distribution.

Tests

  • New regression spec tests/specifications/issue-3484.yaml (required nullable string / integer / string-enum / boolean) wired into mock.config.ts, with a focused assertion in api-generation.spec.ts.
  • Two existing snapshots that already exhibited the bug now collapse to a single wrap: default/all-of (category) and mock/recursive-discriminator-allof (BaseProp).
  • format:check, build, typecheck, lint, test, test:snapshots, and the generated-output typecheck + mock verification (orval-tests build) all pass.

Closes #3484

Summary by CodeRabbit

  • Bug Fixes

    • Fixed double-wrapped null handling in generated mocks so nullable scalars now produce a single null choice and redundant null-wrapping is removed.
  • Tests

    • Added a regression test ensuring single null-branch behavior and updated/added generated mock snapshots.
  • Chores

    • Added a mock generation config entry and a new OpenAPI fixture to reproduce and validate the regression.
  • Samples

    • Updated generated MSW sample mocks to reflect corrected nullable-value selection.

Copilot AI review requested due to automatic review settings May 31, 2026 17:17
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 9ea3abbc-4fbe-4b91-a8c9-8a0a7537be93

📥 Commits

Reviewing files that changed from the base of the PR and between 05ffb3e and f6b14a6.

📒 Files selected for processing (27)
  • packages/mock/src/faker/getters/object.ts
  • packages/mock/src/faker/getters/scalar.ts
  • packages/mock/src/faker/resolvers/value.ts
  • packages/mock/src/types.ts
  • samples/angular-app/__snapshots__/api/endpoints-zod/pets/pets.msw.ts
  • samples/angular-app/__snapshots__/api/http-both/pets/pets.msw.ts
  • samples/angular-app/__snapshots__/api/http-client-custom-params/pets/pets.msw.ts
  • samples/angular-app/__snapshots__/api/http-client/pets/pets.msw.ts
  • samples/angular-app/__snapshots__/api/http-resource-zod/pets/pets.msw.ts
  • samples/angular-app/__snapshots__/api/http-resource/pets/pets.msw.ts
  • samples/angular-app/src/api/endpoints-zod/pets/pets.msw.ts
  • samples/angular-app/src/api/http-both/pets/pets.msw.ts
  • samples/angular-app/src/api/http-client-custom-params/pets/pets.msw.ts
  • samples/angular-app/src/api/http-client/pets/pets.msw.ts
  • samples/angular-app/src/api/http-resource-zod/pets/pets.msw.ts
  • samples/angular-app/src/api/http-resource/pets/pets.msw.ts
  • samples/angular-query/__snapshots__/api/endpoints/pets/pets.msw.ts
  • samples/angular-query/src/api/endpoints/pets/pets.msw.ts
  • tests/__snapshots__/default/all-of/endpoints.ts
  • tests/__snapshots__/mock/issue-3484/endpoints.ts
  • tests/__snapshots__/mock/issue-3484/model/index.ts
  • tests/__snapshots__/mock/issue-3484/model/pet.ts
  • tests/__snapshots__/mock/issue-3484/model/petKind.ts
  • tests/__snapshots__/mock/recursive-discriminator-allof/endpoints.ts
  • tests/api-generation.spec.ts
  • tests/configs/mock.config.ts
  • tests/specifications/issue-3484.yaml
✅ Files skipped from review due to trivial changes (11)
  • tests/snapshots/mock/issue-3484/model/pet.ts
  • tests/snapshots/mock/issue-3484/model/index.ts
  • tests/snapshots/mock/issue-3484/model/petKind.ts
  • samples/angular-app/snapshots/api/http-resource-zod/pets/pets.msw.ts
  • samples/angular-query/snapshots/api/endpoints/pets/pets.msw.ts
  • samples/angular-app/src/api/http-client/pets/pets.msw.ts
  • tests/snapshots/default/all-of/endpoints.ts
  • samples/angular-app/snapshots/api/http-resource/pets/pets.msw.ts
  • samples/angular-app/src/api/endpoints-zod/pets/pets.msw.ts
  • tests/snapshots/mock/recursive-discriminator-allof/endpoints.ts
  • samples/angular-app/src/api/http-client-custom-params/pets/pets.msw.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/configs/mock.config.ts
  • packages/mock/src/faker/getters/object.ts
  • packages/mock/src/faker/resolvers/value.ts
  • tests/api-generation.spec.ts
  • tests/snapshots/mock/issue-3484/endpoints.ts
  • packages/mock/src/types.ts
  • packages/mock/src/faker/getters/scalar.ts

📝 Walkthrough

Walkthrough

Fixes double-wrapped null branches in OpenAPI 3.1 nullable scalar mocks. Adds nullWrapped flag to MockDefinition to track when scalars already embed null handling, preventing the object layer from applying duplicate arrayElement([..., null]) wrapping. Includes regression test for issue #3484.

Changes

Fix double-wrapped null branches in nullable scalar mocks

Layer / File(s) Summary
MockDefinition null-wrapping flag
packages/mock/src/types.ts
MockDefinition interface gains optional nullWrapped?: boolean to indicate a mock value already includes a null branch.
Scalar nullable-detection and marking
packages/mock/src/faker/getters/scalar.ts
getMockScalar computes isNullable/nullWrapped from type: [..., 'null'] and attaches nullWrapped to scalar MockDefinition returns across format/binary/number/string paths, with special handling for enum/const branches.
Schema factory delegation null-wrapping
packages/mock/src/faker/resolvers/value.ts
When delegating to schema factories, resolveMockValue propagates nullWrapped so referenced schemas signal nullable returns.
Object property double-wrap guard
packages/mock/src/faker/getters/object.ts
getMockObject only applies property-level nullable arrayElement([... , null]) when resolvedValue.nullWrapped is not set, preventing duplicate wrapping.
Regression test config and spec
tests/configs/mock.config.ts, tests/specifications/issue-3484.yaml
Adds an Orval mock generator config entry and OpenAPI 3.1 fixture (/pet) exercising required nullable scalar unions.
Issue 3484 snapshot models
tests/__snapshots__/mock/issue-3484/model/*.ts
Generated Pet interface and PetKind nullable enum type used by the regression snapshot.
Issue 3484 snapshot endpoints and mocks
tests/__snapshots__/mock/issue-3484/endpoints.ts
Generated MSW endpoint module, response types, getPet helper, and getGetPetResponseMock faker factory that emits single null branches.
Regression test assertion
tests/api-generation.spec.ts
Vitest regression asserting generated getGetPetResponseMock has only a single null branch for required nullable scalars (no nested arrayElement([...arrayElement(...), null])).
Snapshot and sample updates
samples/**, tests/__snapshots__/**
Regenerated and updated many snapshots and sample MSW mock files as side effects of the fix and snapshot regeneration.

Sequence Diagram

sequenceDiagram
    participant Spec as OpenAPI Spec (type: [string, null])
    participant ScalarGetter as getMockScalar
    participant ObjectGetter as getMockObject
    participant Result as Mock result

    rect rgba(255, 0, 0, 0.5)
    Note over Spec,Result: OLD BEHAVIOR (Double-wrapped)
    Spec->>ScalarGetter: Schema shows null-union
    ScalarGetter->>ScalarGetter: Wraps value -> arrayElement([value, null])
    ScalarGetter->>ObjectGetter: Returns resolvedValue (wrapped)
    ObjectGetter->>ObjectGetter: Detects null-union again
    ObjectGetter->>ObjectGetter: Wraps again -> arrayElement([wrappedValue, null])
    ObjectGetter->>Result: Produces nested arrayElement([arrayElement([value, null]), null])
    end

    rect rgba(0, 255, 0, 0.5)
    Note over Spec,Result: NEW BEHAVIOR (Single wrap with flag)
    Spec->>ScalarGetter: Schema shows null-union
    ScalarGetter->>ScalarGetter: Wraps value and sets nullWrapped=true
    ScalarGetter->>ObjectGetter: Returns { value, nullWrapped: true }
    ObjectGetter->>ObjectGetter: Sees nullWrapped and skips wrapping
    ObjectGetter->>Result: Produces arrayElement([value, null])
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • orval-labs/orval#3426: Related changes to resolver delegation in packages/mock/src/faker/resolvers/value.ts.
  • orval-labs/orval#3248: Touches binary/contentMediaType handling in scalar mock generation relevant to nullWrapped propagation.
  • orval-labs/orval#3476: Prior adjustments to nullable-wrapping logic in the mock pipeline.

Suggested labels

mock, msw, bug

Suggested reviewers

  • melloware

🐰 A double null was curling tight,
We flagged it once to make it right.
Scalar says "I've got this, friend",
Object nods — no extra bend.
One null branch now, peace again!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 PR title accurately describes the main fix: avoiding double-wrapped null branches for required nullable scalars, which is the core bug addressed in the changeset.
Linked Issues check ✅ Passed All code changes directly address issue #3484: scalar getters now set nullWrapped flag to prevent re-wrapping at object property layer, with comprehensive test coverage including regression spec and snapshot updates.
Out of Scope Changes check ✅ Passed All changes are within scope of #3484 fix. While many snapshot files were updated, they reflect the expected output from the null-wrapping logic fix and are not out-of-scope.

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

@wadakatu
wadakatu marked this pull request as draft May 31, 2026 17:20
@wadakatu
wadakatu force-pushed the fix/mock-3484-double-null-wrap branch from 885c559 to 05ffb3e Compare May 31, 2026 17:25
@wadakatu
wadakatu marked this pull request as ready for review May 31, 2026 17:26
…alars

A required property typed as an OpenAPI 3.1 nullable union
(`type: [<scalar>, 'null']`) — the same shape OAS 3.0 `nullable: true` is
upgraded to by @scalar/openapi-parser — was wrapped with a `null` branch
twice in faker/MSW mocks:

    faker.helpers.arrayElement([
      faker.helpers.arrayElement([faker.string.alpha(), null]),
      null,
    ])

The scalar getter (`getNullable`) and the object property layer each
detected the null union independently and each added a branch, pushing
`null` to ~75% instead of ~50%.

Let the scalar getter own the null branch: it now flags the returned
`MockDefinition` with `nullWrapped` when it has already wrapped the value,
and the object property layer skips its own wrap in that case. Boolean
(and number enum/const) stay bare in the scalar getter, so the object
layer still contributes their single null branch.

Closes orval-labs#3484
@wadakatu
wadakatu force-pushed the fix/mock-3484-double-null-wrap branch from 05ffb3e to f6b14a6 Compare May 31, 2026 19:28
@melloware
melloware merged commit 9f8c350 into orval-labs:master May 31, 2026
6 checks passed
@Hypenate

Hypenate commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Good catch!

I ran into this issue at work :D
Looking forwards to a new release (again?!)

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.

Mock: OpenAPI 3.1 type: [..., 'null'] is double-wrapped with null in faker mocks (arrayElement([arrayElement([value, null]), null]))

3 participants