29

Let's say I have an object of type:

public class MyClass
{
    public string Data { get; set; }
}

And I need to convert it to System.Text.Json.JsonElement. The only way I found is:

var json = JsonSerializer.Serialize(new MyClass { Data = "value" });

using var document = JsonDocument.Parse(json);

var jsonElement = document.RootElement;

Seems strange that I have to serialize it first and then parse it. Is there a better approach for this?

Previously I was using JObject from Newtonsoft.Json and I could do it like this:

var jobject = JObject.FromObject(new MyClass { Data = "value" });
rytisk
  • 1,331
  • 2
  • 12
  • 19
  • What are you planing to do with it next? – Guru Stron Jul 20 '20 at 14:01
  • I'm writing web API and there's a controller method which receives a model containing `Type` (enum) and `Content` (JsonElement). Then I convert `Content` to a concrete class based on `Type` enum. The conversion from `object` to `JsonElement` is needed in unit testing. – rytisk Jul 20 '20 at 14:12
  • 2
    Related: [System.Text.Json.JsonElement ToObject workaround](https://stackoverflow.com/q/58138793/3744182). – dbc Jul 20 '20 at 14:13
  • @dbc Saw that, but I need the opposite. – rytisk Jul 20 '20 at 14:16
  • Right, it's not a dup, it's simply a confirmation that the desired API doesn't exist yet. – dbc Jul 20 '20 at 14:19
  • 3
    This is advice coming from experience, especially after checking the [comparison in features between newtonsoft and the system.text.json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to). Stick with newtonsoft for the foreseeable future. You know it's going to be time to switch, when they decide to deprecate Newtonsoft. I know it doesn't answer your specific question, but I think it may be helpful in the long run – Jabberwocky Jul 20 '20 at 15:25

3 Answers3

45

In .NET 6 methods are being added to JsonSerializer to serialize an object directly to a JsonElement or JsonDocument:

public static partial class JsonSerializer
{
    public static JsonDocument SerializeToDocument<TValue>(TValue value, JsonSerializerOptions? options = null);
    public static JsonDocument SerializeToDocument(object? value, Type inputType, JsonSerializerOptions? options = null);
    public static JsonDocument SerializeToDocument<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo);
    public static JsonDocument SerializeToDocument(object? value, Type inputType, JsonSerializerContext context);

    public static JsonElement SerializeToElement<TValue>(TValue value, JsonSerializerOptions? options = null);
    public static JsonElement SerializeToElement(object? value, Type inputType, JsonSerializerOptions? options = null);
    public static JsonElement SerializeToElement<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo);
    public static JsonElement SerializeToElement(object? value, Type inputType, JsonSerializerContext context);
}

Thus in .NET 6 you will be able to do:

using var jsonDocument = JsonSerializer.SerializeToDocument(new MyClass { Data = "value" });

or

var jsonElement = JsonSerializer.SerializeToElement(new MyClass { Data = "value" });

Notes:

  • JsonSerializerContext and JsonTypeInfo<T> are newly exposed in .NET 6 and provide metadata about a set of types, or a single type T, that is relevant to JSON serialization. They are used when serializing using metadata and code generated at compile time. See Try the new System.Text.Json source generator for details.

  • JsonDocument is IDisposable, and in fact must needs be disposed because, according to the docs:

    JsonDocument builds an in-memory view of the data into a pooled buffer. Therefore, unlike JObject or JArray from Newtonsoft.Json, the JsonDocument type implements IDisposable and needs to be used inside a using block.

    In your sample code you do not dispose of the document returned by JsonDocument.Parse(), but you should.

  • The new methods should be present in .NET 6 RC1.

In .NET 5 and earlier a method equivalent to JObject.FromObject() is not currently available out of the box in System.Text.Json. There is an open enhancement about this, currently targeted for Future:

In the interim you may get better performance by serializing to an intermediate byte array rather than to a string, since both JsonDocument and Utf8JsonReader work directly with byte spans rather than strings or char spans, like so:

public static partial class JsonExtensions
{
    public static JsonDocument JsonDocumentFromObject<TValue>(TValue value, JsonSerializerOptions options = default) 
        => JsonDocumentFromObject(value, typeof(TValue), options);

    public static JsonDocument JsonDocumentFromObject(object value, Type type, JsonSerializerOptions options = default)
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(value, type, options);
        return JsonDocument.Parse(bytes);
    }

    public static JsonElement JsonElementFromObject<TValue>(TValue value, JsonSerializerOptions options = default)
        => JsonElementFromObject(value, typeof(TValue), options);

    public static JsonElement JsonElementFromObject(object value, Type type, JsonSerializerOptions options = default)
    {
        using var doc = JsonDocumentFromObject(value, type, options);
        return doc.RootElement.Clone();
    }
}

And then call it like:

using var doc = JsonExtensions.JsonDocumentFromObject(new MyClass { Data = "value" });

Or, if you need to use the root element outside the scope of a using statement:

var element = JsonExtensions.JsonElementFromObject(new MyClass { Data = "value" });

Notes:

  • As noted above, a JsonDocument needs to be disposed after being created. The above JsonExtensions.JsonElementFromObject() extension methods correctly dispose of their internal document and returns a clone of the root element, as recommended in the documentation.

  • Serializing to an intermediate Utf8 byte sequence is likely to be more performant than serializing to a string because, according to the docs:

    Serializing to UTF-8 is about 5-10% faster than using the string-based methods. The difference is because the bytes (as UTF-8) don't need to be converted to strings (UTF-16).

  • For the inverse method, see System.Text.Json.JsonElement ToObject workaround.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Moreover, the type is not used in the code. Bigger issue is if the object value is already a json string, See my proposed improvement. – EricBDev Jan 07 '21 at 11:33
  • The referenced GitHub issue states that the extension methods are included in .net 6. https://github.com/dotnet/runtime/issues/31274 – Coding101 Sep 14 '21 at 15:55
  • 1
    @Coding101 - I've seen that. I'll update the answer once .NET 6 is out of Beta and released (or, at the minimum, the [documentation for .Net 6](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializer?view=net-6.0) is updated to include the [new APIs](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs) so I can link to them.) – dbc Sep 14 '21 at 16:05
  • @Coding101 - went ahead and updated the answer anyway. – dbc Sep 14 '21 at 20:43
  • 1
    Looks like missing to pass the `type` argument in to the `JsonSerializer.SerializeToUtf8Bytes` in the second function of `JsonDocumentFromObject`. – Janeks Malinovskis Jan 21 '22 at 18:35
  • @JaneksMalinovskis - You're right. Fixed. – dbc Jan 21 '22 at 18:59
3

A slick approach in .NET 5 would be:

private JsonElement JsonElementFromObject(object value)
{
    var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions());
    using var doc = JsonDocument.Parse(jsonUtf8Bytes);
    return doc.RootElement.Clone();
}

Steps:

  1. Convert the value into a JSON string, encoded as UTF-8 bytes (SerializeToUtf8Bytes).
  2. Parse the JSON string (JsonDocument.Parse).
  3. Return the root element.
Alex Klaus
  • 8,168
  • 8
  • 71
  • 87
  • 1
    Why u are using `new JsonSerializerOptions { WriteIndented = true }` for serialization? And also don't get why to create the additional `reader` variable with `Utf8JsonReader` instead of using just `JsonDocument.Parse(jsonUtf8Bytes)`? – Janeks Malinovskis Jan 21 '22 at 18:51
  • Thank you @JaneksMalinovskis, you're absolutely right! I didn't trim enough of my code when was posting the answer. Fixed it! – Alex Klaus Jan 24 '22 at 11:26
2

dbc's answer is a good start, but not enough if the object value is already a json string! Moreover, the type is not used in his code.

Thus I propose the following improved version:

    public static JsonDocument JsonDocumentFromObject(object value, JsonSerializerOptions options = null)
    {
        if (value is string valueStr)
        {
            try { return JsonDocument.Parse(valueStr); }
            catch {}
        }

        byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(value, options);
        return JsonDocument.Parse(bytes);
    }

    public static JsonElement JsonElementFromObject(object value, JsonSerializerOptions options = null)
    {
        JsonElement result;
        using (JsonDocument doc = JsonDocumentFromObject(value, options))
        {
            result = doc.RootElement.Clone();
        }
        return result;
    }

with the following unit test (xUnit):

    [Fact()]
    public void JsonElementFromObjectTest()
    {
        object o = new
        {
            id = "myId",
            timestamp = DateTime.UtcNow,
            valid = true,
            seq = 1
        };

        JsonElement element1 = JsonSerializerExtension.JsonElementFromObject(o);
        Assert.Equal(JsonValueKind.Object, element1.ValueKind);
        string oStr1 = element1.GetRawText();
        Assert.NotNull(oStr1);

        JsonElement element2 = JsonSerializerExtension.JsonElementFromObject(oStr1);
        Assert.Equal(JsonValueKind.Object, element2.ValueKind);
        string oStr2 = element2.GetRawText();
        Assert.NotNull(oStr2);

        Assert.Equal(oStr1, oStr2);
    }

without the direct try Parse, element2 is a JsonValueKind.String and oStr2 contains the not escaped unicode characters, thus being an invalid Json string.

EricBDev
  • 1,279
  • 13
  • 21