This document outlines the plan for adding comprehensive JSpecify nullness annotations to the java-dataloader library.
The repository already has partial JSpecify integration:
- JSpecify 1.0.0 dependency is configured as an API dependency
- NullAway is configured with JSpecify mode (
JSpecifyMode=true) andOnlyNullMarked=true - ~21 files are already annotated with
@NullMarkedand@Nullable - OSGi bundle configuration makes JSpecify annotations optional for users
The following files already have @NullMarked annotations:
DataLoader.java✅DataLoaderFactory.java✅ (assumed based on usage)DataLoaderHelper.java✅DataLoaderRegistry.java✅BatchLoader.java✅BatchLoaderWithContext.java✅BatchLoaderContextProvider.java✅BatchLoaderEnvironment.java✅BatchLoaderEnvironmentProvider.java✅MappedBatchLoader.java✅MappedBatchLoaderWithContext.java✅BatchPublisher.java✅BatchPublisherWithContext.java✅MappedBatchPublisher.java✅MappedBatchPublisherWithContext.java✅CacheMap.java✅CacheKey.java✅ValueCache.java✅ValueCacheOptions.java✅DelegatingDataLoader.java✅DispatchResult.java✅
DefaultCacheMap.java✅
ScheduledDataLoaderRegistry.java✅
✅ COMPLETED: All phases have been implemented. See status below.
| File | Status | Notes |
|---|---|---|
Try.java |
✅ Done | Added @NullMarked, marked throwable field as nullable, used nonNull() assertions |
DataLoaderOptions.java |
✅ Done | Added @NullMarked, marked nullable fields and builder setters |
BatchLoaderContextProvider.java |
✅ Done | Marked getContext() return as @Nullable |
| File | Status | Notes |
|---|---|---|
Assertions.java |
✅ Done | Added @NullMarked, marked nonNull() input parameter as nullable |
CompletableFutureKit.java |
✅ Done | Added @NullMarked, marked cause() return as nullable |
PromisedValues.java |
✅ Done | Added @NullMarked, marked nullable returns (cause(), get()) |
PromisedValuesImpl.java |
✅ Done | Added @NullMarked, marked nullable returns |
NoOpValueCache.java |
✅ Done | Added @NullMarked, added nullable type bound V extends @Nullable Object |
DataLoaderAssertionException.java |
✅ Done | Added @NullMarked |
| File | Status | Notes |
|---|---|---|
Statistics.java |
✅ Done | Added @NullMarked |
StatisticsCollector.java |
✅ Done | Added @NullMarked, marked context parameters as nullable |
SimpleStatisticsCollector.java |
✅ Done | Added @NullMarked |
ThreadLocalStatisticsCollector.java |
✅ Done | Added @NullMarked |
NoOpStatisticsCollector.java |
✅ Done | Added @NullMarked |
DelegatingStatisticsCollector.java |
✅ Done | Added @NullMarked |
| File | Status | Notes |
|---|---|---|
IncrementBatchLoadCountByStatisticsContext.java |
✅ Done | Added @NullMarked, nullable callContext |
IncrementBatchLoadExceptionCountStatisticsContext.java |
✅ Done | Added @NullMarked, nullable callContexts |
IncrementCacheHitCountStatisticsContext.java |
✅ Done | Added @NullMarked, nullable callContext |
IncrementLoadCountStatisticsContext.java |
✅ Done | Added @NullMarked, nullable callContext |
IncrementLoadErrorCountStatisticsContext.java |
✅ Done | Added @NullMarked, nullable callContext |
| File | Status | Notes |
|---|---|---|
DataLoaderInstrumentation.java |
✅ Done | Added @NullMarked, nullable returns and loadContext param |
DataLoaderInstrumentationContext.java |
✅ Done | Added @NullMarked, nullable onCompleted parameters |
DataLoaderInstrumentationHelper.java |
✅ Done | Added @NullMarked, nullable parameters |
SimpleDataLoaderInstrumentationContext.java |
✅ Done | Added @NullMarked, nullable fields and callbacks |
ChainedDataLoaderInstrumentation.java |
✅ Done | Added @NullMarked, handle nullable contexts |
| File | Status | Notes |
|---|---|---|
BatchLoaderScheduler.java |
✅ Done | Added @NullMarked |
| File | Status | Notes |
|---|---|---|
DispatchPredicate.java |
✅ Done | Added @NullMarked |
| File | Status | Notes |
|---|---|---|
ReactiveSupport.java |
✅ Done | Added @NullMarked, nullable callContexts |
AbstractBatchSubscriber.java |
✅ Done | Added @NullMarked, nullable callContexts |
BatchSubscriberImpl.java |
✅ Done | Added @NullMarked |
MappedBatchSubscriberImpl.java |
✅ Done | Added @NullMarked, handle nullable futures |
| File | Status | Notes |
|---|---|---|
ExperimentalApi.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
GuardedBy.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
Internal.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
PublicApi.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
PublicSpi.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
VisibleForTesting.java |
⏭️ Skipped | Annotation definition doesn't need null annotations |
Note: Annotation definitions typically don't need
@NullMarkedunless they have methods that return potentially null values.
For each file, add the @NullMarked annotation at the class level:
import org.jspecify.annotations.NullMarked;
@NullMarked
public class MyClass {
// ...
}For any parameter, return type, or field that can be null, add @Nullable:
import org.jspecify.annotations.Nullable;
@NullMarked
public class Example {
private @Nullable String optionalField;
public @Nullable Result findResult(String key) {
// ...
}
public void process(@Nullable String maybeNull) {
// ...
}
}For generic types where the type parameter can be null:
// Type parameter V can be null
public class Try<V extends @Nullable Object> {
private @Nullable V value;
}
// In DataLoader, V can be null
public class DataLoader<K, V extends @Nullable Object> {
// ...
}After annotating each phase, run:
./gradlew compileJavaNullAway will report any nullability errors. Fix any issues before proceeding.
Once all files are annotated, uncomment the following in build.gradle:
option("NullAway:AnnotatedPackages", "org.dataloader")This will enable strict null checking for the entire package.
- Compile-time verification: NullAway catches nullability issues during compilation
- Existing tests: Run the full test suite to ensure no behavioral regressions
- IDE integration: JSpecify annotations work with IDEs that support them (IntelliJ, Eclipse with plugins)
- Adding JSpecify annotations is binary compatible
- The annotations are available at runtime but not required (resolution:=optional in OSGi)
- Users who don't use null-checking tools will see no difference
- Users with NullAway/Checker Framework/IntelliJ will benefit from null checking
When annotating the API, consider:
-
Return types: Should methods return
@Nullableor useOptional?- Existing uses of
Optionalremain unchanged - Raw nullable returns get
@Nullable
- Existing uses of
-
Parameters: Document and annotate nullable parameters
- If the Javadoc says "can be null", add
@Nullable
- If the Javadoc says "can be null", add
-
Generic bounds: For generics that can hold null values
- Use
V extends @Nullable Objectpattern
- Use
- Core public API (
org.dataloaderpackage) - Users interact with these directly - SPI interfaces (
instrumentation,scheduler) - Extension points - Statistics - Commonly used for monitoring
- Implementation details (
impl,reactive) - Internal but important for correctness
| Phase | Files | Time Estimate | Justification |
|---|---|---|---|
| Phase 1-2 (Core + Impl) | 8 files | 2-3 hours | Try.java and DataLoaderOptions.java require careful analysis of nullable generics and multiple nullable fields. Implementation classes need review of internal null handling. |
| Phase 3 (Stats) | 11 files | 1-2 hours | Mostly straightforward classes with few nullable members. Context classes are simple records. |
| Phase 4-5 (Instrumentation + Scheduler) | 6 files | 1-2 hours | Methods explicitly return nullable DataLoaderInstrumentationContext. Requires attention to null callback patterns. |
| Phase 6-7 (Registries + Reactive) | 5 files | 1 hour | DispatchPredicate is simple. Reactive subscribers follow established patterns. |
| Phase 8 (Annotations) | 6 files | 30 minutes | Annotation definitions rarely need null annotations; quick review only. |
| Final verification | N/A | 1-2 hours | Full build, test suite, and enable AnnotatedPackages option. |
Each file requires the following activities:
- Analysis (~5-10 min/file): Review all fields, parameters, return types, and generic bounds
- Annotation (~5-10 min/file): Add
@NullMarkedand@Nullableannotations - Compilation check (~2-5 min/file): Run
./gradlew compileJavaand fix NullAway errors - Iteration (variable): Complex classes may need multiple rounds of fixes
Complexity factors that increase time:
- Generic types with nullable bounds (e.g.,
Try<V>,DataLoader<K, V>) - Classes with many optional/nullable fields (e.g.,
DataLoaderOptions) - Interfaces that return nullable values (e.g.,
DataLoaderInstrumentation) - Chained builders or fluent APIs
Factors that decrease time:
- Simple data classes with all non-null fields
- Annotation definitions (usually no changes needed)
- Classes that already follow non-null patterns
| Scenario | Hours | Conditions |
|---|---|---|
| Optimistic | 6 hours | Experienced with JSpecify, no unexpected issues |
| Expected | 8 hours | Normal pace, some debugging of NullAway errors |
| Pessimistic | 10 hours | Complex edge cases, API design decisions needed |
Recommendation: Plan for 8 hours, which allows for one full working day or 2-3 focused sessions.