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

Allow specifying IndentCharacter and IndentSize when writing JSON #95292

Merged
merged 29 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b0e8003
Add IndentText json option
manandre Nov 25, 2023
d283b53
Add IndentText for json source generator
manandre Nov 25, 2023
eb0d433
Add tests
manandre Nov 25, 2023
6ea532e
IndentText must be non-nullable
manandre Nov 26, 2023
c8995a1
Improve performance
manandre Nov 26, 2023
3f82e03
Add extra tests
manandre Nov 26, 2023
c88aaf3
Cleanup
manandre Nov 27, 2023
86c4de5
Apply suggestions from code review
manandre Nov 28, 2023
976651f
Fixes following code review
manandre Nov 28, 2023
71e77db
Fixes following code review #2
manandre Dec 1, 2023
7c44dda
Add tests for invalid characters
manandre Dec 1, 2023
e42ea75
Handle RawIndent length
manandre Dec 1, 2023
face6cc
Move all to RawIndentation
manandre Dec 1, 2023
29118b4
Update documentation
manandre Dec 1, 2023
6f6eca7
Additional fixes from code review
manandre Dec 4, 2023
037f2fa
Move to the new API
manandre Dec 15, 2023
7c76a35
Extra fixes and enhancements
manandre Dec 15, 2023
cbd6dba
Fixes from code review
manandre Dec 24, 2023
37a305a
Avoid introducing extra fields in JsonWriterOptions
manandre Dec 24, 2023
5b11220
Fix OOM error
manandre Dec 24, 2023
ed7e1b2
Use bitwise logic for IndentedOrNotSkipValidation
manandre Dec 26, 2023
01fc002
Cache indentation options in Utf8JsonWriter
manandre Dec 26, 2023
f00943e
Add missing test around indentation options
manandre Dec 26, 2023
5dc575b
New fixes from code review
manandre Dec 28, 2023
d5e8835
Update src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf…
eiriktsarpalis Dec 29, 2023
2bfbf0c
Add test to check default values of the JsonWriterOptions properties
manandre Dec 29, 2023
00617c2
Fix comment
manandre Jan 2, 2024
4964a53
Merge branch 'main' into json-indent-text
manandre Jan 6, 2024
c14eaf7
Merge branch 'main' into json-indent-text
eiriktsarpalis Jan 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/Common/JsonConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ internal static partial class JsonConstants

public const int StackallocByteThreshold = 256;
public const int StackallocCharThreshold = StackallocByteThreshold / 2;

public const string DefaultIndent = " ";
manandre marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults)
/// </summary>
public bool WriteIndented { get; set; }

/// <summary>
/// Specifies the default value of <see cref="JsonSerializerOptions.IndentText"/> when set.
/// </summary>
public string IndentText { get; set; } = JsonConstants.DefaultIndent;

/// <summary>
/// Specifies the default source generation mode for type declarations that don't set a <see cref="JsonSerializableAttribute.GenerationMode"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti
if (optionsSpec.WriteIndented is bool writeIndented)
writer.WriteLine($"WriteIndented = {FormatBool(writeIndented)},");

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

writer.Indentation--;
writer.WriteLine("};");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
JsonUnmappedMemberHandling? unmappedMemberHandling = null;
bool? useStringEnumConverter = null;
bool? writeIndented = null;
string? indentText = null;

