Skip to content

[BUG] Cross-module schema contamination with mvnd (Maven Daemon) parallel builds since swagger-parser 2.1.24 #23321

@sleicht

Description

@sleicht

Title: [BUG] Cross-module schema contamination with mvnd (Maven Daemon) parallel builds since swagger-parser 2.1.24

Description

When using openapi-generator-maven-plugin with mvnd (Maven Daemon) and parallel builds (-T1C), schemas from one module contaminate another module's generated code. The bug manifests when two independent modules define schemas with case-near-colliding names (e.g. CountryDto vs CountryDTO).

What happened

Module A defines CountryDto with fields identifier, label.
Module B defines CountryDTO with fields code, name.

After several builds with a warm mvnd daemon, module A's generated CountryDto.java sometimes contains:

  • File named CountryDto.java but class is CountryDTO
  • Fields code, name (from module B's schema) instead of identifier, label
  • Correct package (module A's package), wrong class content (module B's schema)

The issue reproduces almost every time with a warm daemon but never on the first build after daemon restart, and never with mvnd clean test-compile (only mvnd clean compile).

What was expected

Each module generates code exclusively from its own OpenAPI spec, regardless of parallel execution or daemon state.

Root cause analysis

The issue was introduced in swagger-parser 2.1.24 and affects all openapi-generator versions that bundle swagger-parser ≥2.1.24 (i.e. openapi-generator 7.14.0+).

swagger-parser 2.1.24 introduced two changes:

  1. ResolverFully response resolution (swagger-parser#2131) — adds response resolution logic that mutates shared state
  2. IdsTraverser for OAS 3.1 $id dereferencing — new 675-line traverser that populates DereferencerContext.idsCache via Json31.pretty(schema)

These changes introduce state that leaks across builds in mvnd's long-lived daemon JVM. When two modules generate code in parallel, schemas from one module's execution contaminate the other.

This is NOT the same issue as swagger-core#4672 (Json.mapper() thread-safety). That was fixed in swagger-core 2.2.24, and both working and broken swagger-parser versions use swagger-core ≥2.2.24 with that fix included.

Version matrix

swagger-parser swagger-core (plugin classpath) Warm mvnd parallel build
2.1.22 2.2.22 ✅ Works
2.1.23 2.2.23 Works (last good)
2.1.24 2.2.24 Breaks (first bad)
2.1.25–2.1.27 2.2.28–2.2.29 ❌ Breaks
2.1.28 2.2.32 ❌ Breaks
2.1.37 2.2.37 ❌ Breaks

Why mvnd specifically

Unlike standard Maven, mvnd keeps a long-lived JVM daemon process alive between builds. Static state in plugin classloaders persists across builds:

  • StringUtils Caffeine caches (camelized/underscored/escaped words)
  • DefaultCodegen.sanitizedNameCache
  • Json.mapper() / Json31.mapper() ObjectMapper singletons
  • Any state accumulated in ResolverFully, ResolverCache, or IdsTraverser

The race condition requires:

  1. Parallel module execution (-T1C) — two independent modules run code generation simultaneously
  2. Warm daemon — the JVM has accumulated state from prior builds
  3. Case-near-colliding schema names — e.g. CountryDto vs CountryDTO across modules

Standard Maven (mvn) gets a fresh JVM per invocation, so the issue does not reproduce there.

Reproduction environment

  • openapi-generator-maven-plugin: 7.20.0 (also tested with 7.14.0, 7.15.0)
  • mvnd: 1.0.5
  • Java: 21 (Temurin 21.0.10+7)
  • OS: macOS (Darwin 25.3.0)
  • Build config: -T1C in .mvn/maven.config

Project structure:

  • Module A: 2 executions (v1 + v2 specs), defines CountryDto (fields: identifier, label)
  • Module B: 55 executions (various third-party specs), defines CountryDTO (fields: code, name)
  • Modules A and B have no Maven dependency between them → run in parallel

How to reproduce:

# With mvnd and -T1C configured:
mvnd clean compile          # warm up daemon
mvnd clean compile          # repeat 3-5 times
# Check module A's generated CountryDto.java — it will contain CountryDTO with wrong fields

Notable: mvnd clean test-compile does not trigger the bug, only mvnd clean compile. The additional phases in test-compile likely give the Caffeine caches time to expire (they use expireAfterAccess(5, SECONDS) in StringUtils and expireAfterAccess(10, SECONDS) in DefaultCodegen), which prevents the stale state from leaking into the next build.

A standalone minimal reproduction project has not been created yet, as the race condition is timing-sensitive and requires sufficient parallel work to trigger.

Workaround

Pin swagger-parser to 2.1.23 in the plugin's dependency override. Version 2.1.23 has the ParseOptions.setResolveResponses() API (needed by openapi-generator 7.15.0+) but predates the buggy ResolverFully implementation:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.20.0</version>
    <dependencies>
        <dependency>
            <groupId>io.swagger.parser.v3</groupId>
            <artifactId>swagger-parser</artifactId>
            <version>2.1.23</version>
        </dependency>
    </dependencies>
</plugin>

Related issues

openapi-generator version

7.20.0 (also affects 7.14.0+)

OpenAPI declaration file content or url

N/A (requires two separate specs with case-near-colliding schema names)

Generation Details

generatorName: spring
library: spring-boot
configOptions:
  useSpringBoot3: true
  interfaceOnly: true
  openApiNullable: false

Steps to reproduce

See "How to reproduce" section above.

Related issues/PRs

See "Related issues" section above.

Suggest a fix

The openapi-generator-maven-plugin should either:

  1. Ensure all static caches are cleared between executions (especially in mvnd context)
  2. Use instance-level caches instead of static caches
  3. Scope shared state per-execution or per-module rather than per-JVM

The immediate fix is in swagger-parser: the ResolverFully and IdsTraverser changes in 2.1.24 need to ensure no mutable state leaks across independent parse operations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions