Skip to content

feat(cli): add preserve-one-of-in-all-of OpenAPI setting - #17289

Merged
willkendall01 merged 4 commits into
mainfrom
devin/1785360578-preserve-one-of-in-all-of
Jul 30, 2026
Merged

feat(cli): add preserve-one-of-in-all-of OpenAPI setting#17289
willkendall01 merged 4 commits into
mainfrom
devin/1785360578-preserve-one-of-in-all-of

Conversation

@fern-api

@fern-api fern-api Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: N/A

A oneOf used as a member of an allOf is a union of object variants:

allOf: [oneOf: [A, B, C], S]   ≡   oneOf: [A & S, B & S, C & S]

The V3 importer flattens it instead — every variant's properties are merged into one object and marked optional — so the union never reaches the IR. The same oneOf in field position converts to an undiscriminatedUnion and renders a variant selector in the API reference, which is why the two positions behave differently today. Flattening also widens validation: { leafId, branchId } becomes representable even though the spec forbids it.

This adds an opt-in OpenAPI spec setting that distributes the allOf over the union instead:

api:
  specs:
    - openapi: ./openapi.yml
      settings:
        preserve-one-of-in-all-of: true

Because the IR is shared by docs and every SDK generator, this changes generated request shapes for affected schemas, so it is off by default rather than a global flip.

Before / after for the fixture added here (ExportRequest: allOf: [oneOf: [ExportLeaf, ExportBranch, ExportTree], {timeout, tag}]):

# default (unchanged)
ExportRequest => object [leafId, format, branchId, timeout, tag]

# preserve-one-of-in-all-of: true
ExportRequest => undiscriminatedUnion [
  { leafId,   format, timeout?, tag? },
  { branchId, format, timeout?, tag? },
  {           format, timeout?, tag? }
]

Changes Made

  • SchemaConverter.tryDistributeAllOfOverOneOf rewrites allOf: [oneOf: [A, B], S] as oneOf: [allOf: [A, S], allOf: [B, S]] before the existing allOf-merging path, so each variant keeps its own properties plus the shared ones. Union members with a discriminator are left alone, since a discriminated union only keeps its discriminator intact.
  • Plumbed preserve-one-of-in-all-of through the existing OpenAPI settings path (generators.yml schema + serialization, configuration loader, APIDefinitionSettings, ParseOpenAPIOptions) and the fern.yml (CLI v2) equivalents, mirroring inline-all-of-schemas. Defaults to false.
  • Regenerated generators-yml.schema.json and fern-yml.schema.json via pnpm jsonschema.
  • Added the allof-oneof-member fixture (setting on) and allof-oneof-member-default fixture (setting off) plus assertions, and the IR snapshots the V3 fixture suites generate for them.
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated

  • Manual testing completed

  • pnpm vitest run src/__test__/allof-oneof-member.test.ts in v3-importer-tests — 3 passed (union with the flag, flattened object without it, field-position union unchanged).

  • pnpm turbo run test for v3-importer-tests, configuration, configuration-loader, cli-v2, openapi-ir-parser, api-workspace-commons — all passed except v3-sdks > url-reference, which fails with TypeError: fetch failed because the sandbox has no network access to the remote spec it downloads.

  • pnpm fern:build, pnpm jsonschema, pnpm format:fix, biome check on the touched files — clean.


Open in Devin Review

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Adds an opt-in preserve-one-of-in-all-of setting that rewrites allOf: [oneOf: [A,B], S] into oneOf: [allOf: [A,S], allOf: [B,S]] before the existing allOf-merge path, plus plumbing through generators.yml/fern.yml schemas and fixtures. The plumbing looks consistent and the default keeps existing behavior. Two correctness gaps in the rewrite: sibling oneOf/anyOf and a bare required on the parent are silently discarded.

  • 🟡 2 warning(s)
  • 🔵 1 suggestion(s)

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-29T05:02:48Z).

Fixture main PR Delta
docs 271.4s (n=5) 239.8s (35 versions) -31.6s (-11.6%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-07-29T05:02:48Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-29 23:33 UTC

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-29T05:02:48Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 71s (n=5) N/A 52s -19s (-26.8%)
go-sdk square 143s (n=5) 253s (n=5) 103s -40s (-28.0%)
java-sdk square 225s (n=5) 235s (n=5) 202s -23s (-10.2%)
php-sdk square 61s (n=5) N/A 44s -17s (-27.9%)
python-sdk square 132s (n=5) 229s (n=5) 130s -2s (-1.5%)
ruby-sdk-v2 square 107s (n=5) 121s (n=5) 85s -22s (-20.6%)
rust-sdk square 213s (n=5) 184s (n=5) 281s +68s (+31.9%)
swift-sdk square 77s (n=5) 445s (n=5) 78s +1s (+1.3%)
ts-sdk square 148s (n=5) 159s (n=5) 174s +26s (+17.6%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-07-29T05:02:48Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-29 23:36 UTC

@willkendall01
willkendall01 merged commit 5f9fc80 into main Jul 30, 2026
222 checks passed
@willkendall01
willkendall01 deleted the devin/1785360578-preserve-one-of-in-all-of branch July 30, 2026 15:44
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