if (attributeData.ConstructorArguments.Length > 0)
{
Expand Down Expand Up @@ -373,6 +374,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
writeIndented = (bool)namedArg.Value.Value!;
break;

case nameof(JsonSourceGenerationOptionsAttribute.IndentText):
indentText = (string)namedArg.Value.Value!;
break;

case nameof(JsonSourceGenerationOptionsAttribute.GenerationMode):
generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!;
break;
Expand Down Expand Up @@ -404,6 +409,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
UnmappedMemberHandling = unmappedMemberHandling,
UseStringEnumConverter = useStringEnumConverter,
WriteIndented = writeIndented,
IndentText = indentText,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public sealed record SourceGenerationOptionsSpec

public required bool? WriteIndented { get; init; }

public required string? IndentText { get; init; }

public JsonKnownNamingPolicy? GetEffectivePropertyNamingPolicy()
=> PropertyNamingPolicy ?? (Defaults is JsonSerializerDefaults.Web ? JsonKnownNamingPolicy.CamelCase : null);
}
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 @@ -394,6 +394,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } }
public bool WriteIndented { get { throw null; } set { } }
public string IndentText { get { throw null; } set { } }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.ObsoleteAttribute("JsonSerializerOptions.AddContext is obsolete. To register a JsonSerializerContext, use either the TypeInfoResolver or TypeInfoResolverChain properties.", DiagnosticId="SYSLIB0049", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public void AddContext<TContext>() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { }
Expand Down Expand Up @@ -439,6 +440,7 @@ public partial struct JsonWriterOptions
private int _dummyPrimitive;
public System.Text.Encodings.Web.JavaScriptEncoder? Encoder { readonly get { throw null; } set { } }
public bool Indented { get { throw null; } set { } }
public string IndentText { get { throw null; } set { } }
public int MaxDepth { readonly get { throw null; } set { } }
public bool SkipValidation { get { throw null; } set { } }
}
Expand Down Expand Up @@ -1074,6 +1076,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } }
public bool UseStringEnumConverter { get { throw null; } set { } }
public bool WriteIndented { get { throw null; } set { } }
public string IndentText { get { throw null; } set { } }
}
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
public partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ internal static partial class JsonConstants
// Explicitly skipping ReverseSolidus since that is handled separately
public static ReadOnlySpan<byte> EscapableChars => "\"nrt/ubf"u8;

public const int SpacesPerIndent = 2;
public const int RemoveFlagsBitMask = 0x7FFFFFFF;

// In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
left._includeFields == right._includeFields &&
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
left._writeIndented == right._writeIndented &&
left._indentText == right._indentText &&
left._typeInfoResolver == right._typeInfoResolver &&
CompareLists(left._converters, right._converters);

Expand Down Expand Up @@ -565,6 +566,7 @@ public int GetHashCode(JsonSerializerOptions options)
AddHashCode(ref hc, options._includeFields);
AddHashCode(ref hc, options._propertyNameCaseInsensitive);
AddHashCode(ref hc, options._writeIndented);
AddHashCode(ref hc, options._indentText.GetHashCode());
manandre marked this conversation as resolved.
Show resolved Hide resolved
AddHashCode(ref hc, options._typeInfoResolver);
AddListHashCode(ref hc, options._converters);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public static JsonSerializerOptions Default
private bool _includeFields;
private bool _propertyNameCaseInsensitive;
private bool _writeIndented;
private string _indentText = JsonConstants.DefaultIndent;

/// <summary>
/// Constructs a new <see cref="JsonSerializerOptions"/> instance.
Expand Down Expand Up @@ -124,6 +125,7 @@ public JsonSerializerOptions(JsonSerializerOptions options)
_includeFields = options._includeFields;
_propertyNameCaseInsensitive = options._propertyNameCaseInsensitive;
_writeIndented = options._writeIndented;
_indentText = options._indentText;
_typeInfoResolver = options._typeInfoResolver;
EffectiveMaxDepth = options.EffectiveMaxDepth;
ReferenceHandlingStrategy = options.ReferenceHandlingStrategy;
Expand Down Expand Up @@ -645,6 +647,30 @@ public bool WriteIndented
}
}

/// <summary>
/// Defines the text used as indent when <see cref="WriteIndented" /> is enabled
/// By default, the JSON is indented with 2 white spaces.
manandre marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this property is set after serialization or deserialization has occurred.
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// </exception>
public string IndentText
{
get
{
return _indentText;
}
set
{
if (value is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(value));
}
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
VerifyMutable();
_indentText = value;
}
}

