Skip to content

feat(spec-merge): add alternative DEEP merge mode with by-method path merging and configurable conflict resolution strategy#24100

Merged
wing328 merged 29 commits into
OpenAPITools:masterfrom
Picazsoo:feature/merge-specs
Jul 14, 2026
Merged

feat(spec-merge): add alternative DEEP merge mode with by-method path merging and configurable conflict resolution strategy#24100
wing328 merged 29 commits into
OpenAPITools:masterfrom
Picazsoo:feature/merge-specs

Conversation

@Picazsoo

@Picazsoo Picazsoo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Note: This change should be fully backwards compatible as it is entirely opt-in for now.

Problem

When merging multiple OpenAPI specs, the previous MergedSpecBuilder overwrote a path
whenever two specs defined the same URL path. If one spec defined GET /pets and another
defined POST /pets, one of the operations was silently lost.

What changed

  • Merge paths by HTTP method: colliding paths now combine their operations
    (e.g. GET from one file + POST from another) instead of overwriting.
  • Path-level data preserved: path-level parameters, servers, and x- vendor
    extensions are merged (first definition wins on conflict); server URLs are de-duplicated.
  • Only real spec files scanned: only .yaml, .yml, .json are processed, in a
    deterministic (sorted) order; other files are ignored.
  • Explicit ordered file list: you can now pass an ordered list of spec files to merge
    (with an output dir) across CLI, Gradle, and Maven, instead of only scanning a directory.

Why the new mergeMode and mergeConflictStrategy

Two merge modes exist so behavior stays backward-compatible while still allowing a real merge:

  • mergeMode

    • REF (default): the original behavior - produces a lightweight index spec where each
      path is a $ref back to its source file. Kept as default so existing setups don't change.
    • DEEP: fully inlines all paths and components into one self-contained spec, with component
      deduplication and the by-method path merging described above.
  • mergeConflictStrategy (only applies in DEEP mode)
    When two specs define the same component name (schema, response, etc.) with different
    definitions, we need a policy:

    • WARN (default): keep the first definition and log a warning.
    • FAIL: abort the merge. Useful in CI to catch accidental conflicts early.

    Note: identical duplicate components are silently de-duplicated. Duplicate path+method
    definitions always fail, since there is no valid reason for the same method on the same path
    to be defined twice.

Where it's exposed

  • CLI: --merge-mode, --merge-conflict-strategy, --input-spec-files, --merged-file-output-dir
  • Gradle: mergeMode, mergeConflictStrategy, inputSpecFiles, mergedFileOutputDir
  • Maven: mergeMode, mergeConflictStrategy, inputSpecFiles, mergedFileOutputDir

Invalid values fail fast with a clear error. Docs and tests updated accordingly.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Adds a DEEP merge mode that merges paths by HTTP method, preserves path-level data, resolves external $refs for a self-contained spec, and uses a configurable conflict strategy. Also supports ordered input spec lists and root metadata merging across CLI, Gradle, and Maven, while keeping REF as the original $ref-based mode.

  • Bug Fixes

    • Merge path items by method on collisions; detect duplicate path+method and apply mergeConflictStrategy in DEEP; REF keeps last.
    • Preserve path-level parameters and vendor extensions; dedupe params by $ref; dedupe servers and add http://localhost:8080 if none.
    • Resolve external $refs into components in DEEP; CLI forwards --auth for authenticated refs.
    • Propagate root-level security to operations unless overridden; warn-and-rename duplicate operationIds.
    • Merge root-level tags, externalDocs, webhooks (first-wins) and components.pathItems (OAS 3.1).
    • Scan only .yaml, .yml, .json deterministically; respect inputSpecFiles order; keep JSON if the first valid spec is JSON.
    • Validate --merge-conflict-strategy regardless of mode; Gradle cleans output before writing; when inputSpecFiles is set, skip directory scan; fail fast on empty.
    • Validate OpenAPI version in DEEP: enforce strict MAJOR.MINOR.PATCH semver, reject missing/malformed values (e.g., 3.0, 3.0.3.1, leading zeroes), and prevent mixing different major versions with clear errors.
  • New Features

    • Add mergeMode (REF, DEEP) and mergeConflictStrategy (WARN, FAIL) in CLI (--merge-mode, --merge-conflict-strategy), Gradle, and Maven; invalid values fail fast.
    • Support an explicit ordered list of spec files to merge across CLI (--input-spec-files with --merged-file-output-dir), Gradle (inputSpecFiles, mergedFileOutputDir), and Maven (inputSpecFiles, mergedFileOutputDir); order overrides directory scanning.
    • Add merged spec Info fields on all three (mergedFileInfoName, mergedFileInfoDescription, mergedFileInfoVersion); Gradle also adds mergedFileName (defaults to merged).

Written for commit e0737b2. Summary will update on new commits.

Review in cubic

@Picazsoo Picazsoo marked this pull request as ready for review June 30, 2026 11:00
@Picazsoo Picazsoo marked this pull request as draft June 30, 2026 11:01

@cubic-dev-ai cubic-dev-ai 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.

1 issue found and verified against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@Picazsoo Picazsoo marked this pull request as ready for review June 30, 2026 13:43
@Picazsoo Picazsoo marked this pull request as draft June 30, 2026 13:44

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/config/MergedSpecBuilder.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/config/MergedSpecBuilder.java:356">
P1: Deep merge path+method conflict detection is unconditional: it treats any duplicate method as a conflict without comparing whether the incoming operation is identical to the existing one. In FAIL mode this throws RuntimeException even for identical operations, which is inconsistent with component merging (where identical duplicates are silently deduplicated via Objects.equals) and contradicts documented intent of only warning on "different definitions".</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return;
}
incoming.readOperationsMap().forEach((method, operation) -> {
if (existing.readOperationsMap() != null && existing.readOperationsMap().containsKey(method)) {

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.

P1: Deep merge path+method conflict detection is unconditional: it treats any duplicate method as a conflict without comparing whether the incoming operation is identical to the existing one. In FAIL mode this throws RuntimeException even for identical operations, which is inconsistent with component merging (where identical duplicates are silently deduplicated via Objects.equals) and contradicts documented intent of only warning on "different definitions".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/config/MergedSpecBuilder.java, line 356:

<comment>Deep merge path+method conflict detection is unconditional: it treats any duplicate method as a conflict without comparing whether the incoming operation is identical to the existing one. In FAIL mode this throws RuntimeException even for identical operations, which is inconsistent with component merging (where identical duplicates are silently deduplicated via Objects.equals) and contradicts documented intent of only warning on "different definitions".</comment>

<file context>
@@ -99,51 +186,238 @@ public String buildMergedSpec() {
+            return;
+        }
+        incoming.readOperationsMap().forEach((method, operation) -> {
+            if (existing.readOperationsMap() != null && existing.readOperationsMap().containsKey(method)) {
+                String message = String.format(Locale.ROOT,
+                        "Path+method collision during spec merge: %s %s is defined in multiple specs with different definitions. Keeping the first definition.",
</file context>

@Picazsoo Picazsoo Jun 30, 2026

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.

This is on purpose - schema reuse across multiple specs (e.g. sharing from some common-schemas.yaml file is expected and the merging should be able to handle that. Having POST operation defined for the same path in multiple specs is unexpected. But having POST operation for a path and PUT operation for the same path in different specs is expected and handled correctly. The documentation now tries to explain it clearly

@Picazsoo Picazsoo changed the title fix(MergedSpecBuilder): merge path items by HTTP method instead of overwriting on path collision feat(spec-merge): add alternative DEEP merge mode with by-method path merging and configurable conflict resolution strategy Jul 13, 2026
@Picazsoo Picazsoo marked this pull request as ready for review July 13, 2026 10:42

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 22 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread modules/openapi-generator-gradle-plugin/src/test/kotlin/TestBase.kt
@Picazsoo Picazsoo marked this pull request as draft July 13, 2026 12:40
- MergedSpecBuilder: resolve external \ into components for self-contained DEEP output; normalize output dir before relativize; propagate root-level security onto operations; apply conflictStrategy to path+method conflicts; detect/rename duplicate operationIds; use \ as parameter identity when deduping path params.
- CLI Generate: extract shared applyMergeOptions and validate --merge-conflict-strategy regardless of merge mode.
- Gradle GenerateTask: skip root-directory merge when inputSpecFiles is set (explicit list precedence).
- TestBase: correct toPropertyReference KDoc.
- Add merger test specs and tests for the above.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo Picazsoo marked this pull request as ready for review July 13, 2026 16:18

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 34 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

… & pathItems

- MergedSpecBuilder: merge root tags/externalDocs/webhooks (first-wins) and components.pathItems (3.1)
- CLI Generate: pass --auth into merge builders so DEEP external-\ resolution honors credentials
- Gradle GenerateTask: track inputSpecFiles order as @input; run cleanupOutput before merge writes
- Tests: yaml/json FAIL operationId variants + direct mergeSpecs metadata/pathItems test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

- Fail when a source omits the required openapi field while others declare one (previously null versions were filtered out and silently accepted)
- Reject malformed versions (e.g. 3.x.3) instead of normalizing non-numeric segments to 0
- Add tests for both cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Tighten version validation so malformed values like '3.0', '3.0.3.1', or
those with leading zeroes are rejected instead of being emitted as the
merged document's openapi value. Extend the malformed-version test to cover
these forms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wing328 wing328 merged commit 7825e58 into OpenAPITools:master Jul 14, 2026
18 checks passed
@wing328 wing328 added this to the 7.24.0 milestone Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants