9

I'm migrating an ASP.NET Core 2 application to ASP.NET Core 3. My controllers need to return objects that have a property which is already a JSON string, so it looks something like this:

public class Thing {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Data { get; set; } // JSON Data
}

var thing = new Thing
{
    Id = 1,
    Name = "Thing",
    Data = "{\"key\":\"value\"}"
};

Thing should be serialized so that the Data property is not a string, but part of the JSON object. Like this:

{
    "id": 1,
    "name": "Thing",
    "data": {
        "key": "value"
    }
}

In .NET Core 2 and Newtonsoft.Json I used JRaw as the type for Data, so the serializer knew that the property is already serialized as JSON and didn't try to represent it as a string.

.NET Core 3 uses System.Text.Json, which is incompatible with JRaw. Is there an equivalent that does the same thing? I was able to use JsonElement as the type for Data and convert the string with JsonDocument.Parse(jsonString).RootElement. This produces the desired result, but I'd like to avoid an unnecessary deserialize+serialize step, since the data object may be relatively big.

Jarru
  • 183
  • 8
  • You can still use Json.Net for the times where System.Text.Json just doesn't cut it. System.Text.Json is intentionally light on features and was introduced to remove the dependency on Json.Net - there's no reason why you can't add it back in though. – phuzi Mar 30 '20 at 13:46
  • 1
    Looks like not, see [Writing raw property values when using System.Text.Json #1784](https://github.com/dotnet/runtime/issues/1784) and [Proposal: Utf8JsonWriter.WriteUtf8Json #32849](https://github.com/dotnet/runtime/issues/32849) – dbc Mar 30 '20 at 13:59
  • Also, don't forget to dispose of the `JsonDocument` after you are done with it. If you need a `JsonElement` to survive after the document is disposed, you need to clone it. – dbc Mar 30 '20 at 14:03
  • 1
    Should I make that an answer? – dbc Apr 06 '20 at 15:32
  • 1
    @dbc Go ahead. It seems that I'll have to continue using the good old Newtonsoft for now. – Jarru Apr 07 '20 at 07:55

1 Answers1

13

.NET 6 has introduced Utf8JsonWriter.WriteRawValue(string json, bool skipInputValidation = false):

Writes the input as JSON content. It is expected that the input content is a single complete JSON value. ...

When writing untrusted JSON values, do not set skipInputValidation to true as this can result in invalid JSON being written, or an invalid overall payload being written to the writer instance.

Thus you can now introduce the following converters:

/// <summary>
/// Serializes the contents of a string value as raw JSON.  The string is validated as being an RFC 8259-compliant JSON payload
/// </summary>
public class RawJsonConverter : JsonConverter<string>
{
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.ParseValue(ref reader);
        return doc.RootElement.GetRawText();
    }

    protected virtual bool SkipInputValidation => false;

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
        // skipInputValidation : true will improve performance, but only do this if you are certain the value represents well-formed JSON!
        writer.WriteRawValue(value, skipInputValidation : SkipInputValidation);
}

/// <summary>
/// Serializes the contents of a string value as raw JSON.  The string is NOT validated as being an RFC 8259-compliant JSON payload
/// </summary>
public class UnsafeRawJsonConverter : RawJsonConverter
{
    protected override bool SkipInputValidation => true;
}

And apply the converter of your choice to your data model:

public class Thing {
    public int Id { get; set; }
    public string Name { get; set; }
    [JsonConverter(typeof(UnsafeRawJsonConverter))]
    public string Data { get; set; } // JSON Data
}

Notes:

  • Utf8JsonReader does not appear to have an equivalent method to read a raw value of any type, so JsonDocument is still required to deserialize a raw JSON value.

  • Utf8JsonWriter.WriteRawValue(null) and Utf8JsonWriter.WriteRawValue("null") generate identical JSON, namely null. When deserialized, doc.RootElement.GetRawText() seems to return a null value for both, rather than a string containing the token null.

  • You seem to be concerned about performance, so I applied UnsafeRawJsonConverter to your data model. However, you should only use this converter if you are certain that Data contains well-formed JSON. If not, use RawJsonConverter:

     [JsonConverter(typeof(RawJsonConverter))]
     public string Data { get; set; } // JSON Data
    
  • In .NET 5 and earlier WriteRawValue() does not exist, so you will have to parse the incoming string to a JsonDocument and write that:

     public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
     {
         using var doc = JsonDocument.Parse(value);
         doc.WriteTo(writer);
     }
    

    This of course checks that value is well-formed; there is no ability to write raw JSON and skip input validation in earlier releases.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • are you aware if there's an equivalent for .net5? or some kind of workaround? – R.L Jun 19 '22 at 21:12
  • @R.L - I believe in .NET 5 you must load your raw JSON into a `JsonDocument` and do [`JsonDocument.WriteTo(Utf8JsonWriter)`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsondocument.writeto?view=net-6.0). – dbc Jun 19 '22 at 21:17
  • @R.L - Or I suppose you could try to write code to manually stream from a `Utf8JsonReader` to a `Utf8JsonWriter` like the one shown in [this answer](https://stackoverflow.com/a/55429664/3744182) by [mtosh](https://stackoverflow.com/users/7217527/mtosh) to [Parsing a JSON file with .NET core 3.0/System.text.Json](https://stackoverflow.com/q/54983533/3744182) but that seems really involved. – dbc Jun 19 '22 at 21:19