Edit

Supported types in System.Text.Json

This article gives an overview of which types are supported for serialization and deserialization.

Types that serialize as JSON objects

The following types serialize as JSON objects:

  • Classes*
  • Structs
  • Interfaces
  • Records and struct records

* Non-dictionary types that implement IEnumerable<T> serialize as JSON arrays. Dictionary types, which do implement IEnumerable<T>, serialize as JSON objects.

The following code snippet shows the serialization of a simple struct.

public static void Main()
{
    var coordinates = new Coords(1.0, 2.0);
    string json = JsonSerializer.Serialize(coordinates);
    Console.WriteLine(json);

    // Output:
    // {"X":1,"Y":2}
}

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; }
    public double Y { get; }
}

Types that serialize as JSON arrays

.NET collection types serialize as JSON arrays. System.Text.Json.JsonSerializer supports a collection type for serialization if it:

The serializer calls the GetEnumerator() method and writes the elements.

Deserialization is more complicated and is not supported for some collection types.

The following sections are organized by namespace and show which types are supported for serialization and deserialization.

System.Array namespace

Type Serialization Deserialization
Single-dimensional arrays* ✔️ ✔️
Multi-dimensional arrays ❌ ❌
Jagged arrays ✔️ ✔️

* byte[] is handled specially and serializes as a base64 string, not a JSON array.

System.Collections namespace

Type Serialization Deserialization
ArrayList ✔️ ✔️
BitArray ✔️ ❌
DictionaryEntry ✔️ ✔️
Hashtable ✔️ ✔️
ICollection ✔️ ✔️
IDictionary ✔️ ✔️
IEnumerable ✔️ ✔️
IList ✔️ ✔️
Queue ✔️ ✔️
SortedList ✔️ ✔️
Stack * ✔️ ✔️

* See Support round trip for Stack types.

System.Collections.Generic namespace

Type Serialization Deserialization
Dictionary<TKey,TValue> * ✔️ ✔️
HashSet<T> ✔️ ✔️
IAsyncEnumerable<T> † ✔️ ✔️
ICollection<T> ✔️ ✔️
IDictionary<TKey,TValue> * ✔️ ✔️
IEnumerable<T> ✔️ ✔️
IList<T> ✔️ ✔️
IReadOnlyCollection<T> ✔️ ✔️
IReadOnlyDictionary<TKey,TValue> * ✔️ ✔️
IReadOnlyList<T> ✔️ ✔️
IReadOnlySet<T> § ✔️ ✔️
ISet<T> ✔️ ✔️
KeyValuePair<TKey,TValue> ✔️ ✔️
LinkedList<T> ✔️ ✔️
LinkedListNode<T> ✔️ ❌
List<T> ✔️ ✔️
Queue<T> ✔️ ✔️
SortedDictionary<TKey,TValue> * ✔️ ✔️
SortedList<TKey,TValue> * ✔️ ✔️
SortedSet<T> ✔️ ✔️
Stack<T> ‡ ✔️ ✔️

* See Supported key types.

† See the following section on IAsyncEnumerable<T>.

‡ See Support round trip for Stack types.

§ System.Text.Json supports IReadOnlySet<T> in .NET 11 and later versions. When you deserialize the interface, the serializer creates a HashSet<T> instance. For generated metadata, JsonMetadataServices.CreateIReadOnlySetInfo creates the collection contract.

IAsyncEnumerable<T>

The following examples use streams as a representation of any async source of data. The source could be files on a local machine, or results from a database query or web service API call.

Stream serialization

System.Text.Json supports serializing IAsyncEnumerable<T> values as JSON arrays, as shown in the following example:

using System.Text.Json;

namespace IAsyncEnumerableSerialize;

public class Program
{
    public static async Task Main()
    {
        using Stream stream = Console.OpenStandardOutput();
        var data = new { Data = PrintNumbers(3) };
        await JsonSerializer.SerializeAsync(stream, data);
    }

