Skip to content
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

Annotate System.Text.Json for nullable #114

Closed
wants to merge 10 commits into from
Closed

Annotate System.Text.Json for nullable #114

wants to merge 10 commits into from

Conversation

buyaa-n
Copy link
Contributor

@buyaa-n buyaa-n commented Nov 18, 2019


JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo;
JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo!;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These properties state.Current.JsonPropertyInfo and state.Current.JsonPropertyInfo.ElementClassInfo are nullable but always set in case of Dictionary or Enumerable type, so just banged them

foreach (DictionaryEntry item in sourceDictionary)
#pragma warning restore 8605
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't found better way to suppress Unboxing a possible null value warning CC @stephentoub @safern

{
Debug.Assert((_elementPropertyInfo ?? ElementClassInfo.PolicyProperty) != null);
(_elementPropertyInfo ?? ElementClassInfo.PolicyProperty).AddObjectToParentEnumerable(addMethodDelegate, value);
Debug.Assert((_elementPropertyInfo ?? ElementClassInfo?.PolicyProperty) != null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ElementClassInfo is nullable

#pragma warning disable 8609
public override string? Message { get { throw null; } }
#pragma warning restore 8609
public string? Path { get { throw null; } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto; should we make the path in the ctor be non-nullable, and then make this non-nullable, too?

@ahsonkhan, @steveharter, can you help review this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make the path in the ctor be non-nullable, and then make this non-nullable, too?

Changing the (string) and (string, Exception) ctors to assign "" instead of null?

Copy link
Contributor Author

@buyaa-n buyaa-n Dec 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So am gonna change it non null and set empty string wherever it setting null. There is also assert checking it for null but seems useless will, delete it

public static System.Threading.Tasks.ValueTask<TValue> DeserializeAsync<TValue>(System.IO.Stream utf8Json, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static TValue Deserialize<TValue>(System.ReadOnlySpan<byte> utf8Json, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
public static TValue Deserialize<TValue>(string json, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
public static TValue Deserialize<TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steveharter, can (and in what situations) might Deserialize return null? I'm wondering if we need to annotate these Deserialize methods with [return: MaybeNull].

Copy link
Contributor

@ahsonkhan ahsonkhan Nov 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Deserialize can return null. If the JSON payload has the "null" value and we are deserializing into a nullable reference type (like string, object, int?, etc.).

Assert.Null(JsonSerializer.Deserialize<string>("null"));
Assert.Null(JsonSerializer.Deserialize<object>("null"));
Assert.Null(JsonSerializer.Deserialize<int?>("null"));
Assert.Null(JsonSerializer.Deserialize<SimpleTestClass>("null"));

I'm wondering if we need to annotate these Deserialize methods with [return: MaybeNull].

Yes we should.

I am not super familiar with the nullable annotation attributes. Do they only apply to return/out values of APIs?

Copy link
Contributor Author

@buyaa-n buyaa-n Nov 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not super familiar with the nullable annotation attributes. Do they only apply to return/out values of APIs?

Some attributes for return/out, some for input/set. [MaybeNull] apply to return/out for generic type.

Yes, Deserialize can return null. If the JSON payload has the "null" value and we are deserializing into a nullable reference type (like string, object, int?, etc.).

We shoudl use [return : MaybeNull] for above method if the method could return null for non nullable ref types. FYI non nullable ref types are <string>, <object> nullable ref types are <string?>, <object?>. Your test below has issue for nullability enabled case:

Assert.Null(JsonSerializer.Deserialize<string>("null"));
Assert.Null(JsonSerializer.Deserialize<object>("null"));

When nullable enabled you cannot pass null implicitly for type parameter, null allowed if you use <string?> instead, above test would look like:

Assert.Null(JsonSerializer.Deserialize<string?>("null"));
Assert.Null(JsonSerializer.Deserialize<object?>("null"));

So the question is if the deserializer could return null in any non null value is passed for ref type, if so we should annotate it with [return: MaybeNull].

@@ -106,7 +107,7 @@ public bool GetBoolean()
/// </exception>
public byte[] GetBytesFromBase64()
{
if (!TryGetBytesFromBase64(out byte[] value))
if (!TryGetBytesFromBase64(out byte[]? value))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is that how we usually annotate the try methods?

The annotation makes you think that the out parameter can be null (since we have [NotNullWhen(true)]) but we throw in that case. The value that we return can never be null which we are conveying by the method signature, so I see why that would make sense.

I am asking because if method is doing a bunch of complex work, then having value appear to contain null value even after the throw, could be confusing.

For example:

public byte[] GetBytesFromBase64()
{
    if (!TryGetBytesFromBase64(out byte[]? value))
    {
        throw ThrowHelper.GetFormatException(DataType.Base64String);
    }

    byte[]? temp = value; // Why is this allowed?
    if (temp == null)
    {
        throw new Exception("We can't get here, compiler please complain!");
    }

    return value;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is that how we usually annotate the try methods?

Yes it is. as TryXXX methods annotated to receive nullable out (sometimes ref) parameters calling routines should send nullable argument as that value can be set null in case of unsuccessful load.

As you mentioned value will not be null after throw, not sure why you think compiler still assume value can be null, as you can see we did not banged returning value because compiler evaluated that it will not be null when TryGetBytesFromBase64(...) returned true.

@@ -511,7 +511,7 @@ public bool ValueTextEquals(ReadOnlySpan<char> text)
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file looks good.

@@ -583,7 +598,7 @@ public partial struct JsonWriterOptions
public bool TrySkip() { throw null; }
public bool ValueTextEquals(System.ReadOnlySpan<byte> utf8Text) { throw null; }
public bool ValueTextEquals(System.ReadOnlySpan<char> text) { throw null; }
public bool ValueTextEquals(string text) { throw null; }
public bool ValueTextEquals(string? text) { throw null; }
}
public sealed partial class Utf8JsonWriter : System.IAsyncDisposable, System.IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes to the Utf8JsonWriter and related types look good.

@@ -14,7 +14,7 @@ namespace System.Text.Json
public class JsonException : Exception
{
// Allow the message to mutate to avoid re-throwing and losing the StackTrace to an inner exception.
private string _message;
internal string? _message;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the nullablility flow changing this from private to internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is kind of a long story. Exception.Message getter shouldn't be nullable (contract) so we changed the getter from return _message into return _message ?? base.Message;. But there is a logic in ThrowHelper set the message when Message is null set t https://github.com/dotnet/runtime/pull/114/files#diff-e586edfb9caf385bf493d49a97f197a7R217 which not setting correct message anymore after changing Message to non null. So instead of using Message property we changed it to use this field direclty ex._message; for which the field need to be internal

@buyaa-n
Copy link
Contributor Author

buyaa-n commented Dec 4, 2019

Closing this PR as this one is not accessable anymore, new one created #528 and comments addressed

@buyaa-n buyaa-n closed this Dec 4, 2019
@ahsonkhan ahsonkhan added the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label Feb 14, 2020
@ahsonkhan
Copy link
Contributor

Highlighting the breaking change here (which became visible when enabling nullability analysis).

Deserializing into a char is now more strict to match user expectations. The payload must contain a single-character string for deserialization to succeed:

public class MyPoco
{
    public char Character { get; set; }
}

public static void TestDeserializeToChar()
{
    // Before (3.1): NullReferenceException
    // After (5.0): JsonException
    JsonSerializer.Deserialize<MyPoco>("{\"Character\": null}");

    // Before (3.1): IndexOutOfRangeException
    // After (5.0): JsonException
    JsonSerializer.Deserialize<MyPoco>("{\"Character\": \"\"}");

    // Before (3.1): Set Character to the first character - 'a'
    // After (5.0): JsonException
    JsonSerializer.Deserialize<MyPoco>("{\"Character\": \"abc\"}");
}

Passing in null for the Type parameter while serializing now correctly throws ArgumentNullException rather than returning "null" JSON:

public static void TestSerializeNullType()
{
    // Before (3.1): Returned a string with value "null"
    // After (5.0): ArgumentNullException: Value cannot be null. (Parameter 'type')
    JsonSerializer.Serialize(null, null);

    // Same with other Serialize{Async} overloads
    // Before (3.1): Returned a byte[] with value "null"
    // After (5.0): ArgumentNullException: Value cannot be null. (Parameter 'type')
    JsonSerializer.SerializeToUtf8Bytes(null, null);
}

@NoneGiven
Copy link

If we're making breaking changes to match user expectation, how about #1198?

@ahsonkhan
Copy link
Contributor

Not all breaking changes are "equivalent" in terms of impact and expectation (changing the output from serialization which can be persisted has a higher bar than changing deserialization behavior). That said, the issue to fix the keyvaluepair issue is still open and targeting 5.0 (#1197), so I suspect it too will get fixed after proper due diligence/evaluating the impact :)

Discussion regarding that issue should be made there.

@ahsonkhan ahsonkhan removed the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label Feb 20, 2020
@ahsonkhan
Copy link
Contributor

Since this PR wasn't merged, removing label and moving to the one that actually got merged:
#528 (comment)

MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Oct 12, 2020
- Add osx RuntimeIdentifiers
- Make up RIDs
- Fix build breaks
- UseAppHost unconditionally
- CMake cleanup

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants