Skip to content

CAMEL-22640: Add NullAway validation profile and fix null annotations in camel-api#23021

Open
gnodet wants to merge 15 commits into
apache:mainfrom
gnodet:CAMEL-22640-nullaway-validation
Open

CAMEL-22640: Add NullAway validation profile and fix null annotations in camel-api#23021
gnodet wants to merge 15 commits into
apache:mainfrom
gnodet:CAMEL-22640-nullaway-validation

Conversation

@gnodet
Copy link
Copy Markdown
Contributor

@gnodet gnodet commented May 6, 2026

CAMEL-22640

Summary

Add comprehensive JSpecify null safety annotations to camel-api, and a NullAway compile-time validation profile to enforce them.

Changes

JSpecify null annotations on camel-api (283 files)

  • Add org.jspecify:jspecify dependency to camel-api
  • Mark all 16 packages in camel-api as @NullMarked via package-info.java files (org.apache.camel, o.a.c.spi, o.a.c.vault, o.a.c.support.jsse, o.a.c.health, o.a.c.resume, etc.)
  • Add ~1200 @Nullable annotations across the public API surface (interfaces, abstract classes, exception constructors, return types, method parameters) to accurately document nullability contracts
  • Key areas annotated:
    • CamelContext — nullable returns for getComponent(), hasComponent(), getRoute(), getProcessor(), resolveDataFormat(), resolveTransformer(), resolveValidator(), getVariable(), getGlobalOption(), etc.
    • Exchange / Message — nullable bodies, headers, properties
    • AggregationStrategy — nullable old/new exchange parameters and return
    • Exception classes — nullable causes and detail fields
    • JSSE/SSL classes — nullable context parameters, key/trust managers
    • spi.* — nullable returns on resolvers, factories, registries, repositories
    • vault.* — nullable optional configuration fields
    • resume.* — nullable serialization results
  • Add null guards where @Nullable values are dereferenced (e.g., HealthCheckHelper, RoutesLoader, GroovyScriptCompiler, JSSE parameter classes)
  • Update Velocity template for generated ExchangeConstantProvider to include @Nullable

NullAway validation profile (parent/pom.xml)

  • Add opt-in nullcheck Maven profile using NullAway via Error Prone
  • Error Prone 2.49.0 + NullAway 0.13.4
  • JSpecify mode enabled, only checks @NullMarked packages
  • All other Error Prone checks disabled — only NullAway runs
  • Requires JDK 21+ (forked compiler with --add-exports flags)
  • Opt-in only: mvn install -Pnullcheck -DskipTests

Test fix

  • Fix import formatting in AggregateProcessorTest to match updated AggregationStrategy signature

Verification

# Run NullAway on camel-api (requires JDK 21+)
JAVA_HOME=$HOME/.sdkman/candidates/java/21 mvn clean install -B -pl core/camel-api -Pnullcheck -DskipTests -am
# Result: BUILD SUCCESS, 0 NullAway warnings

# Also verified clean across full camel-core module tree
JAVA_HOME=$HOME/.sdkman/candidates/java/21 mvn clean install -B -pl core/camel-core -Pnullcheck -DskipTests -am
# Result: BUILD SUCCESS, 0 NullAway warnings

gnodet and others added 9 commits March 29, 2026 23:02
Add @NullMarked (JSpecify 1.0.0) to all packages in camel-api,
making non-null the default. Annotate ~200 API interfaces and
classes with @nullable where parameters or return values can
legitimately be null, covering the core API surface including
CamelContext, Exchange, Message, Component, Endpoint, TypeConverter,
and all SPI interfaces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scan actual implementations (not just Javadoc) for return null,
null field values, and caller null-checks. Adds missing @nullable
annotations and moves @NullMarked to package-level only.

