When adopting JSpecify to introduce standard null-safety annotations into your Java projects, one of the most common questions revolves around scope. If you declare @NullMarked in package-info.java, does it make every field in that package non-null by default? And what does that mean for standard POJOs, default constructors, or JPA entities?

The Short Answer: Yes, @NullMarked Includes Fields

According to the JSpecify specification, @NullMarked applies to all type usages within its enclosing scope. That means classes, method return types, parameters, type arguments, and field declarations within that package are treated as non-null by default unless explicitly annotated with @Nullable.

Your reasoning is correct. In a @NullMarked package, this code:

class MyPojo {
    private String data;

    public void setData(String data) {
        this.data = data;
    }

    public String getData() {
        return this.data;
    }
}

is semantically identical to:

class MyPojo {
    private @NonNull String data;

    public void setData(@NonNull String data) {
        this.data = data;
    }

    public @NonNull String getData() {
        return this.data;
    }
}

Is Calling new MyPojo() Legal?

From a plain Java compiler perspective (javac), the code compiles fine because annotations like JSpecify do not alter standard bytecode execution or compilation rules. However, null-checking tools (such as NullAway, Checker Framework, SonarQube, or IntelliJ IDEA inspections) will flag this as a contract violation.

When an instance is created via the default zero-argument constructor, data holds a runtime value of null. If getData() is called immediately, it will return null despite the contract claiming the return value is non-null. Static analysis tools that enforce initialization will warn you:

[NullAway] @NonNull field data not initialized in constructor or declaration

How to Handle POJOs and JPA Entities in @NullMarked Packages

Frameworks like JPA, Hibernate, Jackson, and Spring often rely on no-arg constructors and reflective population of fields. Here are the best ways to bridge this gap:

1. Mark Uninitialized or Optional Fields as @Nullable

If a field can be absent or is only populated later in the lifecycle (after instantiation), make the contract honest by adding @Nullable:

import org.jspecify.annotations.Nullable;

public class MyPojo {
    private @Nullable String data;

    public void setData(@Nullable String data) {
        this.data = data;
    }

    public @Nullable String getData() {
        return this.data;
    }
}

2. Provide Safe Default Values

For domain models or DTOs where nulls should be avoided entirely, initialize the field directly at declaration or in the constructor:

public class MyPojo {
    private String data = "";

    public MyPojo(String data) {
        this.data = Objects.requireNonNull(data);
    }
}

3. Configure Static Analysis Tooling for Framework Lifecycles

Most static analyzers understand that JPA entities and serialized POJOs are initialized by reflection rather than normal constructors. For instance, in NullAway, you can configure:

  • -XepOpt:NullAway:CustomContractAnnotations or framework-specific flags.
  • Exclusions for packages containing entities.
  • Suppression annotations (like @SuppressWarnings("initialization")) on fields intended for JPA or Jackson.

Should You Put JPA Entities in a @NullMarked Package?

Yes, but with care:

  • It provides value: It prevents other parts of your application from passing unexpected null values to entity setters and ensures safe navigation when getters return non-null types.
  • Database nullable columns: Any column in your database table that is nullable should map to a field annotated with @Nullable.
  • Identifiers (IDs): Primary keys generated by the database (like @GeneratedValue Long id) are technically null prior to persistence. They should typically be marked @Nullable Long id or initialized via dedicated builder patterns to remain strictly null-safe.

Summary

@NullMarked does not ignore fields—it treats them as non-null by default. If your class uses a zero-arg constructor without field initializers, static analysis tools will warn you that the non-null contract is broken. For JPA entities and POJOs, explicitly mark optional or lazily-populated fields with @Nullable or configure your analyzer to recognize framework initialization hooks.