Skip to content

Commit

Permalink
Add new line to be specified for JSON formatting
Browse files Browse the repository at this point in the history
Allow the new line string to use for indented JSON to be specified through options.
Resolves dotnet#84117.
  • Loading branch information
martincostello committed Apr 10, 2024
1 parent b229723 commit 839c642
Show file tree
Hide file tree
Showing 42 changed files with 260 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,10 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults)
/// instead of numeric serialization for all enum types encountered in its type graph.
/// </summary>
public bool UseStringEnumConverter { get; set; }

/// <summary>
/// Specifies the default value of <see cref="JsonSerializerOptions.NewLine"/> when set.
/// </summary>
public string? NewLine { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti
if (optionsSpec.MaxDepth is int maxDepth)
writer.WriteLine($"MaxDepth = {maxDepth},");

if (optionsSpec.NewLine is string newLine)
writer.WriteLine($"NewLine = {FormatStringLiteral(newLine)},");

if (optionsSpec.NumberHandling is JsonNumberHandling numberHandling)
writer.WriteLine($"NumberHandling = {FormatNumberHandling(numberHandling)},");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
bool? ignoreReadOnlyProperties = null;
bool? includeFields = null;
int? maxDepth = null;
string? newLine = null;
JsonNumberHandling? numberHandling = null;
JsonObjectCreationHandling? preferredObjectCreationHandling = null;
bool? propertyNameCaseInsensitive = null;
Expand Down Expand Up @@ -344,6 +345,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
maxDepth = (int)namedArg.Value.Value!;
break;

case nameof(JsonSourceGenerationOptionsAttribute.NewLine):
newLine = (string)namedArg.Value.Value!;
break;

case nameof(JsonSourceGenerationOptionsAttribute.NumberHandling):
numberHandling = (JsonNumberHandling)namedArg.Value.Value!;
break;
Expand Down Expand Up @@ -411,6 +416,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
IgnoreReadOnlyProperties = ignoreReadOnlyProperties,
IncludeFields = includeFields,
MaxDepth = maxDepth,
NewLine = newLine,
NumberHandling = numberHandling,
PreferredObjectCreationHandling = preferredObjectCreationHandling,
PropertyNameCaseInsensitive = propertyNameCaseInsensitive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public sealed record SourceGenerationOptionsSpec

public required int? MaxDepth { get; init; }

public required string? NewLine { get; init; }

public required JsonNumberHandling? NumberHandling { get; init; }

public required JsonObjectCreationHandling? PreferredObjectCreationHandling { get; init; }
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public bool IncludeFields { get { throw null; } set { } }
public bool IsReadOnly { get { throw null; } }
public int MaxDepth { get { throw null; } set { } }
public string NewLine { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonObjectCreationHandling PreferredObjectCreationHandling { get { throw null; } set { } }
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
Expand Down Expand Up @@ -445,6 +446,7 @@ public partial struct JsonWriterOptions
public bool Indented { get { throw null; } set { } }
public char IndentCharacter { get { throw null; } set { } }
public int IndentSize { get { throw null; } set { } }
public string NewLine { get { throw null; } set { } }
public int MaxDepth { readonly get { throw null; } set { } }
public bool SkipValidation { get { throw null; } set { } }
}
Expand Down Expand Up @@ -1072,6 +1074,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau
public bool IgnoreReadOnlyProperties { get { throw null; } set { } }
public bool IncludeFields { get { throw null; } set { } }
public int MaxDepth { get { throw null; } set { } }
public string? NewLine { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonObjectCreationHandling PreferredObjectCreationHandling { get { throw null; } set { } }
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -717,4 +717,7 @@
<data name="InvalidIndentSize" xml:space="preserve">
<value>Indentation size must be between {0} and {1}.</value>
</data>
<data name="InvalidNewLine" xml:space="preserve">
<value>Only the strings '\n' or '\r\n' are permitted.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ internal static partial class JsonConstants
public const byte UtcOffsetToken = (byte)'Z';
public const byte TimePrefix = (byte)'T';

public const string NewLineLineFeed = "\n";
public const string NewLineCarriageReturnLineFeed = "\r\n";

// \u2028 and \u2029 are considered respectively line and paragraph separators
// UTF-8 representation for them is E2, 80, A8/A9
public const byte StartingByteOfNonStandardSeparator = 0xE2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public static JsonSerializerOptions Web
private bool _ignoreReadOnlyProperties;
private bool _ignoreReadonlyFields;
private bool _includeFields;
private string _newLine = Environment.NewLine;
private bool _propertyNameCaseInsensitive;
private bool _writeIndented;
private char _indentCharacter = JsonConstants.DefaultIndentCharacter;
Expand Down Expand Up @@ -141,6 +142,7 @@ public JsonSerializerOptions(JsonSerializerOptions options)
_ignoreReadOnlyProperties = options._ignoreReadOnlyProperties;
_ignoreReadonlyFields = options._ignoreReadonlyFields;
_includeFields = options._includeFields;
_newLine = options._newLine;
_propertyNameCaseInsensitive = options._propertyNameCaseInsensitive;
_writeIndented = options._writeIndented;
_indentCharacter = options._indentCharacter;
Expand Down Expand Up @@ -750,6 +752,30 @@ public ReferenceHandler? ReferenceHandler
}
}

