Skip to content

fix(core): extract named enums in nullable object compositions - #3564

Merged
melloware merged 2 commits into
orval-labs:masterfrom
wadakatu:fix/3563-nullable-object-anyof-enum-const
Jun 7, 2026
Merged

fix(core): extract named enums in nullable object compositions#3564
melloware merged 2 commits into
orval-labs:masterfrom
wadakatu:fix/3563-nullable-object-anyof-enum-const

Conversation

@wadakatu

@wadakatu wadakatu commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #3340 (PR #3560). Enum properties inside a nested object are now extracted into named as const consts when the object's nullability is spelled as the explicit OAS 3.1 composition anyOf: [{ object }, { type: "null" }] (and the analogous oneOf), matching how top-level, non-nullable nested, and type: ["object", "null"] objects already behave.

Before

export type TestMonthSelection = {
  months: ('JANUARY' | 'FEBRUARY' | 'MARCH')[];
  months2?: 'JANUARY' | 'FEBRUARY' | 'MARCH';
} | null;

After

export type TestMonthSelection = {
  months: TestMonthSelectionMonthsItem[];
  months2?: TestMonthSelectionMonths2;
} | null;
// + TestMonthSelectionMonthsItem / TestMonthSelectionMonths2 emitted as `... as const`

Why

#3340 fixed the type: ["object", "null"] shape, but the explicit anyOf/oneOf + null-member form takes a different path: it enters the combiner branch of getObject (packages/core/src/getters/object.ts) and is handed to combineSchemas, which resolves each member with combined: true and — under the v8 default aliasCombinedTypes: false — an undefined propName. The object member's getObject therefore receives name = undefined, and the enum-extraction guard in resolvers/object.ts is blocked (both propName empty and combined true), so the nested enums inline.

How

In the combiner branch, before delegating to combineSchemas, detect a "nullable object composition" — an anyOf/oneOf whose members are exactly one inline object with properties plus one or more null-type members — and divert that single object member to the normal property-iteration path with its name preserved and a synthesized | null. This mirrors the #3340 fix one level up.

The guard is deliberately narrow; these keep the existing combineSchemas behavior:

  • allOf (intersection semantics, not a nullable union)
  • $ref object members (e.g. anyOf: [{ $ref }, { null }]Ref | null; the referenced schema extracts at its own definition)
  • real multi-member unions ([{ object }, { string }, { null }])
  • primitive nullable unions ([{ string }, { null }])
  • empty-properties objects (kept as { [key: string]: unknown } | null)

The divert is unconditional w.r.t. aliasCombinedTypes, consistent with #3340 (which also drops the …AnyOf wrapper for the equivalent type-array shape).

Related precedent: #2710 / PR #3424 (isNullableEnumComposition) made nullable enum compositions transparent inside combineSchemas; this is the object-level analogue.

Tests

  • packages/core/src/generators/schema-definition.test.ts: parametrized regression test over anyOf and oneOf asserting the nested enums extract as named as const schemas and the object keeps its | null.
  • Full suite green: 1980 core unit tests, the snapshot suite (samples + orval-tests) with zero changes to existing generated output, typecheck, and lint.
  • Verified end-to-end via the CLI for both anyOf and oneOf, including aliasCombinedTypes: true; the generated schema files compile under tsc --strict.

Closes #3563

Summary by CodeRabbit

  • Bug Fixes
    • Fixed schema generation for nullable object compositions so nested enum properties are extracted and referenced rather than inlined. Generated types now preserve the nullable union (e.g., "... | null") and reuse named enum schemas, reducing duplication and improving clarity of produced schemas.

The explicit OAS 3.1 nullability form `anyOf|oneOf: [{ object }, { type:
null }]` routed through combineSchemas, which resolves the object member
with `combined: true` and an undefined propName. That dropped the schema
name, so nested enum properties were inlined as string-literal unions
instead of being extracted into named `as const` consts.

Divert the single inline object member to the property-iteration path
with its `name` preserved (and a synthesized ` | null`), mirroring the
orval-labs#3340 fix one level up. The guard is narrow: allOf, `$ref` object
members, real multi-member unions, primitive members, and empty objects
keep the existing combineSchemas behavior.

Closes orval-labs#3563
Copilot AI review requested due to automatic review settings June 7, 2026 16:43
@coderabbitai

coderabbitai Bot commented Jun 7, 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: 4e31ec80-8e9e-4303-a8af-2dd3276c089a

📥 Commits

Reviewing files that changed from the base of the PR and between 26b9e2d and fe46cb2.

📒 Files selected for processing (1)
  • packages/core/src/getters/object.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/getters/object.ts

📝 Walkthrough

Walkthrough

This PR fixes issue #3563 by extending OpenAPI schema composition handling. When anyOf or oneOf combines an object with explicit type: 'null', the schema generator now extracts the non-null object and processes its nested enum properties as named as const schemas instead of inlining literals, while preserving the nullable union type.

