Rule java:S2447 (Boolean method should not return null) — inconsistent with treatment of other nulla

Rule S2447 requires methods returning Boolean to be explicitly annotated (e.g. @Nullable) if they may return null, on the grounds that callers might unbox the result directly (e.g. in an if statement) and risk a NullPointerException.

This rationale is technically correct, but it is applied inconsistently: the exact same risk exists for every other nullable wrapper type — Integer, Long, Double, etc. — when used in comparisons or arithmetic expressions, yet no equivalent rule flags those cases.

Concrete example

java

// Flagged by S2447
public Boolean isMinor() {
    if (birthDate == null) {
        return null; // age unknown — a legitimate third state
    }
    return age < 18;
}

java

// NOT flagged by any equivalent rule, same risk
public Integer getAge() {
    if (birthDate == null) {
        return null; // age unknown
    }
    return age;
}

Both methods can return null intentionally to represent “unknown” as a legitimate third state, distinct from true/false or from any specific number. Both expose callers to the exact same failure mode through auto-unboxing:

java

if (patient.isMinor()) { ... }      // NPE risk
int age = patient.getAge();         // NPE risk
if (age > 18) { ... }

The underlying mechanism (auto-unboxing of a null wrapper) is identical in both cases. There is no technical justification for treating Boolean differently from Integer, Long, or Double in this respect.

Why this matters in practice

In domains where a tri-state boolean is a natural and correct modeling choice — for example, patient consent (granted / denied / not yet expressed) — this rule forces developers to either:

  • add an external annotation dependency purely to satisfy the rule, or
  • suppress the issue case-by-case, or
  • disable the rule at the profile level,

for a pattern that is simply correct domain modeling, not a defect. Meanwhile, the equivalent (and equally risky) pattern with Integer/Long/Double raises no warning at all, so teams have no consistent guidance across wrapper types.

Request

Could the SonarSource team please clarify the reasoning behind singling out Boolean for this treatment? Specifically, we’d like to understand:

  1. Is there a technical reason Boolean is treated differently from other nullable wrapper types with respect to unboxing risk?
  2. If not, would the team consider either:
    • extending equivalent detection to Integer, Long, Double, etc. for consistency, or
    • relaxing/revising S2447 so it doesn’t require extra annotation overhead for legitimate tri-state Boolean usage (e.g. by recognizing common null-check patterns before unboxing).

We think the underlying principle (document nullable wrapper returns explicitly) is sound, but its asymmetric application to Boolean alone creates friction without a corresponding safety benefit relative to other wrapper types.

Thanks for considering this feedback.

Hi @mirko.golfieri, and welcome to the community!

Thank you for taking the time to write this up.

Is there a technical reason Boolean is treated differently?

Not at the level of the unboxing mechanism, you’re right that if (isMinor()) and
getAge() > 18 fail in exactly the same way. The reasons we single out Boolean are
about how often the pattern shows up and what it usually means:

  • Base rate. Returning null from a method that returns Integer/Long/Double
    is a mainstream idiom: Map::get, nullable JDBC/JPA columns, JSON deserialization,
    Integer.getInteger in the JDK itself. A rule flagging those declarations would fire
    on a very large share of perfectly ordinary code. A Boolean method returning null
    is much rarer, and in our experience more often unintentional than deliberate.
  • Type semantics. null as a third state in a two-valued type is worth calling out,
    because there are cheap alternatives that make the third state explicit (an enum,
    Optional<Boolean>). For Integer, null is the natural encoding of “absent” and
    there is no equally cheap alternative.
  • Call-site shape. Boolean methods are usually named isX()/hasX() and read as
    predicates, so they get dropped straight into an if with no visual hint that a
    conversion is happening. getAge() doesn’t read like a condition.

There’s also precedent: SpotBugs (NP_BOOLEAN_RETURN_NULL) and CERT EXP01-J single out
Boolean in the same way.

What the rule is actually asking for

This is the part our description does a poor job of conveying, and it’s worth stressing:
S2447 does not ask you to stop returning null. It’s a code smell about documentation,
not a bug about NPEs, the analyzer never looks at your callers here. It only asks that a
Boolean method which can return null says so in its signature, so that callers (and
our own analysis) know the third state is intentional.

So your tri-state example is compliant as soon as it’s declared:

@Nullable
public Boolean isMinor() {
  if (birthDate == null) {
    return null; // age unknown — a legitimate third state
  }
  return age < 18;
}

On the dependency concern: you very likely don’t need a new one. We recognise nullability
annotations from a long list of ecosystems, including JSpecify
(org.jspecify.annotations.Nullable), jakarta.annotation, javax.annotation, JetBrains,
Spring (org.springframework.lang.Nullable), the Checker Framework, Eclipse JDT, Android/
AndroidX, Reactor, RxJava and SpotBugs. Most projects already have one of these on the
classpath. As a side benefit, the annotation also makes rule S5411 (“Avoid using boxed
Boolean types directly in boolean expressions”) more effective at the call sites that
actually do the unboxing.

On extending the rule to Integer/Long/Double

We looked at this and decided against doing it at the declaration site, for the base-rate
reason above, it would probably produce a great deal of noise on code that is doing nothing wrong,
and the rule would simply get switched off.

That said, your consistency point isn’t wrong, it’s just aimed at the wrong end of the
problem. The more accurate description of the gap is this: for Boolean we cover both the
declaration (S2447) and the call site (S5411), while for the numeric wrappers we cover
neither. The place where the numeric risk is genuinely detectable is the call site, where
the unboxing is actually visible and we can tell whether the value is known to be nullable.
We’re taking that away as a rule idea to evaluate, thank you for prompting it.

What we’ll do in the short term

We’re going to rewrite the S2447 description (I opened a Jira ticket for the documentation changes: Jira ) to (a) explain why Boolean specifically,
rather than leaving an unboxing rationale that reads as if it should apply to every wrapper
type, (b) make it clear that a deliberate tri-state Boolean is a supported, compliant
pattern once annotated, and (c) list the annotations we accept so the “extra dependency”
question doesn’t come up again.

Thanks again for the detailed feedback!