/// <summary>
/// Configures how object references are handled when reading and writing JSON.
/// </summary>
Expand Down Expand Up @@ -876,6 +902,7 @@ internal JsonWriterOptions GetWriterOptions()
{
Encoder = Encoder,
Indented = WriteIndented,
IndentText = IndentText,
MaxDepth = EffectiveMaxDepth,
#if !DEBUG
SkipValidation = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,36 @@ namespace System.Text.Json
{
internal static partial class JsonWriterHelper
{
public static void WriteIndentation(Span<byte> buffer, int indent)
public static void WriteIndentation(Span<byte> buffer, int indent, byte indentByte)
{
Debug.Assert(indent % JsonConstants.SpacesPerIndent == 0);
Debug.Assert(buffer.Length >= indent);

// Based on perf tests, the break-even point where vectorized Fill is faster
// than explicitly writing the space in a loop is 8.
if (indent < 8)
if (indent < 8 && indent % 2 == 0)
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
int i = 0;
while (i < indent)
{
buffer[i++] = JsonConstants.Space;
buffer[i++] = JsonConstants.Space;
buffer[i++] = indentByte;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
buffer[i++] = indentByte;
}
}
else
{
buffer.Slice(0, indent).Fill(JsonConstants.Space);
buffer.Slice(0, indent).Fill(indentByte);
}
}

public static void WriteIndentation(Span<byte> buffer, int indentation, Span<byte> indent)
{
Debug.Assert(buffer.Length >= indentation);

int offset = 0;
while (offset < indentation)
{
indent.CopyTo(buffer.Slice(offset));
offset += indent.Length;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public struct JsonWriterOptions

private int _maxDepth;
private int _optionsMask;
private string _indentText;

/// <summary>
/// The encoder to use when escaping strings, or <see langword="null" /> to use the default encoder.
Expand All @@ -43,6 +44,23 @@ public bool Indented
}
}

/// <summary>
/// Defines the text used as indent by the <see cref="Utf8JsonWriter"/> when <see cref="Indented"/> is enabled
/// By default, the JSON is written with 2 white spaces.
manandre marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public string IndentText
{
get => _indentText ?? JsonConstants.DefaultIndent;
set
{
if (value is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(value));
}
_indentText = value;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// <summary>
/// Gets or sets the maximum depth allowed when writing JSON, with the default (i.e. 0) indicating a max depth of 1000.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ private void WriteBase64Indented(ReadOnlySpan<char> escapedPropertyName, ReadOnl
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -359,7 +359,7 @@ private void WriteBase64Indented(ReadOnlySpan<byte> escapedPropertyName, ReadOnl
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ private void WriteStringIndented(ReadOnlySpan<char> escapedPropertyName, DateTim
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -359,7 +359,7 @@ private void WriteStringIndented(ReadOnlySpan<byte> escapedPropertyName, DateTim
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ private void WriteStringIndented(ReadOnlySpan<char> escapedPropertyName, DateTim
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -358,7 +358,7 @@ private void WriteStringIndented(ReadOnlySpan<byte> escapedPropertyName, DateTim
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ private void WriteNumberIndented(ReadOnlySpan<char> escapedPropertyName, decimal
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -349,7 +349,7 @@ private void WriteNumberIndented(ReadOnlySpan<byte> escapedPropertyName, decimal
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ private void WriteNumberIndented(ReadOnlySpan<char> escapedPropertyName, double
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -353,7 +353,7 @@ private void WriteNumberIndented(ReadOnlySpan<byte> escapedPropertyName, double
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ private void WriteNumberIndented(ReadOnlySpan<char> escapedPropertyName, float v
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -353,7 +353,7 @@ private void WriteNumberIndented(ReadOnlySpan<byte> escapedPropertyName, float v
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ private void WriteStringIndented(ReadOnlySpan<char> escapedPropertyName, Guid va
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down Expand Up @@ -361,7 +361,7 @@ private void WriteStringIndented(ReadOnlySpan<byte> escapedPropertyName, Guid va
WriteNewLine(output);
}

JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent);
WriteIndentation(output.Slice(BytesPending), indent);
BytesPending += indent;

output[BytesPending++] = JsonConstants.Quote;
Expand Down
Loading