Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ca3e8ec
fix(MergedSpecBuilder): merge path items by HTTP method instead of ov…
Picazsoo Jun 22, 2026
8972528
fix(kotlin-spring): fix Flow<String> and Flux<T> array return types f…
Picazsoo Jun 22, 2026
8aa4e27
Revert "fix(kotlin-spring): fix Flow<String> and Flux<T> array return…
Picazsoo Jun 22, 2026
818f196
fix(petstore): update operation IDs for pets endpoints to prevent col…
Picazsoo Jun 22, 2026
6d189e8
Merge branch 'master' into feature/merge-specs
Picazsoo Jun 30, 2026
6d1a54b
feat: add merge conflict strategy to MergedSpecBuilder for handling c…
Picazsoo Jun 30, 2026
fa2d521
feat: add merged file name and merge conflict strategy properties to …
Picazsoo Jun 30, 2026
6b16b12
feat: implement merge mode and conflict strategy options in MergedSpe…
Picazsoo Jun 30, 2026
f5f50a0
restore original behavior
Picazsoo Jun 30, 2026
aefbadd
update documentation
Picazsoo Jun 30, 2026
ff82951
escape asterisk
Picazsoo Jun 30, 2026
29e91ba
fix issue raised in CR
Picazsoo Jun 30, 2026
44bdaef
feat: enhance merge conflict handling and improve spec parsing logic
Picazsoo Jun 30, 2026
7a59aab
improve documentation
Picazsoo Jun 30, 2026
78d80be
feat: add support for merging explicit ordered list of spec files
Picazsoo Jun 30, 2026
fe3d29f
test: add comprehensive MergedSpecBuilder tests and fix path-level pa…
Picazsoo Jun 30, 2026
5ef1eec
refactor: rename inputSpecFilesOutputDir to mergedFileOutputDir for c…
Picazsoo Jun 30, 2026
e0ce9d0
feat: add inputSpecFiles explicit merge list support to Maven and CLI…
Picazsoo Jun 30, 2026
fcbf679
fix tests
Picazsoo Jun 30, 2026
13baea2
implement feedback from CR
Picazsoo Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ public class Generate extends OpenApiGeneratorCommand {
@Option(name = "--merged-spec-filename", title = "Name of resulted merged specs file (used along with --input-spec-root-directory option)")
private String mergedFileName;

@Option(name = "--merge-conflict-strategy", title = "Merge conflict strategy",
description = "Strategy when two specs define the same component/path+method with different definitions: WARN (default, keep first) or FAIL (abort). Only applies with --merge-mode DEEP.")
private String mergeConflictStrategy;

@Option(name = "--merge-mode", title = "Merge mode",
description = "How multiple spec files are merged: REF (default, original $ref-based) or DEEP (full inline merge with component deduplication).")
private String mergeMode;

@Option(name = "--input-spec-files", title = "Explicit spec files",
description = "Ordered list of spec files to merge (repeat the flag for each file). " +
"Alternative to --input-spec-root-directory; takes precedence when set.")
private List<String> inputSpecFiles;

@Option(name = "--merged-file-output-dir", title = "Merged file output directory",
description = "Directory where the merged spec file is written when --input-spec-files is used. Required when --input-spec-files is set.")
private String mergedFileOutputDir;

@Option(name = {"-t", "--template-dir"}, title = "template directory",
description = "folder containing the template files")
private String templateDir;
Expand Down Expand Up @@ -345,9 +362,64 @@ public class Generate extends OpenApiGeneratorCommand {

@Override
public void execute() {
if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
spec = new MergedSpecBuilder(inputSpecRootDirectory, StringUtils.isBlank(mergedFileName) ? "_merged_spec" : mergedFileName)
.buildMergedSpec();
if (inputSpecFiles != null && !inputSpecFiles.isEmpty()) {
if (StringUtils.isBlank(mergedFileOutputDir)) {
System.err.println("[error] --merged-file-output-dir must be set when --input-spec-files is used");
System.exit(1);
}

MergedSpecBuilder builder = new MergedSpecBuilder(
inputSpecFiles,
mergedFileOutputDir,
StringUtils.isBlank(mergedFileName) ? "_merged_spec" : mergedFileName
);

MergedSpecBuilder.MergeMode resolvedMergeMode = MergedSpecBuilder.MergeMode.REF;
if (StringUtils.isNotBlank(mergeMode)) {
try {
resolvedMergeMode = MergedSpecBuilder.MergeMode.valueOf(mergeMode.toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException e) {
System.err.println("[error] Invalid --merge-mode value '" + mergeMode + "'. Valid values are: REF, DEEP");
System.exit(1);
}
builder.withMergeMode(resolvedMergeMode);
}

if (resolvedMergeMode == MergedSpecBuilder.MergeMode.DEEP && StringUtils.isNotBlank(mergeConflictStrategy)) {
try {
builder.withConflictStrategy(MergedSpecBuilder.MergeConflictStrategy.valueOf(mergeConflictStrategy.toUpperCase(java.util.Locale.ROOT)));
} catch (IllegalArgumentException e) {
System.err.println("[error] Invalid --merge-conflict-strategy value '" + mergeConflictStrategy + "'. Valid values are: WARN, FAIL");
System.exit(1);
}
}

spec = builder.buildMergedSpec();
System.out.println("Merged input spec from explicit file list: " + spec);
} else if (StringUtils.isNotBlank(inputSpecRootDirectory)) {
MergedSpecBuilder builder = new MergedSpecBuilder(inputSpecRootDirectory, StringUtils.isBlank(mergedFileName) ? "_merged_spec" : mergedFileName);

MergedSpecBuilder.MergeMode resolvedMergeMode = MergedSpecBuilder.MergeMode.REF;
if (StringUtils.isNotBlank(mergeMode)) {
try {
resolvedMergeMode = MergedSpecBuilder.MergeMode.valueOf(mergeMode.toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException e) {
System.err.println("[error] Invalid --merge-mode value '" + mergeMode + "'. Valid values are: REF, DEEP");
System.exit(1);
}
builder.withMergeMode(resolvedMergeMode);
}

if (resolvedMergeMode == MergedSpecBuilder.MergeMode.DEEP && StringUtils.isNotBlank(mergeConflictStrategy)) {
try {
builder.withConflictStrategy(MergedSpecBuilder.MergeConflictStrategy.valueOf(mergeConflictStrategy.toUpperCase(java.util.Locale.ROOT)));
} catch (IllegalArgumentException e) {
System.err.println("[error] Invalid --merge-conflict-strategy value '" + mergeConflictStrategy + "'. Valid values are: WARN, FAIL");
System.exit(1);
}
}

spec = builder.buildMergedSpec();
System.out.println("Merge input spec would be used - " + spec);
}

Expand Down
16 changes: 13 additions & 3 deletions modules/openapi-generator-gradle-plugin/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,18 @@ apply plugin: 'org.openapi.generator'

|mergedFileName
|String / Provider<String>
|None
|Name of the file that will contain all merged specs
|`merged`
|Name of the output file that will contain all merged specs (used with `inputSpecRootDirectory`)

|mergeMode
|String / Provider<String>
|`REF`
|How multiple spec files are merged. `REF` (default) preserves the original behaviour: generates a lightweight index spec where each path is a `$ref` pointing back to its source file — identical to the master behaviour. `DEEP` fully parses and inlines all paths and components into a single self-contained spec, with component deduplication and conflict detection controlled by `mergeConflictStrategy`.

|mergeConflictStrategy
|String / Provider<String>
|`WARN`
|How to handle name conflicts when `mergeMode` is `DEEP` and two specs define the same component (schema, response, etc.) or the same path+method with different definitions. `WARN` (default) keeps the first definition and logs a warning. `FAIL` throws an exception and aborts the build.

|remoteInputSpec
|String / Provider<String>
Expand Down Expand Up @@ -248,7 +258,7 @@ apply plugin: 'org.openapi.generator'
|forcedGenerateSchemas
|List / Provider<List>
|None
|Forces generation of the named schemas even when they are listed in `schemaMappings` or `importMappings` (which would normally suppress their generation). The primary use case is producing a *reference copy* of a hand-written class — for example, a custom enum that replaces a generated one — so you can assert in a test that the hand-written version has not diverged from what the generator would produce. List of schema names, e.g. `["MyEnum", "OtherSchema"]`. Use `["*"]` as a wildcard to force-generate *all* schemas that would otherwise be suppressed by a mapping.
|Forces generation of the named schemas even when they are listed in `schemaMappings` or `importMappings` (which would normally suppress their generation). The primary use case is producing a *reference copy* of a hand-written class — for example, a custom enum that replaces a generated one — so you can assert in a test that the hand-written version has not diverged from what the generator would produce. List of schema names, e.g. `["MyEnum", "OtherSchema"]`. Use `["\*"]` as a wildcard to force-generate *all* schemas that would otherwise be suppressed by a mapping.

|nameMappings
|Map / Provider<Map>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
inputSpec.set(generate.inputSpec)
inputSpecRootDirectory.set(generate.inputSpecRootDirectory)
inputSpecRootDirectorySkipMerge.set(generate.inputSpecRootDirectorySkipMerge)
inputSpecFiles.from(generate.inputSpecFiles)
mergedFileOutputDir.set(generate.mergedFileOutputDir)
mergedFileName.set(generate.mergedFileName)
mergedFileInfoName.set(generate.mergedFileInfoName)
mergedFileInfoDescription.set(generate.mergedFileInfoDescription)
mergedFileInfoVersion.set(generate.mergedFileInfoVersion)
mergeMode.set(generate.mergeMode)
mergeConflictStrategy.set(generate.mergeConflictStrategy)
remoteInputSpec.set(generate.remoteInputSpec)
templateDir.set(generate.templateDir)
templateResourcePath.set(generate.templateResourcePath)
Expand Down Expand Up @@ -172,4 +180,3 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
}



Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package org.openapitools.generator.gradle.plugin.extensions

import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.kotlin.dsl.listProperty
import org.gradle.kotlin.dsl.mapProperty
Expand Down Expand Up @@ -73,13 +75,53 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
*/
val inputSpecRootDirectory: DirectoryProperty = project.objects.directoryProperty()

/**
* An explicit ordered list of spec files to merge.
*
* When set, the generator merges exactly these files in the given order, rather than scanning a
* directory. Use with [mergeMode] and [mergeConflictStrategy]. The merged output is written to
* [mergedFileOutputDir]. Takes precedence over [inputSpecRootDirectory] if both are set.
*/
val inputSpecFiles: ConfigurableFileCollection = project.objects.fileCollection()

/**
* Directory where the merged spec file is written when [inputSpecFiles] is used.
* Must be set when [inputSpecFiles] is non-empty.
*/
val mergedFileOutputDir: DirectoryProperty = project.objects.directoryProperty()

/**
* Skip bundling all spec files into a merged spec file, if true.
*
* Default false.
*/
val inputSpecRootDirectorySkipMerge = project.objects.property<Boolean>()

/**
* Name of the file that will contain all merged specs (used with inputSpecRootDirectory).
*
* Default: "merged"
*/
val mergedFileName = project.objects.property<String>()
val mergedFileInfoName = project.objects.property<String>()
val mergedFileInfoDescription = project.objects.property<String>()
val mergedFileInfoVersion = project.objects.property<String>()

/**
* How multiple spec files are merged. Accepted values: "REF" (default, original $ref-based
* shallow merge, backward-compatible) or "DEEP" (full inline merge with component
* deduplication and conflict detection).
*/
val mergeMode = project.objects.property<String>()

/**
* Strategy when two specs define the same component name or path+method with conflicting
* (non-identical) definitions. Accepted values: "WARN" (default, keep first definition and
* log a warning) or "FAIL" (throw an exception and abort the build). Only applies when
* mergeMode is "DEEP".
*/
val mergeConflictStrategy = project.objects.property<String>()

/**
* The remote Open API 2.0/3.x specification URL location.
*/
Expand Down Expand Up @@ -449,6 +491,12 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
fun applyDefaults() {
releaseNote.convention("Minor update")
inputSpecRootDirectorySkipMerge.convention(false)
mergedFileName.convention("merged")
mergedFileInfoName.convention("merged spec")
mergedFileInfoDescription.convention("merged spec")
mergedFileInfoVersion.convention("1.0.0")
mergeMode.convention("REF")
mergeConflictStrategy.convention("WARN")
modelNamePrefix.convention("")
modelNameSuffix.convention("")
apiNameSuffix.convention("")
Expand Down Expand Up @@ -482,7 +530,7 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
fun setInputSpec(path: String) {
if (path.isRemoteUri()) {
remoteInputSpec.set(path)
inputSpec.set(null as File?) // Clear local file to prevent conflicts
inputSpec.set(null as RegularFile?) // Clear local file to prevent conflicts
} else {
inputSpec.set(project.layout.projectDirectory.file(path))
remoteInputSpec.set(null as String?) // Clear remote URL to prevent conflicts
Expand Down
Loading
Loading