    static async IAsyncEnumerable<int> PrintNumbers(int n)
    {
        for (int i = 0; i < n; i++)
        {
            await Task.Delay(1000);
            yield return i;
        }
    }
}
// output:
//  {"Data":[0,1,2]}

IAsyncEnumerable<T> values are only supported by the asynchronous serialization methods, such as JsonSerializer.SerializeAsync.

In .NET 11 and later versions, JsonSerializer.SerializeAsyncEnumerable writes an IAsyncEnumerable<T> sequence to either a Stream or a PipeWriter. With the default topLevelValues: false, the method writes a single root-level JSON array. Set topLevelValues: true to write JSON Lines instead, where each element is a separate top-level value:

{"id":1,"name":"apple"}
{"id":2,"name":"banana"}

The method writes a single line feed (LF), \n, after every value, including the last. It always uses LF, regardless of JsonSerializerOptions.NewLine. The method ignores JsonSerializerOptions.WriteIndented, so each value remains on one line.

Stream deserialization

The DeserializeAsyncEnumerable method supports streaming deserialization, as shown in the following example:

using System.Text;
using System.Text.Json;

namespace IAsyncEnumerableDeserialize;

public class Program
{
    public static async Task Main()
    {
        using var stream = new MemoryStream(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
        await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
        {
            Console.WriteLine(item);
        }
    }
}
// output:
//0
//1
//2
//3
//4

By default, JsonSerializer.DeserializeAsyncEnumerable reads elements from a single root-level JSON array. Set topLevelValues: true to read a sequence of whitespace-separated top-level values instead. This input format is a superset of JSON Lines. Overloads accept either a Stream or a PipeReader.

The DeserializeAsync method supports IAsyncEnumerable<T>, but its signature doesn't allow streaming. It returns the final result as a single value, as shown in the following example.

using System.Text;
using System.Text.Json;

namespace IAsyncEnumerableDeserializeNonStreaming;

public class MyPoco
{
    public IAsyncEnumerable<int>? Data { get; set; }
}

public class Program
{
    public static async Task Main()
    {
        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"{""Data"":[0,1,2,3,4]}"));
        MyPoco? result = await JsonSerializer.DeserializeAsync<MyPoco>(stream)!;
        await foreach (int item in result!.Data!)
        {
            Console.WriteLine(item);
        }
    }
}
// output:
//0
//1
//2
//3
//4

In this example, the deserializer buffers all IAsyncEnumerable<T> contents in memory before returning the deserialized object. This behavior is necessary because the deserializer needs to read the entire JSON payload before returning a result.

System.Collections.Immutable namespace

Type Serialization Deserialization
IImmutableDictionary<TKey,TValue> † ✔️ ✔️
IImmutableList<T> ✔️ ✔️
IImmutableQueue<T> ✔️ ✔️
IImmutableSet<T> ✔️ ✔️
IImmutableStack<T> * ✔️ ✔️
ImmutableArray<T> ✔️ ✔️
ImmutableDictionary<TKey,TValue> † ✔️ ✔️
ImmutableHashSet<T> ✔️ ✔️
ImmutableQueue<T> ✔️ ✔️
ImmutableSortedDictionary<TKey,TValue> † ✔️ ✔️
ImmutableSortedSet<T> ✔️ ✔️
ImmutableStack<T> * ✔️ ✔️

* See Support round trip for Stack types.

† See Supported key types.

System.Collections.Specialized namespace

Type Serialization Deserialization
BitVector32 ✔️ ❌*
HybridDictionary ✔️ ✔️
IOrderedDictionary ✔️ ❌
ListDictionary ✔️ ✔️
NameValueCollection ✔️ ❌
StringCollection ✔️ ❌
StringDictionary ✔️ ❌

* When BitVector32 is deserialized, the Data property is skipped because it doesn't have a public setter. No exception is thrown.

System.Collections.Concurrent namespace

