Specify nullness for nodes in the graphql.language package#3899
Specify nullness for nodes in the graphql.language package#3899dondonz merged 9 commits intographql-java:masterfrom
graphql.language package#3899Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR applies nullability annotations across the graphql.language package to clarify which fields and parameters can be null.
- Added @NullMarked and @nullable annotations to key classes and constructors.
- Updated Builder classes for several Value types to accept null parameters.
- Adjusted method signatures (e.g. isEqualTo) and field declarations to reflect nullability.
Reviewed Changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| VariableReference.java | Annotated the class and its Builder with nullability hints. |
| Value.java | Marked the interface with @NullMarked. |
| StringValue.java | Adjusted the field and getter for value to be @nullable. |
| ScalarValue.java | Applied @NullMarked to the interface. |
| ObjectValue.java | Annotated constructors and Builder methods with @nullable. |
| NullValue.java | Updated constructor and Builder to support null SourceLocation. |
| Node.java | Updated getSourceLocation and isEqualTo for nullability. |
| IntValue.java, FloatValue.java, EnumValue.java, BooleanValue.java, ArrayValue.java, AbstractNode.java | Applied similar nullability annotations to constructors, methods, and Builder classes. |
| private @Nullable SourceLocation sourceLocation; | ||
| private ImmutableList<Comment> comments = emptyList(); | ||
| private String name; | ||
| private @Nullable String name; |
There was a problem hiding this comment.
The VariableReference.Builder now allows a null name. Consider adding validation in the build() method to ensure that a non-null name is provided if that is an invariant in your domain.
There was a problem hiding this comment.
name in the above constructor and builder method are not nullable, did you meant to make this field nullable?
I think in practice, a VariableReference should always have a non-nullable name
| private SourceLocation sourceLocation; | ||
| private String name; | ||
| private @Nullable SourceLocation sourceLocation; | ||
| private @Nullable String name; |
There was a problem hiding this comment.
The EnumValue.Builder permits a null name. It may be beneficial to enforce a non-null name during object construction if that is required by the API contract.
There was a problem hiding this comment.
Hello, thanks for your patience while away for Easter.
In this builder, name is set to nullable, but the constructor above has name as non-nullable, and the builder method below has it as non-nullable. Did you mean for the Builder's name to be instead non-nullable?
There was a problem hiding this comment.
No, nullable fields in the builder are expected.
By default, when a builder instance is created, all values are set to their defaults (null for reference fields) - thus, it's necessary to mark these fields @Nullable.
This initial Builder state represents an incomplete configuration, and the EnumValue should not be constructible in this state - calling the build() method should result in an IllegalStateException.
I planned to add something like this later:
public EnumValue build() {
if (name == null) {
throw new IllegalStateException("name must be set");
}
return new EnumValue(name, sourceLocation, comments, ignoredChars, additionalData);
}| private SourceLocation sourceLocation; | ||
| private BigInteger value; | ||
| private @Nullable SourceLocation sourceLocation; | ||
| private @Nullable BigInteger value; |
There was a problem hiding this comment.
The IntValue.Builder now accepts a null value. Consider adding a check in the build() method to prevent creating an IntValue instance with a null value if that contradicts expected behavior.
There was a problem hiding this comment.
Same comment as for FloatValue, I will double check if a null value is in practice possible or not
| private SourceLocation sourceLocation; | ||
| private BigDecimal value; | ||
| private @Nullable SourceLocation sourceLocation; | ||
| private @Nullable BigDecimal value; |
There was a problem hiding this comment.
The FloatValue.Builder now permits a null value. If a non-null value is expected, it would be prudent to validate this in the build() method to prevent possible misuse, as noted in the PR description.
There was a problem hiding this comment.
This builder should be consistent with the constructor for value
@andimarek @bbakerman: is it possible in practice for FloatValue to ever be constructed with a null? Or would such an object always be constructed as a NullValue?
|
Giving Copilot a try for fun, I'm curious what it says |
| private SourceLocation sourceLocation; | ||
| private String value; | ||
| private @Nullable SourceLocation sourceLocation; | ||
| private @Nullable String value; |
There was a problem hiding this comment.
Similar to above Value classes, I will double check if value can be nullable or if in practice it is represented by a NullValue
|
Coming back in here to say thanks for waiting and thanks for the PR. We have a new PR up to add ErrorProne & NullAway checks which I'd like to merge in before new JSpecify PRs |
| public ArrayValue deepCopy() { | ||
| return new ArrayValue(deepCopy(values), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); | ||
| List<Value> copiedValues = deepCopy(values); | ||
| return new ArrayValue(copiedValues != null ? copiedValues : emptyList(), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); |
There was a problem hiding this comment.
Want to call out this is a small change to not allow a null value. A list will be used instead. Without this change, there is a NullAway error
| return builder.build(); | ||
| } | ||
|
|
||
| @NullUnmarked |
There was a problem hiding this comment.
After giving a few classes the JSpecify treatment: we now generally annotate Builders with @NullUnmarked
| public ObjectValue deepCopy() { | ||
| return new ObjectValue(deepCopy(objectFields), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); | ||
| List<ObjectField> copiedFields = deepCopy(objectFields); | ||
| return new ObjectValue(copiedFields != null ? copiedFields : emptyList(), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); |
There was a problem hiding this comment.
Similar to ArrayValue, want to call out this is a small change to not allow a null value. A list will be used instead. Without this change, there is a NullAway error
|
Hello after a few other PRs with JSpecify annotations, we settled on using We've also since added NullAway compile time checks which identified a very small extra change, to make the values of both ArrayValue and ObjectValue empty lists rather than null. Thanks for the PR! |
|
|
||
| @SuppressWarnings("unchecked") | ||
| protected <V extends Node> V deepCopy(V nullableObj) { | ||
| protected <V extends @Nullable Node> @Nullable V deepCopy(@Nullable V nullableObj) { |
There was a problem hiding this comment.
I don't understand this change @dondonz ... why should we allow null values here?
There was a problem hiding this comment.
The first line we check whether the input is nullable if (nullableObj == null) { so I have allowed the input to be nullable
I agree it's strange in the context of Value like FloatValue.
Maybe there was another reason to allow this to be nullable in the first place?
There was a problem hiding this comment.
So we have two options here: either declare V as a type that also can be null or declare input and output as Nullable. We have done both here, which doesn't make sense I think.
There was a problem hiding this comment.
Repeating conversation here: I will change the method signature to become
protected <V extends Node> @Nullable V deepCopy(@Nullable V nullableObj)
This in English is meant to express that V extending Node is not nullable, but because the method will handle null inputs, it may still return a null
andimarek
left a comment
There was a problem hiding this comment.
Can be merged, but also could first fix the small comment I left.
|
|
||
| @SuppressWarnings("unchecked") | ||
| protected <V extends Node> V deepCopy(V nullableObj) { | ||
| protected <V extends @Nullable Node> @Nullable V deepCopy(@Nullable V nullableObj) { |
There was a problem hiding this comment.
So we have two options here: either declare V as a type that also can be null or declare input and output as Nullable. We have done both here, which doesn't make sense I think.
| @Override | ||
| public ArrayValue deepCopy() { | ||
| return new ArrayValue(deepCopy(values), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); | ||
| List<Value> copiedValues = assertNotNull(deepCopy(values)); |
There was a problem hiding this comment.
Add an assert here to keep NullAway happy
In practice it is not possible to have null values. Have to put this here otherwise you'll get a NullAway error
| @Override | ||
| public ObjectValue deepCopy() { | ||
| return new ObjectValue(deepCopy(objectFields), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData()); | ||
| List<ObjectField> copiedFields = assertNotNull(deepCopy(objectFields)); |
There was a problem hiding this comment.
Add this assert to keep NullAway happy, even though objectFields is not null
|
@dondonz We are working to upgrade to GraphQL-Java v25. We use Kotlin so nullability annotations directly affect our ability to compile. I'm a bit confused about why StringValue.getValue() is now null. I looked through the codebase and could not find any instances where a StringValue is constructed with a null value, which makes sense — there's a NullValue in the type hierarchy to cover that case. As far as I can tell, the impact of declaring StringValue.getValue() as nullable rather than non-null is just to force anyone writing software in a way that depends on these annotations (like Kotlin) to insert pointless null checks that will never fail. I suppose that declaring the value as nullable is a compatibility issue since users could be creating StringValues with null value ourselves, but is there a real use case for this, or could we change StringValue in v26 to require (and check!) non-null value? |
Related: #3878
I applied
@NullMarkedand@Nullableannotations to these classes:graphql.language.Node- partially annotated with@Nullablegraphql.language.AbstractNodegraphql.language.ArrayValuegraphql.language.BooleanValuegraphql.language.EnumValuegraphql.language.FloatValuegraphql.language.IntValuegraphql.language.NullValuegraphql.language.ObjectValuegraphql.language.ScalarValuegraphql.language.StringValue- I was not sure about the nullability ofvalue, for now I marked it@Nullable.graphql.language.Valuegraphql.language.VariableReferenceNotes
SourceLocation sourceLocation- can contain a null value in all placesPotential problems (will be pointed by #3852 )
EnumValue.Builder#build()can build anEnumValueobject with a nullnameFloatValue.Builder#build()can build anFloatValueobject with a nullvalueIntValue.Builder#build()can build anIntValueobject with a nullvalueVariableReference.Builder#build()can build anVariableReferenceobject with a nullname