Change (string)null behaviors in Utf8JsonWriter
authorJeremy Barton <jbarton@microsoft.com>
Sat, 13 Jul 2019 14:20:12 +0000 (07:20 -0700)
committerGitHub <noreply@github.com>
Sat, 13 Jul 2019 14:20:12 +0000 (07:20 -0700)
ArgumentNullException when used as a property name, empty string
as a property name if it's already in ReadOnlySpan<char>.

* WriteNull(string)
* WriteBoolean(string, bool)
* WriteNumber(string, *)
* WriteString(string, *)
* WriteStartArray(string)
* WriteStartObject(string)
* WritePropertyName(string)

Emits the null literal (vs the empty string):

* WriteStringValue(string)
* WriteString(*, string)

Since the writer will write null via WriteStringValue, the reader GetString()
now will return null if TokenType is null (matches JsonElement behavior).

Commit migrated from https://github.com/dotnet/corefx/commit/adce92338911eec22e5c8f9f071ece857de7d624

19 files changed:
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Comment.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs
src/libraries/System.Text.Json/tests/Serialization/Value.WriteTests.cs
src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs
src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.cs
src/libraries/System.Text.Json/tests/Utf8JsonWriterTests.cs

index a4ff13a..c6700c5 100644 (file)
@@ -13,14 +13,23 @@ namespace System.Text.Json
         /// <summary>
         /// Parses the current JSON token value from the source, unescaped, and transcoded as a <see cref="string"/>.
         /// </summary>
+        /// <remarks>
+        /// Returns <see langword="null" /> when <see cref="TokenType"/> is <see cref="JsonTokenType.Null"/>.
+        /// </remarks>
         /// <exception cref="InvalidOperationException">
         /// Thrown if trying to get the value of the JSON token that is not a string
-        /// (i.e. other than <see cref="JsonTokenType.String"/> or <see cref="JsonTokenType.PropertyName"/>).
+        /// (i.e. other than <see cref="JsonTokenType.String"/>, <see cref="JsonTokenType.PropertyName"/> or
+        /// <see cref="JsonTokenType.Null"/>).
         /// <seealso cref="TokenType" />
         /// It will also throw when the JSON string contains invalid UTF-8 bytes, or invalid UTF-16 surrogates.
         /// </exception>
         public string GetString()
         {
+            if (TokenType == JsonTokenType.Null)
+            {
+                return null;
+            }
+
             if (TokenType != JsonTokenType.String && TokenType != JsonTokenType.PropertyName)
             {
                 throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType);
index cf10dcd..75ac62a 100644 (file)
@@ -47,11 +47,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteBase64String(string propertyName, ReadOnlySpan<byte> bytes)
-            => WriteBase64String(propertyName.AsSpan(), bytes);
+            => WriteBase64String((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), bytes);
 
         /// <summary>
         /// Writes the property name and raw bytes value (as a base 64 encoded JSON string) as part of a name/value pair of a JSON object.
index b944e4a..12a90b7 100644 (file)
@@ -43,6 +43,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -51,7 +54,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteString(string propertyName, DateTime value)
-            => WriteString(propertyName.AsSpan(), value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="DateTime"/> value (as a JSON string) as part of a name/value pair of a JSON object.
index 0e3f907..556e962 100644 (file)
@@ -43,6 +43,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -51,7 +54,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteString(string propertyName, DateTimeOffset value)
-            => WriteString(propertyName.AsSpan(), value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="DateTimeOffset"/> value (as a JSON string) as part of a name/value pair of a JSON object.
index 0b27787..39f2f24 100644 (file)
@@ -43,6 +43,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -51,7 +54,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteNumber(string propertyName, decimal value)
-            => WriteNumber(propertyName.AsSpan(), value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="decimal"/> value (as a JSON number) as part of a name/value pair of a JSON object.
index 0fb4ee4..c6f1d97 100644 (file)
@@ -45,6 +45,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -53,7 +56,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteNumber(string propertyName, double value)
-            => WriteNumber(propertyName.AsSpan(), value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="double"/> value (as a JSON number) as part of a name/value pair of a JSON object.
index 2c8898c..7553e38 100644 (file)
@@ -45,6 +45,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -53,7 +56,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteNumber(string propertyName, float value)
-            => WriteNumber(propertyName.AsSpan(), value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="float"/> value (as a JSON number) as part of a name/value pair of a JSON object.
index db80b2b..694f8ec 100644 (file)
@@ -43,6 +43,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -51,7 +54,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteString(string propertyName, Guid value)
-            => WriteString(propertyName.AsSpan(), value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="Guid"/> value (as a JSON string) as part of a name/value pair of a JSON object.
index 7034339..d0db932 100644 (file)
@@ -44,11 +44,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteNull(string propertyName)
-            => WriteNull(propertyName.AsSpan());
+            => WriteNull((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan());
 
         /// <summary>
         /// Writes the property name and the JSON literal "null" as part of a name/value pair of a JSON object.
@@ -136,11 +139,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteBoolean(string propertyName, bool value)
-            => WriteBoolean(propertyName.AsSpan(), value);
+            => WriteBoolean((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="bool"/> value (as a JSON literal "true" or "false") as part of a name/value pair of a JSON object.
index 3a64538..e651ffd 100644 (file)
@@ -43,6 +43,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -51,7 +54,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteNumber(string propertyName, long value)
-            => WriteNumber(propertyName.AsSpan(), value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="long"/> value (as a JSON number) as part of a name/value pair of a JSON object.
@@ -126,6 +129,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -134,7 +140,7 @@ namespace System.Text.Json
         /// The property name is escaped before writing.
         /// </remarks>
         public void WriteNumber(string propertyName, int value)
-            => WriteNumber(propertyName.AsSpan(), (long)value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), (long)value);
 
         /// <summary>
         /// Writes the property name and <see cref="int"/> value (as a JSON number) as part of a name/value pair of a JSON object.
index cea0c82..5d77fd6 100644 (file)
@@ -42,11 +42,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WritePropertyName(string propertyName)
-            => WritePropertyName(propertyName.AsSpan());
+            => WritePropertyName((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan());
 
         /// <summary>
         /// Writes the property name (as a JSON string) as the first part of a name/value pair of a JSON object.
@@ -391,11 +394,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(string propertyName, JsonEncodedText value)
-            => WriteString(propertyName.AsSpan(), value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and string text value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -403,16 +409,39 @@ namespace System.Text.Json
         /// <param name="propertyName">The property name of the JSON object to be transcoded and written as UTF-8.</param>
         /// <param name="value">The value to be written as a UTF-8 transcoded JSON string as part of the name/value pair.</param>
         /// <remarks>
+        /// <para>
         /// The property name and value is escaped before writing.
+        /// </para>
+        /// <para>
+        /// If <paramref name="value"/> is <see langword="null"/> the JSON null value is written,
+        /// as if <see cref="WriteNull(System.ReadOnlySpan{byte})"/> were called.
+        /// </para>
         /// </remarks>
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name or value is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(string propertyName, string value)
-            => WriteString(propertyName.AsSpan(), value.AsSpan());
+        {
+            if (propertyName == null)
+            {
+                throw new ArgumentNullException(nameof(propertyName));
+            }
+
+            if (value == null)
+            {
+                WriteNull(propertyName.AsSpan());
+            }
+            else
+            {
+                WriteString(propertyName.AsSpan(), value.AsSpan());
+            }
+        }
 
         /// <summary>
         /// Writes the property name and text value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -468,7 +497,14 @@ namespace System.Text.Json
         /// <param name="propertyName">The JSON encoded property name of the JSON object to be transcoded and written as UTF-8.</param>
         /// <param name="value">The value to be written as a UTF-8 transcoded JSON string as part of the name/value pair.</param>
         /// <remarks>
-        /// The property name should already be escaped when the instance of <see cref="JsonEncodedText"/> was created. The value is escaped before writing.
+        /// <para>
+        /// The property name should already be escaped when the instance of <see cref="JsonEncodedText"/> was created.
+        /// The value is escaped before writing.
+        /// </para>
+        /// <para>
+        /// If <paramref name="value"/> is <see langword="null"/> the JSON null value is written,
+        /// as if <see cref="WriteNull(System.Text.Json.JsonEncodedText)"/> was called.
+        /// </para>
         /// </remarks>
         /// <exception cref="ArgumentException">
         /// Thrown when the specified value is too large.
@@ -477,7 +513,16 @@ namespace System.Text.Json
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(JsonEncodedText propertyName, string value)
-            => WriteString(propertyName, value.AsSpan());
+        {
+            if (value == null)
+            {
+                WriteNull(propertyName);
+            }
+            else
+            {
+                WriteString(propertyName, value.AsSpan());
+            }
+        }
 
         /// <summary>
         /// Writes the pre-encoded property name and text value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -530,11 +575,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name or value is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(string propertyName, ReadOnlySpan<char> value)
-            => WriteString(propertyName.AsSpan(), value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the UTF-8 property name and text value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -611,11 +659,14 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name or value is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(string propertyName, ReadOnlySpan<byte> utf8Value)
-            => WriteString(propertyName.AsSpan(), utf8Value);
+            => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), utf8Value);
 
         /// <summary>
         /// Writes the property name and UTF-8 text value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -687,7 +738,13 @@ namespace System.Text.Json
         /// <param name="propertyName">The property name of the JSON object to be transcoded and written as UTF-8.</param>
         /// <param name="value">The value to be written as a UTF-8 transcoded JSON string as part of the name/value pair.</param>
         /// <remarks>
-        /// The property name and value is escaped before writing.
+        /// <para>
+        /// The property name and value are escaped before writing.
+        /// </para>
+        /// <para>
+        /// If <paramref name="value"/> is <see langword="null"/> the JSON null value is written,
+        /// as if <see cref="WriteNull(System.ReadOnlySpan{char})"/> was called.
+        /// </para>
         /// </remarks>
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name or value is too large.
@@ -696,7 +753,16 @@ namespace System.Text.Json
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(ReadOnlySpan<char> propertyName, string value)
-            => WriteString(propertyName, value.AsSpan());
+        {
+            if (value == null)
+            {
+                WriteNull(propertyName);
+            }
+            else
+            {
+                WriteString(propertyName, value.AsSpan());
+            }
+        }
 
         /// <summary>
         /// Writes the UTF-8 property name and pre-encoded value (as a JSON string) as part of a name/value pair of a JSON object.
@@ -744,7 +810,13 @@ namespace System.Text.Json
         /// <param name="utf8PropertyName">The UTF-8 encoded property name of the JSON object to be written.</param>
         /// <param name="value">The value to be written as a UTF-8 transcoded JSON string as part of the name/value pair.</param>
         /// <remarks>
-        /// The property name and value is escaped before writing.
+        /// <para>
+        /// The property name and value are escaped before writing.
+        /// </para>
+        /// <para>
+        /// If <paramref name="value"/> is <see langword="null"/> the JSON null value is written,
+        /// as if <see cref="WriteNull(System.ReadOnlySpan{byte})"/> was called.
+        /// </para>
         /// </remarks>
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name or value is too large.
@@ -753,7 +825,16 @@ namespace System.Text.Json
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteString(ReadOnlySpan<byte> utf8PropertyName, string value)
-            => WriteString(utf8PropertyName, value.AsSpan());
+        {
+            if (value == null)
+            {
+                WriteNull(utf8PropertyName);
+            }
+            else
+            {
+                WriteString(utf8PropertyName, value.AsSpan());
+            }
+        }
 
         private void WriteStringEscapeValueOnly(ReadOnlySpan<byte> escapedPropertyName, ReadOnlySpan<byte> utf8Value, int firstEscapeIndex)
         {
index b57ebea..94706ec 100644 (file)
@@ -44,6 +44,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -53,7 +56,7 @@ namespace System.Text.Json
         /// </remarks>
         [CLSCompliant(false)]
         public void WriteNumber(string propertyName, ulong value)
-            => WriteNumber(propertyName.AsSpan(), value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value);
 
         /// <summary>
         /// Writes the property name and <see cref="ulong"/> value (as a JSON number) as part of a name/value pair of a JSON object.
@@ -131,6 +134,9 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
@@ -140,7 +146,7 @@ namespace System.Text.Json
         /// </remarks>
         [CLSCompliant(false)]
         public void WriteNumber(string propertyName, uint value)
-            => WriteNumber(propertyName.AsSpan(), (ulong)value);
+            => WriteNumber((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), (ulong)value);
 
         /// <summary>
         /// Writes the property name and <see cref="uint"/> value (as a JSON number) as part of a name/value pair of a JSON object.
index baee4d2..ef7ab6b 100644 (file)
@@ -23,8 +23,11 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified value is too large OR if the given string text value contains a comment delimiter (i.e. */).
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="value"/> parameter is <see langword="null"/>.
+        /// </exception>
         public void WriteCommentValue(string value)
-            => WriteCommentValue(value.AsSpan());
+            => WriteCommentValue((value ?? throw new ArgumentNullException(nameof(value))).AsSpan());
 
         /// <summary>
         /// Writes the text value (as a JSON comment).
index 797b40d..6623b0c 100644 (file)
@@ -37,7 +37,11 @@ namespace System.Text.Json
         /// </summary>
         /// <param name="value">The value to be written as a UTF-8 transcoded JSON string element of a JSON array.</param>
         /// <remarks>
-        /// The value is escaped before writing.
+        /// <para>The value is escaped before writing.</para>
+        /// <para>
+        /// If <paramref name="value"/> is <see langword="null"/> the JSON null value is written,
+        /// as if <see cref="WriteNullValue"/> was called.
+        /// </para>
         /// </remarks>
         /// <exception cref="ArgumentException">
         /// Thrown when the specified value is too large.
@@ -46,7 +50,16 @@ namespace System.Text.Json
         /// Thrown if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteStringValue(string value)
-           => WriteStringValue(value.AsSpan());
+        {
+            if (value == null)
+            {
+                WriteNullValue();
+            }
+            else
+            {
+                WriteStringValue(value.AsSpan());
+            }
+        }
 
         /// <summary>
         /// Writes the text value (as a JSON string) as an element of a JSON array.
index 0954db1..29728fd 100644 (file)
@@ -718,12 +718,15 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 
         /// OR if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteStartArray(string propertyName)
-            => WriteStartArray(propertyName.AsSpan());
+            => WriteStartArray((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan());
 
         /// <summary>
         /// Writes the beginning of a JSON object with a property name as the key.
@@ -735,12 +738,15 @@ namespace System.Text.Json
         /// <exception cref="ArgumentException">
         /// Thrown when the specified property name is too large.
         /// </exception>
+        /// <exception cref="ArgumentNullException">
+        /// The <paramref name="propertyName"/> parameter is <see langword="null"/>.
+        /// </exception>
         /// <exception cref="InvalidOperationException">
         /// Thrown when the depth of the JSON has exceeded the maximum depth of 1000 
         /// OR if this would result in an invalid JSON to be written (while validation is enabled).
         /// </exception>
         public void WriteStartObject(string propertyName)
-            => WriteStartObject(propertyName.AsSpan());
+            => WriteStartObject((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan());
 
         /// <summary>
         /// Writes the beginning of a JSON array with a property name as the key.
index 2267749..222cd3b 100644 (file)
@@ -29,6 +29,11 @@ namespace System.Text.Json.Serialization.Tests
             }
 
             {
+                string json = JsonSerializer.Serialize((string)null);
+                Assert.Equal("null", json);
+            }
+
+            {
                 Span<byte> json = JsonSerializer.SerializeToUtf8Bytes(1);
                 Assert.Equal(Encoding.UTF8.GetBytes("1"), json.ToArray());
             }
index 555162e..a6fb1ce 100644 (file)
@@ -722,7 +722,7 @@ namespace System.Text.Json.Tests
         [Fact]
         public static void InvalidConversion()
         {
-            string jsonString = "[\"stringValue\", true, /* Comment within */ 1234] // Comment outside";
+            string jsonString = "[\"stringValue\", true, /* Comment within */ 1234, null] // Comment outside";
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
 
             var state = new JsonReaderState(options: new JsonReaderOptions { CommentHandling = JsonCommentHandling.Allow });
@@ -731,13 +731,14 @@ namespace System.Text.Json.Tests
             {
                 if (json.TokenType != JsonTokenType.String)
                 {
-                    try
+                    if (json.TokenType == JsonTokenType.Null)
                     {
-                        string value = json.GetString();
-                        Assert.True(false, "Expected GetString to throw InvalidOperationException due to mismatch token type.");
+                        Assert.Null(json.GetString());
+                    }
+                    else
+                    {
+                        JsonTestHelper.AssertThrows<InvalidOperationException>(json, (jsonReader) => jsonReader.GetString());
                     }
-                    catch (InvalidOperationException)
-                    { }
 
                     try
                     {
index 532b0ea..a0cc38b 100644 (file)
@@ -3128,6 +3128,7 @@ namespace System.Text.Json.Tests
                 {
                     case JsonTokenType.Null:
                         Assert.Equal(expectedString, Encoding.UTF8.GetString(json.ValueSpan.ToArray()));
+                        Assert.Null(json.GetString());
                         foundPrimitiveValue = true;
                         break;
                     case JsonTokenType.Number:
index 8b85008..c99e238 100644 (file)
@@ -4821,6 +4821,480 @@ namespace System.Text.Json.Tests
             Assert.Equal(1, jsonUtf8.BytesCommitted);
         }
 
+        [Fact]
+        public static void WriteBase64String_NullPropertyName()
+        {
+            WriteNullPropertyName_Simple(
+                new byte[] { 0x01, 0x00, 0x01 },
+                "\"AQAB\"",
+                (writer, name, value) => writer.WriteBase64String(name, value),
+                (writer, name, value) => writer.WriteBase64String(name, value),
+                (writer, name, value) => writer.WriteBase64String(name, value));
+        }
+
+        [Fact]
+        public static void WriteBoolean_NullPropertyName()
+        {
+            WriteNullPropertyName_Simple(
+                false,
+                "false",
+                (writer, name, value) => writer.WriteBoolean(name, value),
+                (writer, name, value) => writer.WriteBoolean(name, value),
+                (writer, name, value) => writer.WriteBoolean(name, value));
+        }
+
+        [Fact]
+        public static void WriteNull_NullPropertyName()
+        {
+            WriteNullPropertyName_NoValue(
+                "null",
+                cleanupAction: null,
+                (writer, name) => writer.WriteNull(name),
+                (writer, name) => writer.WriteNull(name),
+                (writer, name) => writer.WriteNull(name));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_Decimal()
+        {
+            decimal numericValue = 1.04m;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "1.04",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_Double()
+        {
+            double numericValue = 1.05d;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "1.05",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_Int32()
+        {
+            int numericValue = 1048576;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "1048576",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_Int64()
+        {
+            long numericValue = 0x0100_0000_0000;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "1099511627776",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_Single()
+        {
+            float numericValue = 1e3f;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "1000",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_UInt32()
+        {
+            uint numericValue = 0x8000_0000;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "2147483648",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WriteNumber_NullPropertyName_UInt64()
+        {
+            ulong numericValue = ulong.MaxValue;
+
+            WriteNullPropertyName_Simple(
+                numericValue,
+                "18446744073709551615",
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value),
+                (writer, name, value) => writer.WriteNumber(name, value));
+        }
+
+        [Fact]
+        public static void WritePropertyName_NullPropertyName()
+        {
+            WriteNullPropertyName_NoValue(
+                "null",
+                writer => writer.WriteNullValue(),
+                (writer, name) => writer.WritePropertyName(name),
+                (writer, name) => writer.WritePropertyName(name),
+                (writer, name) => writer.WritePropertyName(name));
+        }
+
+        [Fact]
+        public static void WriteStartArray_NullPropertyName()
+        {
+            WriteNullPropertyName_NoValue(
+                "[]",
+                writer => writer.WriteEndArray(),
+                (writer, name) => writer.WriteStartArray(name),
+                (writer, name) => writer.WriteStartArray(name),
+                (writer, name) => writer.WriteStartArray(name));
+        }
+
+        [Fact]
+        public static void WriteStartObject_NullPropertyName()
+        {
+            WriteNullPropertyName_NoValue(
+                "{}",
+                writer => writer.WriteEndObject(),
+                (writer, name) => writer.WriteStartObject(name),
+                (writer, name) => writer.WriteStartObject(name),
+                (writer, name) => writer.WriteStartObject(name));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_DateTime()
+        {
+            WriteNullPropertyName_Simple(
+                DateTime.MinValue,
+                "\"0001-01-01T00:00:00\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_DateTimeOffset()
+        {
+            WriteNullPropertyName_Simple(
+                DateTimeOffset.MinValue,
+                "\"0001-01-01T00:00:00+00:00\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_Guid()
+        {
+            WriteNullPropertyName_Simple(
+                Guid.Empty,
+                "\"00000000-0000-0000-0000-000000000000\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_ReadOnlySpan_Byte()
+        {
+            WriteNullPropertyName_Simple(
+                Encoding.UTF8.GetBytes("utf8"),
+                "\"utf8\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_ReadOnlySpan_Char()
+        {
+            WriteNullPropertyName_Simple(
+                "utf16",
+                "\"utf16\"",
+                (writer, name, value) => writer.WriteString(name, value.AsSpan()),
+                (writer, name, value) => writer.WriteString(name, value.AsSpan()),
+                (writer, name, value) => writer.WriteString(name, value.AsSpan()));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_String()
+        {
+            WriteNullPropertyName_Simple(
+                "string",
+                "\"string\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteString_NullPropertyName_JsonEncodedText()
+        {
+            WriteNullPropertyName_Simple(
+                JsonEncodedText.Encode("jet"),
+                "\"jet\"",
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value),
+                (writer, name, value) => writer.WriteString(name, value));
+        }
+
+        [Fact]
+        public static void WriteCommentValue_NullString()
+        {
+            // WriteCommentValue is sufficiently different (no comma after a legal value)
+            // that it doesn't warrant a helper for expansion.
+            var output = new ArrayBufferWriter<byte>(1024);
+            string nullString = null;
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartArray();
+
+                AssertExtensions.Throws<ArgumentNullException>(
+                    "value",
+                    () => writer.WriteCommentValue(nullString));
+
+                ReadOnlySpan<char> nullStringSpan = nullString.AsSpan();
+                writer.WriteCommentValue(nullStringSpan);
+
+                writer.WriteCommentValue(ReadOnlySpan<byte>.Empty);
+
+                writer.WriteEndArray();
+                writer.Flush();
+            }
+
+            AssertContents("[/**//**/]", output);
+        }
+
+        [Fact]
+        public static void WriteStringValue_NullString()
+        {
+            WriteNullValue_InArray(
+                "\"\"",
+                "null",
+                (writer, value) => writer.WriteStringValue(value),
+                (writer, value) => writer.WriteStringValue(value),
+                (writer, value) => writer.WriteStringValue(value));
+        }
+
+        [Fact]
+        public static void WriteStringValue_StringProperty_NullString()
+        {
+            WriteNullValue_InObject(
+                "\"propStr\":\"\"",
+                "\"propStr\":null",
+                (writer, value) => writer.WriteString("propStr", value),
+                (writer, value) => writer.WriteString("propStr", value),
+                (writer, value) => writer.WriteString("propStr", value));
+        }
+
+        [Fact]
+        public static void WriteStringValue_ReadOnlySpanCharProperty_NullString()
+        {
+            WriteNullValue_InObject(
+                "\"propUtf16\":\"\"",
+                "\"propUtf16\":null",
+                (writer, value) => writer.WriteString("propUtf16".AsSpan(), value),
+                (writer, value) => writer.WriteString("propUtf16".AsSpan(), value),
+                (writer, value) => writer.WriteString("propUtf16".AsSpan(), value));
+        }
+
+        [Fact]
+        public static void WriteStringValue_ReadOnlySpanBytesProperty_NullString()
+        {
+            byte[] propertyName = Encoding.UTF8.GetBytes("propUtf8");
+
+            WriteNullValue_InObject(
+                "\"propUtf8\":\"\"",
+                "\"propUtf8\":null",
+                (writer, value) => writer.WriteString(propertyName, value),
+                (writer, value) => writer.WriteString(propertyName, value),
+                (writer, value) => writer.WriteString(propertyName, value));
+        }
+
+        [Fact]
+        public static void WriteStringValue_JsonEncodedTextProperty_NullString()
+        {
+            JsonEncodedText jet = JsonEncodedText.Encode("propJet");
+
+            WriteNullValue_InObject(
+                "\"propJet\":\"\"",
+                "\"propJet\":null",
+                (writer, value) => writer.WriteString(jet, value),
+                (writer, value) => writer.WriteString(jet, value),
+                (writer, value) => writer.WriteString(jet, value));
+        }
+
+        private delegate void WriteValueSpanAction<T>(
+            Utf8JsonWriter writer,
+            ReadOnlySpan<T> value);
+
+        private delegate void WritePropertySpanAction<T>(
+            Utf8JsonWriter writer,
+            ReadOnlySpan<T> propertyName);
+
+        private delegate void WritePropertySpanAction<T1, T2>(
+            Utf8JsonWriter writer,
+            ReadOnlySpan<T1> propertyName,
+            T2 value);
+
+        private static void WriteNullPropertyName_Simple<T>(
+            T value,
+            string wireValue,
+            Action<Utf8JsonWriter, string, T> stringAction,
+            WritePropertySpanAction<char, T> charSpanAction,
+            WritePropertySpanAction<byte, T> byteSpanAction)
+        {
+            var output = new ArrayBufferWriter<byte>(1024);
+            string nullString = null;
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartObject();
+
+                AssertExtensions.Throws<ArgumentNullException>(
+                    "propertyName",
+                    () => stringAction(writer, nullString, value));
+
+                writer.WriteEndObject();
+                writer.Flush();
+            }
+
+            AssertContents("{}", output);
+            output.Clear();
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartObject();
+
+                ReadOnlySpan<char> nullStringSpan = nullString.AsSpan();
+                charSpanAction(writer, nullStringSpan, value);
+            
+                byteSpanAction(writer, ReadOnlySpan<byte>.Empty, value);
+                
+                writer.WriteEndObject();
+                writer.Flush();
+            }
+
+            AssertContents($"{{\"\":{wireValue},\"\":{wireValue}}}", output);
+        }
+
+        private static void WriteNullPropertyName_NoValue(
+            string wireValue,
+            Action<Utf8JsonWriter> cleanupAction,
+            Action<Utf8JsonWriter, string> stringAction,
+            WritePropertySpanAction<char> charSpanAction,
+            WritePropertySpanAction<byte> byteSpanAction)
+        {
+            var output = new ArrayBufferWriter<byte>(1024);
+            string nullString = null;
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartObject();
+
+                AssertExtensions.Throws<ArgumentNullException>(
+                    "propertyName",
+                    () => stringAction(writer, nullString));
+
+                writer.WriteEndObject();
+                writer.Flush();
+            }
+
+            AssertContents("{}", output);
+            output.Clear();
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartObject();
+
+                ReadOnlySpan<char> nullStringSpan = nullString.AsSpan();
+                charSpanAction(writer, nullStringSpan);
+                cleanupAction?.Invoke(writer);
+
+                byteSpanAction(writer, ReadOnlySpan<byte>.Empty);
+                cleanupAction?.Invoke(writer);
+
+                writer.WriteEndObject();
+                writer.Flush();
+            }
+
+            AssertContents($"{{\"\":{wireValue},\"\":{wireValue}}}", output);
+        }
+
+        private static void WriteNullValue_InObject(
+            string wireValue,
+            string nullValue,
+            Action<Utf8JsonWriter, string> stringAction,
+            WriteValueSpanAction<char> charSpanAction,
+            WriteValueSpanAction<byte> byteSpanAction)
+        {
+            var output = new ArrayBufferWriter<byte>(1024);
+            string nullString = null;
+
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartObject();
+
+                stringAction(writer, nullString);
+
+                ReadOnlySpan<char> nullStringSpan = nullString.AsSpan();
+                charSpanAction(writer, nullStringSpan);
+
+                byteSpanAction(writer, ReadOnlySpan<byte>.Empty);
+
+                writer.WriteEndObject();
+                writer.Flush();
+            }
+
+            AssertContents($"{{{nullValue},{wireValue},{wireValue}}}", output);
+        }
+
+        private static void WriteNullValue_InArray(
+            string wireValue,
+            string nullValue,
+            Action<Utf8JsonWriter, string> stringAction,
+            WriteValueSpanAction<char> charSpanAction,
+            WriteValueSpanAction<byte> byteSpanAction)
+        {
+            var output = new ArrayBufferWriter<byte>(1024);
+            string nullString = null;
+            
+            using (var writer = new Utf8JsonWriter(output))
+            {
+                writer.WriteStartArray();
+
+                stringAction(writer, nullString);
+
+                ReadOnlySpan<char> nullStringSpan = nullString.AsSpan();
+                charSpanAction(writer, nullStringSpan);
+
+                byteSpanAction(writer, ReadOnlySpan<byte>.Empty);
+
+                writer.WriteEndArray();
+                writer.Flush();
+            }
+
+            AssertContents($"[{nullValue},{wireValue},{wireValue}]", output);
+        }
+
         private static string GetHelloWorldExpectedString(bool prettyPrint, string propertyName, string value)
         {
             var ms = new MemoryStream();