/// <summary>
/// Gets or sets the new line string to use when <see cref="WriteIndented"/> is <see langword="true"/>.
/// The default is the value of <see cref="Environment.NewLine"/>.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the new line string is not <c>\n</c> or <c>\r\n</c>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown if this property is set after serialization or deserialization has occurred.
/// </exception>
public string NewLine
{
get
{
return _newLine;
}
set
{
JsonWriterHelper.ValidateNewLine(value);
VerifyMutable();
_newLine = value;
}
}

/// <summary>
/// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers.
/// </summary>
Expand Down Expand Up @@ -970,6 +996,7 @@ internal JsonWriterOptions GetWriterOptions()
IndentCharacter = IndentCharacter,
IndentSize = IndentSize,
MaxDepth = EffectiveMaxDepth,
NewLine = NewLine,
#if !DEBUG
SkipValidation = true
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ internal static partial class ThrowHelper
// If the exception source is this value, the serializer will re-throw as JsonException.
public const string ExceptionSourceValueToRethrowAsJsonException = "System.Text.Json.Rethrowable";

[DoesNotReturn]
public static void ThrowArgumentOutOfRangeException_NewLine(string parameterName)
{
throw GetArgumentOutOfRangeException(parameterName, SR.InvalidNewLine);
}

[DoesNotReturn]
public static void ThrowArgumentOutOfRangeException_IndentCharacter(string parameterName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ public static void WriteIndentation(Span<byte> buffer, int indent, byte indentBy
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidateNewLine(string value)
{
if (value is not JsonConstants.NewLineLineFeed and not JsonConstants.NewLineCarriageReturnLineFeed)
ThrowHelper.ThrowArgumentOutOfRangeException_NewLine(nameof(value));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidateIndentCharacter(char value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ namespace System.Text.Json
/// </summary>
public struct JsonWriterOptions
{
private static readonly string s_alternateNewLine = Environment.NewLine.Length == 2 ? JsonConstants.NewLineLineFeed : JsonConstants.NewLineCarriageReturnLineFeed;

internal const int DefaultMaxDepth = 1000;

private int _maxDepth;
Expand Down Expand Up @@ -68,11 +70,11 @@ public char IndentCharacter
/// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> is out of the allowed range.</exception>
public int IndentSize
{
readonly get => EncodeIndentSize((_optionsMask & IndentSizeMask) >> 3);
readonly get => EncodeIndentSize((_optionsMask & IndentSizeMask) >> OptionsBitCount);
set
{
JsonWriterHelper.ValidateIndentSize(value);
_optionsMask = (_optionsMask & ~IndentSizeMask) | (EncodeIndentSize(value) << 3);
_optionsMask = (_optionsMask & ~IndentSizeMask) | (EncodeIndentSize(value) << OptionsBitCount);
}
}

Expand Down Expand Up @@ -135,11 +137,33 @@ public bool SkipValidation
}
}

/// <summary>
/// Gets or sets the new line string to use when <see cref="Indented"/> is <see langword="true"/>.
/// The default is the value of <see cref="Environment.NewLine"/>.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the new line string is not <c>\n</c> or <c>\r\n</c>.
/// </exception>
public string NewLine
{
get => (_optionsMask & NewLineBit) != 0 ? s_alternateNewLine : Environment.NewLine;
set
{
JsonWriterHelper.ValidateNewLine(value);
if (value != Environment.NewLine)
_optionsMask |= NewLineBit;
else
_optionsMask &= ~NewLineBit;
}
}

internal bool IndentedOrNotSkipValidation => (_optionsMask & (IndentBit | SkipValidationBit)) != SkipValidationBit; // Equivalent to: Indented || !SkipValidation;

private const int OptionsBitCount = 4;
private const int IndentBit = 1;
private const int SkipValidationBit = 2;
private const int IndentCharacterBit = 4;
private const int IndentSizeMask = JsonConstants.MaximumIndentSize << 3;
private const int NewLineBit = 4;
private const int IndentCharacterBit = 8;
private const int IndentSizeMask = JsonConstants.MaximumIndentSize << OptionsBitCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,11 @@ private void WriteBase64Indented(ReadOnlySpan<char> escapedPropertyName, ReadOnl

int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);

Debug.Assert(escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding < int.MaxValue - indent - encodedLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding < int.MaxValue - indent - encodedLength - 7 - _newLineLength);

// All ASCII, 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, 1 colon, and 1 space => indent + escapedPropertyName.Length + encodedLength + 6
// Optionally, 1 list separator, 1-2 bytes for new line, and up to 3x growth when transcoding.
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + encodedLength + 7 + s_newLineLength;
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + encodedLength + 7 + _newLineLength;

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down Expand Up @@ -334,11 +334,11 @@ private void WriteBase64Indented(ReadOnlySpan<byte> escapedPropertyName, ReadOnl

int encodedLength = Base64.GetMaxEncodedToUtf8Length(bytes.Length);

Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - encodedLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - encodedLength - 7 - _newLineLength);

// 2 quotes for property name, 2 quotes to surround the base-64 encoded string value, 1 colon, and 1 space => indent + escapedPropertyName.Length + encodedLength + 6
// Optionally, 1 list separator, and 1-2 bytes for new line.
int maxRequired = indent + escapedPropertyName.Length + encodedLength + 7 + s_newLineLength;
int maxRequired = indent + escapedPropertyName.Length + encodedLength + 7 + _newLineLength;

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,11 @@ private void WriteStringIndented(ReadOnlySpan<char> escapedPropertyName, DateTim
int indent = Indentation;
Debug.Assert(indent <= _indentLength * _options.MaxDepth);

Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - _newLineLength);

// All ASCII, 2 quotes for property name, 2 quotes for date, 1 colon, and 1 space => escapedPropertyName.Length + JsonConstants.MaximumFormatDateTimeOffsetLength + 6
// Optionally, 1 list separator, 1-2 bytes for new line, and up to 3x growth when transcoding
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumFormatDateTimeOffsetLength + 7 + s_newLineLength;
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumFormatDateTimeOffsetLength + 7 + _newLineLength;

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down Expand Up @@ -335,10 +335,10 @@ private void WriteStringIndented(ReadOnlySpan<byte> escapedPropertyName, DateTim
int indent = Indentation;
Debug.Assert(indent <= _indentLength * _options.MaxDepth);

Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - _newLineLength);

int minRequired = indent + escapedPropertyName.Length + JsonConstants.MaximumFormatDateTimeOffsetLength + 6; // 2 quotes for property name, 2 quotes for date, 1 colon, and 1 space
int maxRequired = minRequired + 1 + s_newLineLength; // Optionally, 1 list separator and 1-2 bytes for new line
int maxRequired = minRequired + 1 + _newLineLength; // Optionally, 1 list separator and 1-2 bytes for new line

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,11 @@ private void WriteStringIndented(ReadOnlySpan<char> escapedPropertyName, DateTim
int indent = Indentation;
Debug.Assert(indent <= _indentLength * _options.MaxDepth);

Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - _newLineLength);

// All ASCII, 2 quotes for property name, 2 quotes for date, 1 colon, and 1 space => escapedPropertyName.Length + JsonConstants.MaximumFormatDateTimeOffsetLength + 6
// Optionally, 1 list separator, 1-2 bytes for new line, and up to 3x growth when transcoding
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumFormatDateTimeOffsetLength + 7 + s_newLineLength;
int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumFormatDateTimeOffsetLength + 7 + _newLineLength;

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down Expand Up @@ -334,10 +334,10 @@ private void WriteStringIndented(ReadOnlySpan<byte> escapedPropertyName, DateTim
int indent = Indentation;
Debug.Assert(indent <= _indentLength * _options.MaxDepth);

Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - s_newLineLength);
Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - JsonConstants.MaximumFormatDateTimeOffsetLength - 7 - _newLineLength);

int minRequired = indent + escapedPropertyName.Length + JsonConstants.MaximumFormatDateTimeOffsetLength + 6; // 2 quotes for property name, 2 quotes for date, 1 colon, and 1 space
int maxRequired = minRequired + 1 + s_newLineLength; // Optionally, 1 list separator and 1-2 bytes for new line
int maxRequired = minRequired + 1 + _newLineLength; // Optionally, 1 list separator and 1-2 bytes for new line

if (_memory.Length - BytesPending < maxRequired)
{
Expand Down
Loading

0 comments on commit 839c642

Please sign in to comment.