Key additions found by implementation scanning:
- Exchange.getPattern() can be null (field uninitialized)
- Message.getExchange() can be null (field starts null)
- Route.getOnException/navigate() can return null
- DelegateProcessor.getProcessor() can be null (LazyStartProducer)
- Various transient exception fields nullable after deserialization
- ConsumerTemplate.receiveBody() without timeout can return null
- CamelContext.getComponent() overloads can return null
- ExtendedCamelContext.getErrorHandlerFactory() can be null
- Multiple SPI methods found nullable through impl analysis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CamelExchangeException: accept @nullable exchange (field already nullable)
- CamelExecutionException: accept @nullable exchange (called with null from ExchangeHelper)
- ExchangeTimedOutException: accept @nullable exchange (consistent with parent)
- Transformer: setFrom/setTo accept @nullable (fields already nullable, called with null from reifiers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…null-annotations

# Conflicts:
#	core/camel-api/src/main/java/org/apache/camel/spi/Transformer.java
#	core/camel-api/src/main/java/org/apache/camel/vault/AwsVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/AzureVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/CyberArkVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/GcpVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/HashicorpVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/IBMSecretsManagerVaultConfiguration.java
#	core/camel-api/src/main/java/org/apache/camel/vault/SpringCloudConfigConfiguration.java
#	parent/pom.xml
…null-annotations

# Conflicts:
#	core/camel-api/src/main/java/org/apache/camel/NamedNode.java
#	core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add nullcheck Maven profile with Error Prone + NullAway (JDK 21+)
- Fix 134 NullAway warnings in camel-api annotations
- Make HasCamelContext.getCamelContext() return @nullable
- Fix BulkTypeConverters to match TypeConverter interface nullability
- Fix JSSE classes null safety (BaseSSLContextParameters, etc.)
- Add null guards for @nullable dereferences
- Update Velocity template for generated code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…validation

# Conflicts:
#	core/camel-api/src/main/java/org/apache/camel/CamelContext.java
#	core/camel-api/src/main/java/org/apache/camel/package-info.java
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

gnodet and others added 2 commits May 6, 2026 20:16
Remove JDK activation so the profile only activates via -Pnullcheck,
not automatically on JDK 21+ CI builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sync with origin/main and fix fully-qualified TimeUnit import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gnodet gnodet closed this May 6, 2026
@gnodet gnodet reopened this May 6, 2026
@davsclaus
Copy link
Copy Markdown
Contributor

I assume you can close your old PR

@davsclaus
Copy link
Copy Markdown
Contributor

There are uncommitted changes
HEAD detached at pull/23021/merge
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git restore ..." to discard changes in working directory)
modified: core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregateProcessorTest.java

Replace java.util.concurrent.TimeUnit.SECONDS with imported TimeUnit
to pass the OpenRewrite uncommitted-changes CI check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@davsclaus
Copy link
Copy Markdown
Contributor

It may be good to add a note in the 4.21 upgrade guide that camel-api has dependency on jspecify JAR

Copy link
Copy Markdown
Contributor

@apupier apupier left a comment

Choose a reason for hiding this comment

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

last release of jspecify was 2 years ago.

Is the benefit really that huge to introduce a dependency on a project which doesn't seem very active?

- Mark jspecify dependency as <optional>true</optional> in camel-api
  so it is not pulled transitively into consumer projects
- Add note to 4.21 upgrade guide about the new jspecify dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gnodet
Copy link
Copy Markdown
Contributor Author

gnodet commented May 7, 2026

@davsclaus Good point — I've added a note to the 4.21 upgrade guide and also made the jspecify dependency <optional>true</optional> so it's not pulled transitively into consumer projects.

Claude Code on behalf of Guillaume Nodet

@gnodet
Copy link
Copy Markdown
Contributor Author

gnodet commented May 7, 2026

@apupier JSpecify 1.0.0 is a stable release — the project only contains 4 annotation types (`@Nullable`, `@NonNull`, `@NullMarked`, `@NullUnmarked`), so there's very little reason for frequent releases. It's backed by Google, JetBrains, and others, and is becoming the de facto standard for null annotations in the Java ecosystem (supported by NullAway, Error Prone, Kotlin, IntelliJ, etc.).

That said, I've made the dependency `true` so it's not pulled transitively into consumer projects. Annotations gracefully degrade if the JAR isn't on the classpath — they simply become invisible to reflection.

Claude Code on behalf of Guillaume Nodet

@github-actions github-actions Bot added the docs label May 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🧪 CI tested the following changed modules:

  • core/camel-api
  • docs
  • parent
  • tooling/maven/camel-package-maven-plugin

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.

Build reactor — dependencies compiled but only changed modules were tested (4 modules)
  • Camel :: API
  • Camel :: Docs
  • Camel :: Maven Plugins :: Camel Maven Package
  • Camel :: Parent

⚙️ View full build and test results

Ensures jspecify annotations are on the classpath for downstream
modules when compiling with -Pnullcheck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines 52 to 54
public OAuthClientConfig setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
this.clientSecret = Objects.requireNonNull(clientSecret, "clientSecret");
return this;
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.

the clientSecret is nullable, why do we requireNonNull?

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.

For this constructor, yes. But the no-arg constructor does not set the clientSecret field, which ends up being null, hence the @nullable.


protected Transformer(String name) {
this.name = name;
this.name = Objects.requireNonNull(name, "name");
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.

the name is marked as Nullable but here there is a requireNonNull

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.

For this constructor, yes. But the no-arg constructor does not set the name field, which ends up being null, hence the @Nullable.

Copy link
Copy Markdown
Contributor

@apupier apupier left a comment

Choose a reason for hiding this comment

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

most of (all?) requireNonNullMessage are not giving any information. Either remove them or provide a useful message.

To be honest, I feel like this is the kind of changes that shouldn't be done in a bulk change and reviewed carefully at each place. I checked only few files and found inconsistencies

@gnodet
Copy link
Copy Markdown
Contributor Author

gnodet commented May 7, 2026

most of (all?) requireNonNullMessage are not giving any information. Either remove them or provide a useful message.

To be honest, I feel like this is the kind of changes that shouldn't be done in a bulk change and reviewed carefully at each place. I checked only few files and found inconsistencies

This PR does not modify current semantics. So most fields are annotated with @Nullable because there's a no-arg constructor, even though it's not supposed to be null at runtime. This is a design flaw in the API, but cannot be fixed by annotations. This PR also adds a profile which uses compile-time source analysis via Error Prone to make sure the whole code actually follows the annotations.

I could improve the requireNonNull messages, but beyond the name of the field, anything else is just noise imho, but I can do some "xxx must be non nullinstead of just "xxx". Though the JDK itself is using just the field name, and so does Guava. Spring is different and usesAssert.notNull(obj, "'obj' must not be null")`.

…validation

# Conflicts:
#	core/camel-api/src/main/java/org/apache/camel/NamedRoute.java
public abstract class ResumeStrategyConfiguration {
private Cacheable.FillPolicy cacheFillPolicy;
private ResumeCache<?> resumeCache;
private Cacheable.@Nullable FillPolicy cacheFillPolicy;
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.

this looks a bit funny

* @return the fill policy to use
*/
public Cacheable.FillPolicy getCacheFillPolicy() {
public Cacheable.@Nullable FillPolicy getCacheFillPolicy() {
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.

here as well


@Override
default <T> T mandatoryConvertTo(Class<T> type, Object value)
default <T> T mandatoryConvertTo(Class<T> type, @Nullable Object value)
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.

the value is not null - you cannot converter a null value so this should be changed


@Override
default <T> T convertTo(Class<T> type, Exchange exchange, Object value) throws TypeConversionException {
default <T> @Nullable T convertTo(Class<T> type, @Nullable Exchange exchange, @Nullable Object value)
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.

the value is not null - you cannot converter a null value so this should be changed


@Override
default <T> T convertTo(Class<T> type, Object value) throws TypeConversionException {
default <T> @Nullable T convertTo(Class<T> type, @Nullable Object value) throws TypeConversionException {
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.

the value is not null - you cannot converter a null value so this should be changed

@SuppressWarnings("NullAway")
default <T> T mandatoryConvertTo(Class<?> from, Class<T> to, @Nullable Exchange exchange, @Nullable Object value)
throws TypeConversionException, NoTypeConversionAvailableException {
Object t = convertTo(from, to, exchange, value);
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.

the value is not null - you cannot converter a null value so this should be changed


@Override
default <T> T tryConvertTo(Class<T> type, Object value) {
default <T> @Nullable T tryConvertTo(Class<T> type, @Nullable Object value) {
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.

the value is not null - you cannot converter a null value so this should be changed


@Override
default <T> T tryConvertTo(Class<T> type, Exchange exchange, Object value) {
default <T> @Nullable T tryConvertTo(Class<T> type, @Nullable Exchange exchange, @Nullable Object value) {
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.

the value is not null - you cannot converter a null value so this should be changed

* @return the converted value, or <tt>null</tt> if not possible to convert
*/
default <T> T tryConvertTo(Class<?> from, Class<T> to, Exchange exchange, Object value) throws TypeConversionException {
default <T> @Nullable T tryConvertTo(Class<?> from, Class<T> to, @Nullable Exchange exchange, @Nullable Object value)
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.

the value is not null - you cannot converter a null value so this should be changed

* @throws TypeConversionException is thrown if error during type conversion
*/
<T> T convertTo(Class<?> from, Class<T> to, Exchange exchange, Object value) throws TypeConversionException;
<T> @Nullable T convertTo(Class<?> from, Class<T> to, @Nullable Exchange exchange, @Nullable Object value)
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.

the value is not null - you cannot converter a null value so this should be changed

class Builder<T extends Component> {
private final Class<T> type;
private BiPredicate<String, Component> condition;
private @Nullable BiPredicate<String, Component> condition;
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.

why is this nullable. The point of the customizer is to call the condition

class Builder<T extends DataFormat> {
private final Class<T> type;
private BiPredicate<String, DataFormat> condition;
private @Nullable BiPredicate<String, DataFormat> condition;
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.

same here as last comment

void onThreadPoolAdd(
CamelContext camelContext, ThreadPoolExecutor threadPool, String id,
String sourceId, String routeId, String threadPoolProfileId);
CamelContext camelContext, ThreadPoolExecutor threadPool, @Nullable String id,
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.

we should likely enforce that some of these are not null as creating a thread pool we should have id / source id to identify it.

default void onThreadPoolAdd(
CamelContext camelContext, ExecutorService executorService, String id,
String sourceId, String routeId, String threadPoolProfileId) {
CamelContext camelContext, ExecutorService executorService, @Nullable String id,
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.

we should likely enforce that some of these are not null as creating a thread pool we should have id / source id to identify it.

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.

3 participants