Claude-skill-registry java-null-safety
JSpecify null safety annotations with @NullMarked, @Nullable, and package-level configuration
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/java-null-safety" ~/.claude/skills/majiayu000-claude-skill-registry-java-null-safety && rm -rf "$T"
manifest:
skills/data/java-null-safety/SKILL.mdsource content
Java Null Safety Skill
Prerequisites
This skill requires JSpecify annotations:
(NullMarked, Nullable, NonNull)org.jspecify:jspecify
Required Imports
// JSpecify Null Safety Annotations import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.jspecify.annotations.NonNull;
Maven Dependency
<dependency> <groupId>org.jspecify</groupId> <artifactId>jspecify</artifactId> </dependency>
Core Annotations
- Marks a package or class where all types are non-null by default@NullMarked
- Marks a type as nullable (exception to @NullMarked default)@Nullable
- Explicitly marks a type as non-null (only needed without @NullMarked)@NonNull
Package-Level Configuration (PREFERRED)
Always prefer
@NullMarked in package-info.java for consistent null-safety across the entire package.
Correct package-info.java Structure
The
package-info.java file has a unique syntax that differs from regular Java classes:
// package-info.java /* * Copyright headers and license... */ /** * Token validation and authentication services. * * <p>All types in this package are non-null by default due to {@code @NullMarked}. * Use {@code @Nullable} to explicitly mark nullable types. */ @NullMarked package de.cuioss.portal.authentication; import org.jspecify.annotations.NullMarked;
CRITICAL: Unique package-info.java Syntax
The structure is special and MUST follow this exact order:
- File header comment (copyright, license)
- Package JavaDoc comment (describes the package)
- Package annotations (like
)@NullMarked
declarationpackage
statements (AFTER the package declaration)import
Why This Is Different:
In regular Java classes, imports come BEFORE the class declaration:
import java.util.List; // Import first public class MyClass { // Then class }
In
package-info.java, imports come AFTER the package declaration:
@NullMarked // Annotation first package com.example; // Then package import org.jspecify.annotations.NullMarked; // Import last
This reverse ordering is the Java Language Specification requirement for package-info.java files. Placing imports before the package declaration will cause compilation errors.
Benefits:
- Consistent null-safety across entire package
- Less annotation noise (default is non-null)
- Clear contract for package APIs
- Easier to maintain
API Return Type Guidelines
Pattern 1: Guaranteed Non-Null Return (Default)
Methods return non-null by default with package-level
@NullMarked:
/** * Validates the JWT token and returns the result. * * @param token the token to validate, must not be null * @return validation result, never null */ public ValidationResult validate(String token) { // Implementation must ensure non-null return return new ValidationResult(token, checkSignature(token)); }
Pattern 2: Optional Result
Use
Optional<T> when the method may not have a result to return:
/** * Finds a user by their unique identifier. * * @param userId the user identifier, must not be null * @return the user if found, or Optional.empty() if not found */ public Optional<User> findById(String userId) { User user = repository.get(userId); return Optional.ofNullable(user); }
CRITICAL RULE: Never Use @Nullable for Return Types
NEVER use
@Nullable for return types. Either guarantee a non-null return or use Optional.
// ❌ BAD - Never do this public @Nullable ValidationResult validate(String token) { // Nullable returns are forbidden } // ✅ GOOD - Guaranteed non-null public ValidationResult validate(String token) { // Must return non-null } // ✅ GOOD - Use Optional for "no result" scenarios public Optional<ValidationResult> tryValidate(String token) { // Returns Optional.empty() when no result }
Null-Safe Implementation Patterns
With @NullMarked (Package Level)
// With @NullMarked at package level, everything is non-null by default public class TokenValidator { // Field is non-null by default private final TokenConfig config; // Parameter is non-null by default public TokenValidator(TokenConfig config) { // No null check needed if caller respects contract // But defensive programming is still acceptable: this.config = Objects.requireNonNull(config, "config must not be null"); } // Parameter and return are non-null by default public ValidationResult validate(String token) { Objects.requireNonNull(token, "token must not be null"); // Implementation must return non-null return new ValidationResult(/*...*/); } // Mark nullable parameters explicitly public String processWithDefault(@Nullable String input) { return input != null ? input.toUpperCase() : "DEFAULT"; } // Use Optional instead of @Nullable returns public Optional<UserInfo> extractUserInfo(String token) { return parseToken(token) .map(this::extractUser); } }
Without @NullMarked (Explicit Annotations)
If not using package-level
@NullMarked, you must explicitly annotate:
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public class TokenValidator { @NonNull private final TokenConfig config; public TokenValidator(@NonNull TokenConfig config) { this.config = config; } @NonNull public ValidationResult validate(@NonNull String token) { return new ValidationResult(/*...*/); } @NonNull public String processWithDefault(@Nullable String input) { return input != null ? input.toUpperCase() : "DEFAULT"; } }
Implementation Requirements
1. Package-Level Configuration
- Always prefer
in@NullMarkedpackage-info.java - Document the null-safety contract in package documentation
- Use
only for exceptions to the non-null default@Nullable
2. Default Non-Null
- With
, all types are non-null by default@NullMarked - Only use
for exceptions@Nullable - Never annotate with
when using@NonNull
(redundant)@NullMarked
3. Implementation Responsibility
- The implementation MUST ensure that non-nullable methods never return null
- Add defensive null checks at API boundaries
- Use
for parameter validationObjects.requireNonNull()
public ValidationResult validate(String token) { // Defensive programming at API boundary Objects.requireNonNull(token, "token must not be null"); // Implementation ensures non-null return if (isValid(token)) { return ValidationResult.valid(); } return ValidationResult.invalid("Token validation failed"); }
Nullable Parameters
Use
@Nullable sparingly for parameters that genuinely accept null:
// Good - null has clear meaning (use default) public String format(@Nullable Locale locale) { Locale effectiveLocale = locale != null ? locale : Locale.getDefault(); return formatter.format(effectiveLocale); } // Best - overload methods instead of nullable parameters public String format() { return format(Locale.getDefault()); } public String format(Locale locale) { return formatter.format(locale); }
Collections and Generics
Apply null-safety to collection types:
// With @NullMarked, all elements are non-null public List<User> getActiveUsers() { // Returns non-null list of non-null User objects return users.stream() .filter(User::isActive) .toList(); } // Use @Nullable for nullable elements public List<@Nullable String> getOptionalValues() { // List is non-null, but elements can be null return Arrays.asList("value1", null, "value3"); }
Unit Testing
Test that non-nullable methods never return null under any valid input conditions:
@Test void shouldNeverReturnNull() { // With @NullMarked, non-nullable methods must never return null assertNotNull(service.processToken("valid")); assertNotNull(service.processToken("")); // Non-nullable methods should handle edge cases without returning null assertNotNull(service.processToken("edge-case")); } @Test void shouldUseOptionalForMissingValues() { // Use Optional.empty() instead of null returns assertTrue(service.findUser("unknown").isEmpty()); assertTrue(service.findUser("existing").isPresent()); } @Test void shouldRejectNullParameters() { // Non-nullable parameters should be validated assertThrows(NullPointerException.class, () -> service.processToken(null)); }
Migration Strategy
For New Code
- Add
to@NullMarkedpackage-info.java - Write code assuming non-null by default
- Use
only where null is explicitly allowed@Nullable - Use
for "no result" return typesOptional<T> - Validate with unit tests
For Existing Code
- Add
to package@NullMarked - Review all public APIs
- Add
where null is currently accepted/returned@Nullable - Refactor nullable returns to Optional where appropriate
- Add null checks with
at API boundariesObjects.requireNonNull() - Update tests to verify null-safety contracts
Quality Checklist
- Package has @NullMarked in package-info.java
- No @Nullable used for return types (use Optional instead)
- Nullable parameters documented and justified
- Defensive null checks at API boundaries
- Unit tests verify non-null contracts (see
skill)pm-dev-java:junit-core - Static analysis configured and passing
- JavaDoc documents null-safety contract
- Collections specify element nullability if needed
Related Skills
- Core Java patternspm-dev-java:java-core
- Lombok patterns (interop with null safety)pm-dev-java:java-lombok