Skip to content

JsonEncodedText: Allow skipping and customizing escaping while writing JSON #29374

@ahsonkhan

Description

@ahsonkhan

Motivation:

  1. If the user wants to write string literals (or known strings), checking to escape is an unnecessary expense.

  2. Today, for defense in depth, the writer over-escapes by default. This includes the characters that is required to be escaped according to the JSON spec, but also all non-ASCII characters, and some within the ASCII range as well (+, &, etc.).

API Proposal:

namespace System.Text.Json
{
    // JsonEncodedString? JsonEscapedString/Text/Value?
    public sealed class JsonEncodedText
    {
        // Solves motivation 1: Default escaping (internally uses equivalent to JavaScriptEncoder.Default)

        public JsonEncodedText(string value);
        public JsonEncodedText(ReadOnlySpan<char> value);
        public JsonEncodedText(ReadOnlySpan<byte> utf8Value);

        // Solves motivation 2: Custom escaping via JavaScriptEncoder

        // OR optional encoder parameter where null means default?
        public JsonEncodedText(string value, JavaScriptEncoder encoder);
        public JsonEncodedText(ReadOnlySpan<char> value, JavaScriptEncoder encoder);
        public JsonEncodedText(ReadOnlySpan<byte> utf8Value, JavaScriptEncoder encoder);
        public byte[] EncodedUtf8String { get ; } // OR ReadOnlySpan<byte>?
        public string EncodedString { get ; } // OR ReadOnlySpan<char>?
        public JavaScriptEncoder Encoder { get ; }
        public bool IsAlreadyEncoded(JavaScriptEncoder encoder);
        public override string ToString();
    }

    public partial struct JsonWriterOptions
    {
        // Exists:
        public bool Indented { get; set { } }
        public bool SkipValidation { get; set { } }

        // New:
        public JavaScriptEncoder Encoder { get; set { } }
    }

    public sealed partial class Utf8JsonWriter : System.IDisposable
    {
        // ...
        // other overloads
        // ...
        public void WriteStringValue(string value) { }

        // All overloads that take string get one that takes JsonEncodedText
        public void WriteStringValue(JsonEncodedText value) { }

        public void WriteString(JsonEncodedText propertyName, DateTime value) { }
        public void WriteString(JsonEncodedText propertyName, DateTimeOffset value) { }
        public void WriteString(JsonEncodedText propertyName, Guid value) { }

        public void WriteString(string propertyName, JsonEncodedText value) { }
        public void WriteString(JsonEncodedText propertyName, ReadOnlySpan<byte> utf8Value) { }
        public void WriteString(JsonEncodedText propertyName, ReadOnlySpan<char> value) { }
        public void WriteString(JsonEncodedText propertyName, string value) { }
        public void WriteString(JsonEncodedText propertyName, JsonEncodedText value) { }
        public void WriteString(ReadOnlySpan<char> propertyName, JsonEncodedText value) { }
        public void WriteString(ReadOnlySpan<byte> utf8PropertyName, JsonEncodedText value) { }

        public void WriteStartObject(JsonEncodedText propertyName) { }
        public void WriteStartArray(JsonEncodedText propertyName) { }

        public void WriteBoolean(JsonEncodedText propertyName, bool value) { }
        public void WriteNull(JsonEncodedText propertyName) { }

        public void WriteNumber(JsonEncodedText propertyName, decimal value) { }
        public void WriteNumber(JsonEncodedText propertyName, double value) { }
        public void WriteNumber(JsonEncodedText propertyName, int value) { }
        public void WriteNumber(JsonEncodedText propertyName, long value) { }
        public void WriteNumber(JsonEncodedText propertyName, float value) { }
        [System.CLSCompliantAttribute(false)]
        public void WriteNumber(JsonEncodedText propertyName, uint value) { }
        [System.CLSCompliantAttribute(false)]
        public void WriteNumber(JsonEncodedText propertyName, ulong value) { }
    }
}

Sample usage within JsonWriter:

public void WriteStringValue(JsonEncodedText value)
{
    // skip escaping check, skip escaping
    if (value.IsAlreadyEncoded(Options.Encoder))
    {
        WriteStringValueHelper(value.EncodedUtf8String);
    }
    else
    {
        // Existing UTF-8 write string API, which checks for escaping/etc.
        WriteStringValue(value.EncodedUtf8String);
    }
}

Sample user code:

// Custom encoder optional
private static readonly JsonEncodedText FirstJson = new JsonEncodedText("first");
private static readonly JsonEncodedText LastJson = new JsonEncodedText("last");
private static readonly JsonEncodedText AgeJson = new JsonEncodedText("age", JavaScriptEncoder.Default);

public void WriteBasicJsonEncoded(ArrayBufferWriter<byte> output)
{
    // Default encoder is `JavaScriptEncoder.Default`
    using (var json = new Utf8JsonWriter(output, new JsonWriterOptions { Indented = false, SkipValidation = true, Encoder = JavaScriptEncoder.Default }))
    {
        json.WriteStartObject();
        json.WriteNumber(AgeJson, 42);
        json.WriteString(FirstJson, "John");
        json.WriteString(LastJson, "Smith");
        json.WriteEndObject();
        json.Flush();
    }
}

Questions:

  1. Should JsonEncodedText override the encoder passed in to the Utf8JsonWriter via JsonWriterOptions or should there be no escaping?
    • If the Utf8JsonWriter always wins, we don't need the IsAlreadyEncoded.
    • struct or class?
  2. Do we want optional ctor arguments that take JavaScriptEncoder or just overloads?
  3. Should we return ROS<char>/ROS<byte> or regular string/byte[]?
  4. Type names? JsonEncodedString? JsonEscapedString/Text/Value?
  5. Should this new JsonWriterOption be exposed via the serializer?
  6. Should this implement IEquatable<JsonEncodedText>?

Notes:

cc @ycrumeyrolle, @ccnxpt, @GrabYourPitchforks, @steveharter, @bartonjs, @BrennanConroy, @JamesNK, @rynowak, @KrzysztofCwalina, @stephentoub

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions