-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Fix up some nullability annotations to remove unnecessary null-forgiving operations (!) #32186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
8308a9b
2d65d64
7ab2968
c19c05a
1ec16e4
c6ef742
c0aa571
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace System.Text.Json.Serialization.Converters | ||
| { | ||
|
|
@@ -36,7 +37,7 @@ internal override bool OnTryRead( | |
| Type typeToConvert, | ||
| JsonSerializerOptions options, | ||
| ref ReadStack state, | ||
| out TCollection value) | ||
| [MaybeNullWhen(false)] out TCollection value) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this, once #32090 is merged, we can remove the Everywhere we do: |
||
| { | ||
| bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace System.Text.Json.Serialization.Converters | ||
| { | ||
|
|
@@ -11,7 +12,7 @@ namespace System.Text.Json.Serialization.Converters | |
| /// </summary> | ||
| internal sealed class JsonObjectDefaultConverter<T> : JsonObjectConverter<T> | ||
| { | ||
| internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) | ||
| internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may become protected (instead of just internal) and I don't think we want to say this shouldn't be null. Also what's the difference between
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Even if we make it protected (and expose it), the nullability is showcasing the intent of the API. We are doing We can revisit the annotation if the implementation/design changes as part of exposing it. This change makes sense to me as it accurately represents what the API is doing so the caller knows what to do with the out parameter. |
||
| { | ||
| bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); | ||
| object obj; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| namespace System.Text.Json.Serialization | ||
| { | ||
|
|
@@ -392,6 +393,6 @@ internal void VerifyWrite(int originalDepth, Utf8JsonWriter writer) | |
| /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param> | ||
| /// <param name="value">The value to convert.</param> | ||
| /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param> | ||
| public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options); | ||
| public abstract void Write(Utf8JsonWriter writer, [NotNull] T value, JsonSerializerOptions options); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DisallowNull? |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,42 +14,32 @@ public static partial class JsonSerializer | |
| internal static readonly JsonEncodedText s_metadataRef = JsonEncodedText.Encode("$ref", encoder: null); | ||
| internal static readonly JsonEncodedText s_metadataValues = JsonEncodedText.Encode("$values", encoder: null); | ||
|
|
||
| internal static MetadataPropertyName GetResolvedReferenceHandling( | ||
| JsonConverter converter, | ||
| object value, | ||
| ref WriteStack state, | ||
| out string? referenceId) | ||
| { | ||
| if (!converter.CanHaveIdMetadata || converter.TypeToConvert.IsValueType) | ||
| { | ||
| referenceId = default; | ||
| return MetadataPropertyName.NoMetadata; | ||
| } | ||
|
|
||
| if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(value, out referenceId)) | ||
| { | ||
| return MetadataPropertyName.Ref; | ||
| } | ||
|
|
||
| return MetadataPropertyName.Id; | ||
| } | ||
|
|
||
| internal static MetadataPropertyName WriteReferenceForObject( | ||
| JsonConverter jsonConverter, | ||
| object currentValue, | ||
| ref WriteStack state, | ||
| Utf8JsonWriter writer) | ||
| { | ||
| MetadataPropertyName metadataToWrite = GetResolvedReferenceHandling(jsonConverter, currentValue, ref state, out string? referenceId); | ||
| MetadataPropertyName metadataToWrite; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was the motivation here only to remove the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that and perf (avoiding unnecessary/duplicate checks on the enum). |
||
|
|
||
| if (metadataToWrite == MetadataPropertyName.Ref) | ||
| // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata | ||
| if (!jsonConverter.CanHaveIdMetadata || jsonConverter.TypeToConvert.IsValueType) | ||
| { | ||
| metadataToWrite = MetadataPropertyName.NoMetadata; | ||
| } | ||
| else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId)) | ||
| { | ||
| writer.WriteString(s_metadataRef, referenceId!); | ||
| Debug.Assert(referenceId != null); | ||
| writer.WriteString(s_metadataRef, referenceId); | ||
| writer.WriteEndObject(); | ||
| metadataToWrite = MetadataPropertyName.Ref; | ||
| } | ||
| else if (metadataToWrite == MetadataPropertyName.Id) | ||
| else | ||
| { | ||
| writer.WriteString(s_metadataId, referenceId!); | ||
| // TryGetOrAddReferenceOnSerialize is guaranteed to not return null. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this comment necessary? The purpose of nullable reference types is you shouldn't need a comment like this, given the signature says "out string" rather than "out string?"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added it along with the I am fine removing the comment though. Should I remove the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The comment seemed superfluous. I'm fine with the Debug.Assert if you think it adds value. |
||
| Debug.Assert(referenceId != null); | ||
| writer.WriteString(s_metadataId, referenceId); | ||
| metadataToWrite = MetadataPropertyName.Id; | ||
| } | ||
|
|
||
| return metadataToWrite; | ||
|
|
@@ -61,24 +51,30 @@ internal static MetadataPropertyName WriteReferenceForCollection( | |
| ref WriteStack state, | ||
| Utf8JsonWriter writer) | ||
| { | ||
| MetadataPropertyName metadataToWrite = GetResolvedReferenceHandling(jsonConverter, currentValue, ref state, out string? referenceId); | ||
| MetadataPropertyName metadataToWrite; | ||
|
|
||
| if (metadataToWrite == MetadataPropertyName.NoMetadata) | ||
| // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata | ||
| if (!jsonConverter.CanHaveIdMetadata || jsonConverter.TypeToConvert.IsValueType) | ||
| { | ||
| writer.WriteStartArray(); | ||
| metadataToWrite = MetadataPropertyName.NoMetadata; | ||
| } | ||
| else if (metadataToWrite == MetadataPropertyName.Id) | ||
| else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId)) | ||
| { | ||
| Debug.Assert(referenceId != null); | ||
| writer.WriteStartObject(); | ||
| writer.WriteString(s_metadataId, referenceId!); | ||
| writer.WriteStartArray(s_metadataValues); | ||
| writer.WriteString(s_metadataRef, referenceId); | ||
| writer.WriteEndObject(); | ||
| metadataToWrite = MetadataPropertyName.Ref; | ||
| } | ||
| else | ||
| { | ||
| Debug.Assert(metadataToWrite == MetadataPropertyName.Ref); | ||
| // TryGetOrAddReferenceOnSerialize is guaranteed to not return null. | ||
| Debug.Assert(referenceId != null); | ||
| writer.WriteStartObject(); | ||
| writer.WriteString(s_metadataRef, referenceId!); | ||
| writer.WriteEndObject(); | ||
| writer.WriteString(s_metadataId, referenceId); | ||
| writer.WriteStartArray(s_metadataValues); | ||
| metadataToWrite = MetadataPropertyName.Id; | ||
| } | ||
|
|
||
| return metadataToWrite; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NotNull doesn't make sense here. Did you mean DisallowNull? And just so I understand, it's invalid to write nulls but read can return nulls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Writewon't receive null butReadcan return null.The intent is the
JsonSerializerwill never pass in null T values to theJsonConverter<T>.Writemethod and hence the implementer of that method doesn't need to do a null check on it.They can certainly continue to write null JSON literals in the method itself or do whatever else. If an nullable object has the
nullvalue, the serializer writes null for you (and doesn't call theJsonConverter).On the read side, the implementer should honor the nullability of the T. If T is nullable, the implementer of
JsonConverter<T>.Readcan certainly return null if they wish. If T is non-nullable (like string, or value type), it doesn't make sense for them to do so.For types where null doesn't make sense (i.e. non-nullable valuetypes), the
JsonSerializercan and will pass in aUtf8JsonReaderwith thenullTokenType if the payload we are processing has the null JSON literal.For types where null is allowed (nullable/non-nullable reference types), the
JsonSerializerwill not pass in aUtf8JsonReaderin that state and will eagerly set the object to null up front, without calling theJsonConverter.