1

Suppose I have a class with a ToString function. I can easily customize json.net serialization so that the class is written as the string returned by ToString. That is easy.

But I do not like the idea of creating a very short lived string just for that purpose. When serializing a lot of objects (in the hundred of thousands) this creates extra pressure on the GC.

Instead, I would like to write to the JsonWriter directly mimicking the logic of the ToString function, i.e. to have something like this:

class X
{
    public override string ToString(){ ... }
    public void Write(JsonWriter writer){ ... }
}

The json serializer will be customized to invoke X.Write function, but the problem is that I do not know how to implement it properly so that it respects the configured formatting and all the other json settings.

My current implementation has to resort to reflection:

    private static readonly Action<JsonWriter, JsonToken> s_internalWriteValue = (Action<JsonWriter, JsonToken>)Delegate
        .CreateDelegate(typeof(Action<JsonWriter, JsonToken>), typeof(JsonWriter)
        .GetMethod("InternalWriteValue", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic));

...

        internal void Write(JsonWriter writer)
        {
...
            s_internalWriteValue(writer, JsonToken.String);
            writer.WriteRaw("\"[");
            writer.WriteRaw(statusCode);
            writer.WriteRaw(",");
            writer.WriteRaw(isCompilerGeneratedCode);
            writer.WriteRaw(",");
            writer.WriteRaw(scope);
            writer.WriteRaw(",");
            writer.WriteRaw(kind);
            writer.WriteRaw(",");
            writer.WriteRaw(rawName);
            writer.WriteRaw("] ");
            writer.WriteRaw(Signature);
            writer.WriteRaw("\"");
        }

I failed to find a solution that would use only public API. I use json.net 13.0.3

So my question is - how can we have this approach using only public json.net API?

3
  • You could end up writing malformed JSON with this approach if your nested strings rawName, Signature and so on include any characters that must be escaped. See e.g. this answer to How to use string interpolation and verbatim string together to create a JSON string literal? for a breakdown of what you need to do to correctly format JSON strings manually. Commented Aug 7, 2024 at 15:59
  • With that in mind, are you sure you really want to do this? The Newtonsoft docs cover something similar, in Performance Tips: Manually Serialize they show how to optimize performance by manually writing each field. Is that sufficient for your needs? Do you really need to to write the whole thing as a sequence of raws? Commented Aug 7, 2024 at 16:03
  • The problem is not writing each field. The problem is writing one particular field which string representation is computed from different pieces. I would like to write these pieces directly to Json writer rather than create a dedicated string first, then write that string and then let that string become garbage right away. Commented Aug 7, 2024 at 23:52

1 Answer 1

1

To update the WriteState of the incoming JsonWriter to reflect that a value has been written, call WriteRawValue() instead of WriteRaw() for exactly one of your text values. This method:

Writes raw JSON where a value is expected and updates the writer's state.

Thus your Write(JsonWriter writer) becomes:

public void Write(JsonWriter writer)
{
    writer.WriteRawValue("\"["); // Write the beginning of the JSON string literal and update the WriteState
    writer.WriteRaw(statusCode); // Write the remainder of the JSON string literal without changing the WriteState
    writer.WriteRaw(",");
    writer.WriteRaw(isCompilerGeneratedCode);
    writer.WriteRaw(",");
    writer.WriteRaw(scope);
    writer.WriteRaw(",");
    writer.WriteRaw(kind);
    writer.WriteRaw(",");
    writer.WriteRaw(rawName);
    writer.WriteRaw("] ");
    writer.WriteRaw(Signature);
    writer.WriteRaw("\"");
}

Demo fiddle #1 here.

That being said, your WriteJson() method will output malformed JSON in the event that any of your string-valued fields contain characters that must be escaped according to RFC 8259. If you want to write raw values manually you must take care of this yourself. First introduce the following extension method:

public static partial class JsonExtensions
{
    static Dictionary<char, string> GetMandatoryEscapes()
    {
        // Standard escapes from https://www.json.org/json-en.html
        var fixedEscapes = new KeyValuePair<char, string> []
        {
            new('\\', "\\\\"),
            new('"',  "\\\""), // This is correct, but System.Text.Json preferrs the longer "\\u0022" for security reasons.
            new('/',  "\\/"),
            new('\b', "\\b"),
            new('\f', "\\f"),
            new('\n', "\\n"),
            new('\r', "\\r"),
            new('\t', "\\t"),
        };
        // Required control escapes from https://www.rfc-editor.org/rfc/rfc8259#section-7
        var controlEscapes = Enumerable.Range(0, 0x1F + 1)
            .Select(i => (char)i)
            .Except(fixedEscapes.Select(p => p.Key))
            .Select(c => new KeyValuePair<char, string>(c, @"\u"+((int)c).ToString("X4")));
        return fixedEscapes.Concat(controlEscapes).ToDictionary(p => p.Key, p => p.Value);
    }

    static Dictionary<char, string> Escapes { get; } = GetMandatoryEscapes();

    public static void WriteRawWithEscaping(this JsonWriter writer, string s)
    {
        ArgumentNullException.ThrowIfNull(writer);
        if (s == null)
            return;
        if (s.Any(c => Escapes.ContainsKey(c)))
        {
            // There is no method WriteRaw(char c) so our options are to create a string for each character, or a single escaped string.
            var escaped = s.Aggregate(new StringBuilder(), (sb, c) => Escapes.TryGetValue(c, out var s) ? sb.Append(s) : sb.Append(c)).ToString();
            writer.WriteRaw(escaped);
        }
        else
            writer.WriteRaw(s);
    }
}

And modify your Write(JsonWriter writer) method as follows:

public void Write(JsonWriter writer)
{
    //s_internalWriteValue(writer, JsonToken.String);
    writer.WriteRawValue("\"[");
    writer.WriteRawWithEscaping(statusCode);
    writer.WriteRaw(",");
    writer.WriteRawWithEscaping(isCompilerGeneratedCode);
    writer.WriteRaw(",");
    writer.WriteRawWithEscaping(scope);
    writer.WriteRaw(",");
    writer.WriteRawWithEscaping(kind);
    writer.WriteRaw(",");
    writer.WriteRawWithEscaping(rawName);
    writer.WriteRaw("] ");
    writer.WriteRawWithEscaping(Signature);
    writer.WriteRaw("\"");
}

Your JSON should now be well formed. Demo fiddle #2 here.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.