Type Serialization Deserialization
BlockingCollection<T> ✔️ ❌
ConcurrentBag<T> ✔️ ❌
ConcurrentDictionary<TKey,TValue> † ✔️ ✔️
ConcurrentQueue<T> ✔️ ✔️
ConcurrentStack<T> * ✔️ ✔️

* See Support round trip for Stack types.

† See Supported key types.

System.Collections.ObjectModel namespace

Type Serialization Deserialization
Collection<T> ✔️ ✔️
KeyedCollection<string, TValue> * ✔️ ❌
ObservableCollection<T> ✔️ ✔️
ReadOnlyCollection<T> ✔️ ❌
ReadOnlyDictionary<TKey,TValue> ✔️ ❌
ReadOnlyObservableCollection<T> ✔️ ❌

* Non-string keys are not supported.

Custom collections

Any collection type that isn't in one of the preceding namespaces is considered a custom collection. Such types include user-defined types and types defined by ASP.NET Core. For example, Microsoft.Extensions.Primitives is in this group.

All custom collections (everything that derives from IEnumerable) are supported for serialization, as long as their element types are supported.

Deserialization support

A custom collection is supported for deserialization if it:

Known issues

There are known issues with the following custom collections:

For more information about known issues, see the open issues in System.Text.Json.

Supported key types

When used as the keys of Dictionary and SortedList types, the following types have built-in support:

  • BFloat16 (.NET 11 and later)
  • Boolean
  • Byte
  • DateTime
  • DateTimeOffset
  • Decimal
  • Decimal32 (.NET 11 and later)
  • Decimal64 (.NET 11 and later)
  • Decimal128 (.NET 11 and later)
  • Double
  • Enum
  • Guid
  • Int16
  • Int32
  • Int64
  • Object (Only on serialization and if the runtime type is one of the supported types in this list.)
  • SByte
  • Single
  • String
  • TimeSpan
  • UInt16
  • UInt32
  • UInt64
  • Uri
  • Version

In addition, the JsonConverter<T>.WriteAsPropertyName(Utf8JsonWriter, T, JsonSerializerOptions) and JsonConverter<T>.ReadAsPropertyName(Utf8JsonReader, Type, JsonSerializerOptions) methods let you add dictionary key support for any type of your choosing.

BFloat16 and decimal floating-point types

Starting in .NET 11, System.Text.Json includes built-in converters for the BFloat16, Decimal32, Decimal64, and Decimal128 types. Finite values serialize as JSON numbers.

These types behave like the other built-in numeric types:

JsonMetadataServices exposes converter properties for source-generated metadata. The properties are JsonMetadataServices.BFloat16Converter, JsonMetadataServices.Decimal32Converter, JsonMetadataServices.Decimal64Converter, and JsonMetadataServices.Decimal128Converter.

F# discriminated unions

Starting in .NET 11, System.Text.Json serializes and deserializes F# discriminated unions, including class, struct, and recursive unions:

type Shape =
    | Point
    | Circle of radius: float
  • A case without fields serializes as a JSON string that contains the case name, such as "Point".
  • A case that has fields serializes as a JSON object. The object contains a $type discriminator followed by the case's named fields, such as {"$type":"Circle","radius":3.14}.

JsonSerializerOptions.PropertyNamingPolicy applies to case names and field names. A case-level JsonPropertyNameAttribute takes precedence. To use a discriminator property name other than $type, set JsonPolymorphicAttribute.TypeDiscriminatorPropertyName.

Important

F# discriminated union support is reflection-only. It requires dynamic code and untrimmed reflection metadata. You can't use it with System.Text.Json source generation or Native AOT.

Unsupported types

The following types aren't supported for serialization:

System.Data namespace

There are no built-in converters for DataSet, DataTable, and related types in the System.Data namespace. Deserializing these types from untrusted input is not safe, as explained in the security guidance. However, you can write a custom converter to support these types. For sample custom converter code that serializes and deserializes a DataTable, see RoundtripDataTable.cs.

See also