Skip to content

feat: honor thread context classloader when loading custom normalizers and generators - #24617

Open
Picazsoo wants to merge 13 commits into
OpenAPITools:masterfrom
Picazsoo:feature/NORMALIZER-CLASS-in-process
Open

feat: honor thread context classloader when loading custom normalizers and generators#24617
Picazsoo wants to merge 13 commits into
OpenAPITools:masterfrom
Picazsoo:feature/NORMALIZER-CLASS-in-process

Conversation

@Picazsoo

@Picazsoo Picazsoo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

A custom NORMALIZER_CLASS normalizer could fail to load, especially under the Gradle plugin's process worker isolation:

  1. OpenAPINormalizer.createNormalizer used bare Class.forName(String), which resolves against the classloader that loaded openapi-generator core, not any user-supplied classpath.
  2. The Gradle plugin never called classpath.from(...) in either isolation branch. Under process isolation this is fatal: the forked JVM has zero visibility into anything not explicitly forwarded. Under classloader isolation it was merely fragile: since the worker runs in the same JVM, a custom normalizer could work by accident if it happened to already be reachable through the existing classloader hierarchy (e.g. stuck on the buildscript classpath), but this was never a supported or documented mechanism.

Changes

Core (OpenAPINormalizer.java)

  • createNormalizer now resolves the class via the current thread's context classloader (TCCL) first (which Gradle's Worker API sets to include user classpath), falling back to the original defining classloader for backward compatibility.
  • Failure messages are split by cause: ClassNotFoundException gets classpath guidance, other reflective failures (bad constructor, instantiation errors) get a distinct, accurate message.

Gradle plugin

  • Added generatorClasspath (file collection) and an openApiGeneratorExtra resolvable configuration, both forwarded to the worker in processIsolation and classLoaderIsolation. Users can now supply a custom normalizer jar via normal Gradle dependencies or file collections.
  • Documented the classpath requirement in the openapiNormalizer KDoc, README, and customization docs.

Tests

  • Core: added unit tests covering TCCL resolution, TCCL-null and TCCL-miss fallback, and the clearer error message.
  • Gradle plugin: added GeneratorClasspathIsolationTest covering both classpath mechanisms under both isolation modes, plus a negative control. Positive tests assert a marker file written by the fixture normalizer's normalize() to directly prove it ran, not just that the build succeeded.

Compatibility

Fully backward compatible. No NORMALIZER_CLASS set, or normalizer already visible on the default classpath: unchanged behavior. New properties default to empty/unset.

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

Prefer the thread context classloader when loading custom NORMALIZER_CLASS and custom generators, and forward user classpaths in the Gradle plugin for both isolation modes. Adds precise, tool‑agnostic errors, better generator discovery, updated docs, and tests that prove the classes actually run.

  • New Features

    • Core: OpenAPINormalizer now loads via the thread context classloader with a safe fallback and clearer errors (missing classpath vs bad constructor). CodegenConfigLoader uses the context classloader, getAll() merges/de‑dupes from both classloaders, and forName() returns precise errors for classpath/not‑found, incompatible Java version, static initializer failures, linkage errors, and bad constructors/implementations.
    • Gradle plugin: adds openApiGeneratorExtra and a generatorClasspath property; both are forwarded to the worker classpath for workerIsolation process and classloader. README/KDoc document classpath requirements for NORMALIZER_CLASS and custom generators by name/FQCN. Tests cover both isolation modes, generator‑by‑FQCN, negative controls, and execution proven via marker files.
  • Migration

    • When using a custom NORMALIZER_CLASS or a custom generator (by name/FQCN) with the Gradle plugin, add it to openApiGeneratorExtra (preferred) or generatorClasspath, e.g. dependencies { openApiGeneratorExtra("com.acme:my-normalizer:1.0.0") } or openApiGenerate { generatorClasspath.from(files("libs/my-generator.jar")) }.

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

Review in cubic

@Picazsoo
Picazsoo marked this pull request as ready for review August 5, 2026 08:46
@Picazsoo
Picazsoo marked this pull request as draft August 5, 2026 08:47

@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 8 files

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

Re-trigger cubic

Picazsoo and others added 2 commits August 5, 2026 11:21
- Keep core error message tool-agnostic (no Gradle-specific terms);
  Gradle-specific guidance remains only in the plugin README/docs.
- Add missing test coverage for generatorClasspath + classloader isolation.
- Clean up temp directories created by classloader fallback tests to
  avoid leaking compiled fixture files across test runs.

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

Cross-reference generatorClasspath/openApiGeneratorExtra directly on the
openapiNormalizer property (both extension and task) so the requirement
is discoverable from IDE tooltips/KDoc, not just the README.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo
Picazsoo marked this pull request as ready for review August 5, 2026 09:43
@Picazsoo
Picazsoo marked this pull request as draft August 5, 2026 09:43

@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 8 files

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

Re-trigger cubic

- Split ClassNotFoundException (classpath guidance) from other
  ReflectiveOperationException cases (constructor/instantiation failures)
  in OpenAPINormalizer.createNormalizer so the error message matches the
  actual failure.
- Replace redundant expectedExceptions + manual catch/rethrow in
  OpenAPINormalizerTest with expectThrows.
- Clean up normalizer fixture temp directories after each test in
  GeneratorClasspathIsolationTest (AfterMethod deleteRecursively).
- Assert on the wrapped classpath-guidance log message in addition to
  the raw ClassNotFoundException text, reducing coupling to log format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo
Picazsoo marked this pull request as ready for review August 5, 2026 10:54
@Picazsoo
Picazsoo marked this pull request as draft August 5, 2026 10:55

@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 8 files

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

Re-trigger cubic

Picazsoo and others added 2 commits August 5, 2026 13:33
- Skip (not NPE) the TCCL-fixture test when running on a JRE without a
  system Java compiler (ToolProvider.getSystemJavaCompiler() returns null).
- Assert that positive GeneratorClasspathIsolationTest cases show no
  NORMALIZER_CLASS load failure/ClassNotFoundException in the build log,
  in addition to TaskOutcome.SUCCESS, since DefaultGenerator logs but
  does not fail the build on a normalizer load failure. Without this,
  a regression dropping the forwarded classpath would pass silently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The fixture normalizer now overrides normalize() to write a marker file
(path passed via a MARKER_FILE inputRule) before delegating to super,
giving direct proof the custom NORMALIZER_CLASS ran under the worker -
across a forked JVM in 'process' isolation - rather than relying only on
the build succeeding and no failure text appearing in the log. The
negative-control test asserts the marker is absent when the class fails
to load, corroborating that normalize() never executed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo
Picazsoo marked this pull request as ready for review August 5, 2026 14:53

@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 8 files

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

Re-trigger cubic

Picazsoo and others added 4 commits August 5, 2026 21:59
…fixture

Mirrors the guard already present in OpenAPINormalizerTest's
compileNormalizerFixture: skip with a clear SkipException instead of an
opaque NullPointerException when running on a JRE without a system
Java compiler.

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 5 files (changes from recent commits).

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

Re-trigger cubic

@Picazsoo Picazsoo changed the title feat(normalizer): enhance custom normalizer class loading with context classloader support feat: honor thread context classloader when loading custom normalizers and generators Aug 5, 2026

@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 3 files (changes from recent commits).

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

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

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.

1 participant