Changes

Nullable object composition and enum extraction

Layer / File(s) Summary
Nullable object composition detection in getObject
packages/core/src/getters/object.ts
Added detection logic for anyOf/oneOf patterns combining an inline object with a null member. Filters null members, validates that one object member with properties remains, then recursively processes the object member while appending | null to nullable, bypassing combineSchemas for this specific case.
Regression test for issue #3563
packages/core/src/generators/schema-definition.test.ts
Parameterized test covering anyOf and oneOf object-or-null compositions. Asserts that nested enum properties are extracted into named as const schemas and referenced by the composed type, which preserves | null without inlining enum literals.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

  • orval-labs/orval#3560: Handles the corresponding type: ['object', 'null'] nullable-object pattern with the same named enum extraction logic and regression test structure.

Suggested labels

bug, openapi

Suggested reviewers

  • melloware

Poem

🐰 A rabbit hops through schemas bright,
Finding nulls and objects in the night,
Now enums dance as constants true,
Named and extracted, fresh and new! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly corresponds to the main change: extracting named enums from nullable object compositions (anyOf/oneOf with object and null members).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes a regression where nullable objects expressed as OAS 3.1 anyOf/oneOf compositions with a { type: 'null' } member lose their name, causing nested enums to be inlined instead of emitted as named as const definitions.

Changes:

  • Adds a guard in getObject() to divert nullable { object } | null compositions away from combineSchemas() to preserve naming for nested enum extraction.
  • Adds a regression test covering both anyOf and oneOf nullable-object composition forms.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/core/src/getters/object.ts Detects { object-with-properties } | null composition and routes through the object property-iteration path to preserve name.
packages/core/src/generators/schema-definition.test.ts Adds regression coverage to ensure nested enums become named as const schemas for anyOf/oneOf nullable-object compositions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -211,6 +211,73 @@ export function getObject({
if (itemAllOf || itemOneOf || itemAnyOf) {
const separator = itemAllOf ? 'allOf' : itemOneOf ? 'oneOf' : 'anyOf';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in fe46cb2: members is now derived from separator (anyOf/oneOf only, undefined otherwise) so both paths inspect the same combiner and allOf never diverts.

Comment thread packages/core/src/getters/object.ts Outdated
// `allOf` is intersection, not a nullable union, so it is excluded; real
// unions, `$ref` object members, primitive members, and empty objects keep
// the combineSchemas behavior via the guard below.
const members = itemAnyOf ?? itemOneOf;
Comment thread packages/core/src/getters/object.ts Outdated
Comment on lines +242 to +243
const objectMembers = members.filter((member) => !isNullMember(member));
const objectMember = objectMembers[0];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to nonNullMembers/nonNullMember in fe46cb2 — agreed, the array holds all non-null members (incl. non-object and $ref schemas), and the object/$ref/properties checks narrow it afterwards.

Comment thread packages/core/src/getters/object.ts Outdated

const isNullableObjectComposition =
members.some(isNullMember) &&
objectMembers.length === 1 &&

@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

🤖 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/core/src/getters/object.ts`:
- Around line 225-226: The nullable-object shortcut is being applied by
selecting members via "const members = itemAnyOf ?? itemOneOf" without checking
the active combiner, which can run when separator === "allOf" (or pick the wrong
branch) and bypass combineSchemas; update the logic in the getter where members
is computed (the block using itemAnyOf, itemOneOf and separator around lines 225
and again at 257-279) to respect the active combiner: only use itemAnyOf when
separator === "anyOf" and only use itemOneOf when separator === "oneOf" (fall
back to null/undefined otherwise), and ensure the path then calls combineSchemas
when separator is a sibling-combiner (e.g., "allOf") so sibling constraints are
preserved.
🪄 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: d2fcb0ad-187d-4253-bc03-fede01d8cac6

📥 Commits

Reviewing files that changed from the base of the PR and between 064fb98 and 26b9e2d.

📒 Files selected for processing (2)
  • packages/core/src/generators/schema-definition.test.ts
  • packages/core/src/getters/object.ts

Comment thread packages/core/src/getters/object.ts Outdated
Select the composition `members` from the active `separator` instead of
`itemAnyOf ?? itemOneOf`, so the nullable-object shortcut and the
combineSchemas fallback always operate on the same combiner. This stops
the shortcut from firing on an `allOf`-primary schema (which would drop
sibling-combiner constraints) and from inspecting `anyOf` members when
`oneOf` is the active separator.

Also rename `objectMembers`/`objectMember` to `nonNullMembers`/
`nonNullMember`: the array holds every non-null member (including non-
object and `$ref` schemas), not only object members.
@melloware
melloware merged commit 77000de into orval-labs:master Jun 7, 2026
5 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.

Nested object enums inlined instead of as const when nullable via anyOf/oneOf: [{object}, {type: null}] (follow-up to #3340)

3 participants