C# System.Text.Json
Tags: csharp, dotnetcore, json, codenewbie
Published: 2024-02-19 22:28:35


![razor pages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0uy58j3q0bcfmxtu3ifz.png)

## Introduction

When working with json using strong typed classes and perfect json using [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-8.0) functionality for the most part is easy although there can be roadblocks which this article will address.

## Official documentation

Microsoft has documented working with json in the following two links, [serialize](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/how-to) and [deserialize](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/deserialization) json which is well worth taking some time to review.

Using these two links is where things started for the following topics. Even with great documentation there are still things that need to be drill down into.

{% cta https://github.com/karenpayneoregon/json-samples %} GitHub Source code {% endcta %}

## JsonSerializerOptions

[JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions?view=net-8.0)  is a class that provides a way to specify various serialization and deserialization behaviors. 

For ASP.NET Core

```csharp
services.AddControllers()  
 .AddJsonOptions(options => { ... });
```

Example

```csharp
builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
```

Both of the above will be discussed later.

In the code sample provided, some samples will have options defined in the method for ease of working things out while others will use options from a class.

Example with options in the method

## Working with lowercased property names

```csharp
public static void CasingPolicy()
{

    JsonSerializerOptions options = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };
   
    List<Product>? list = JsonSerializer.Deserialize<List<Product>>(json, options);

}
```

While the proper way would be

```csharp
public static void CasingPolicy()
{
    List<Product>? list = JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.LowerCaseOptions);
}
```

JsonHelpers is a class in a separate class project with several predefined configurations.

```csharp
public class JsonHelpers
{
    public static JsonSerializerOptions CaseInsensitiveOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };
    public static readonly JsonSerializerOptions WebOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true
    };
    public static JsonSerializerOptions WithWriteIndentOptions = new()
    {
        WriteIndented = true
    };

    public static JsonSerializerOptions WithWriteIndentAndIgnoreReadOnlyPropertiesOptions = new()
    {
        WriteIndented = true, IgnoreReadOnlyProperties = true
    };

    public static JsonSerializerOptions EnumJsonSerializerOptions = new() 
        { Converters = { new JsonStringEnumConverter() }, WriteIndented = true };

    public static JsonSerializerOptions LowerCaseOptions = new() 
        { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };

    public static List<T>? Deserialize<T>(string json)
        => JsonSerializer.Deserialize<List<T>>(json, WebOptions);

    public class LowerCaseNamingPolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name) => name.ToLower();
    }

}
```

And for desktop typically set up at class level as a static read-only property.



## Class property casing

Most times when deserializing json property names are in the following format, Id, FirstName, LastName,BirthDate etc but what if json is id, firstname, lastname, birthdate?

For this we are working with the following model

```csharp
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}
```

And are receiving the following json.

```json
[
  {
    "name": "iPhone max",
    "id": 1
  },
  {
    "name": "iPhone case",
    "id": 2
  },
  {
    "name": "iPhone ear buds",
    "id": 3
  }
]
```

**Code**

- SerializeLowerCasing method generates the json shown above
- DeserializeLowerCasing method deserializes the json above and display the json to a console window

**Important**
The deserialization option must, in this case match the same options as when serialized but let's look at it as matching the options from an external source.

```csharp
public static void CasingPolicy()
{
    var json = SerializeLowerCasing();

    DeserializeLowerCasing(json);
    
}

public static string SerializeLowerCasing()
{
    return JsonSerializer.Serialize(
        new List<Product>
        {
            new() { Id = 1, Name = "iPhone max"},
            new() { Id = 2, Name = "iPhone case" },
            new() { Id = 3, Name = "iPhone ear buds" }
        }, JsonHelpers.LowerCaseOptions);
}

public static void DeserializeLowerCasing(string json)
{
    List<Product>? products = JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.LowerCaseOptions);
    WriteOutJson(json);

    Console.WriteLine();
    Console.WriteLine();
    foreach (var product in products)
    {
        Console.WriteLine($"{product.Id,-3}{product.Name}");
    }
}
```


![results of serialize and deserialize operations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4r3ggjngx8zudf6814sh.png)

## Working with Enum

Its common place to use an Enum to represent options for a property, for this there is the [JsonStringEnumConverter](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter?view=net-8.0) class which converts enumeration values to and from strings.

Example using the following model.

```csharp
public class PersonWithGender
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}
```

Option for this is in the class JsonHelpers.

```csharp
public static JsonSerializerOptions EnumJsonSerializerOptions = new()
{
    Converters = { new JsonStringEnumConverter() }, 
    WriteIndented = true
};
```

Usage

```csharp
public static void WorkingWithEnums()
{

    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, JsonHelpers.EnumJsonSerializerOptions);
    
    WriteOutJson(json);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(
        json, 
        JsonHelpers.EnumJsonSerializerOptions);

    Console.WriteLine();
    Console.WriteLine();
    list.ForEach(Console.WriteLine);
}
```

Results

![output from working with an enum](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/785g8ocpikrt2aygsihi.png)

Note, if deserialization is missing the options a runtime exception is thrown.


![deserialization missing options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zp8ophb93ao6ef3wakie.png)

If options are not defined for serialization the numeric values are provided, not the actual Enum member.


![serialization missing options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kdi7jkf94udwz12ssxu7.png)

With ASP.NET Core and Razor Pages using the same model we can serialize and deserialize as done with desktop.

```csharp
public class JsonSamples
{
    public List<PersonWithGender> CreatePeopleWithGender() =>
    [
        new() { Id = 1, FirstName = "Anne", LastName = "Jones", Gender = Gender.Female },
        new() { Id = 2, FirstName = "John", LastName = "Smith", Gender = Gender.Male },
        new() { Id = 3, FirstName = "Bob", LastName = "Adams", Gender = Gender.Unknown }
    ];

    public void WorkingWithEnums()
    {
        var options = new JsonSerializerOptions
        {
            Converters = { new JsonStringEnumConverter() },
            WriteIndented = true
        };

        List<PersonWithGender> people = CreatePeopleWithGender();
        var json = JsonSerializer.Serialize(people);
        List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json);
    }

}
```

Or use the method in JsonHelpers class

```csharp
public void WorkingWithEnums()
{


    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, 
        JsonHelpers.EnumJsonSerializerOptions);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json, 
        JsonHelpers.EnumJsonSerializerOptions);
}
```
To get the following.

```json
[
  {
    "Id": 1,
    "FirstName": "Anne",
    "LastName": "Jones",
    "Gender": "Female"
  },
  {
    "Id": 2,
    "FirstName": "John",
    "LastName": "Smith",
    "Gender": "Male"
  },
  {
    "Id": 3,
    "FirstName": "Bob",
    "LastName": "Adams",
    "Gender": "Unknown"
  }
]
```

The other option is through adding options through WebApplicationBuilder in Program.cs

```csharp
builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    options.JsonSerializerOptions.WriteIndented = true;
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
```

Then alter the last method.

```csharp
public void WorkingWithEnums(JsonOptions options)
{

    List<PersonWithGender> people = CreatePeopleWithGender();
    var json = JsonSerializer.Serialize(people, 
        options.JsonSerializerOptions);
    List<PersonWithGender>? list = JsonSerializer.Deserialize<List<PersonWithGender>>(json, 
        options.JsonSerializerOptions);
}
```

In this case, in index.cshtml.cs we setup the options using dependency injection.

```csharp
public class IndexModel : PageModel
{
    private readonly IOptions<JsonOptions> _options;

    public IndexModel(IOptions<JsonOptions> options)
    {
        _options = options;
    }
```

## Spaces in property names

There may be cases were json data has properties with spaces.

```json
[
  {
    "Id": 1,
    "First Name": "Mary",
    "LastName": "Jones"
  },
  {
    "Id": 2,
    "First Name": "John",
    "LastName": "Burger"
  },
  {
    "Id": 3,
    "First Name": "Anne",
    "LastName": "Adams"
  }
]
```
For this, specify the property name from json with [JsonPropertyNameAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute?view=net-8.0) as shown below.

```csharp
public class Person
{
    public int Id { get; set; }
    [JsonPropertyName("First Name")]
    public string FirstName { get; set; }
    [JsonPropertyName("Last Name")]
    public string LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";
    
    /// <summary>
    /// Used for demonstration purposes 
    /// </summary>
    public override string ToString() => $"{Id,-4}{FirstName, -12} {LastName}";

}
```

C# Code (from provided code in a GitHub repository)

```csharp
public static void ReadPeopleWithSpacesInPropertiesOoops()
{
    var people = JsonSerializer.Deserialize<List<Person>>(File.ReadAllText("Json\\people2.json"));
    foreach (var person in people)
    {
        Console.WriteLine(person);
    }
}
```

For more details on this and more like hyphens in property names see the following well written [Microsoft documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/customize-properties?pivots=dotnet-8-0).

## Read string values as int

There may be cases were a json file is provided with your model expects an int.

```json
[
  {
    "Id": "1",
    "Name": "iPhone max"
  },
  {
    "Id": "2",
    "Name": "iPhone case"
  },
  {
    "Id": "3",
    "Name": "iPhone ear buds"
  }
]
```

Model where Id is an int but in json a string.

```csharp
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}
```
For this, use in desktop projects or see below for another option using an attribute on the Id property. 

```csharp
var jsonOptions = new JsonSerializerOptions()
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};
```

[JsonSerializerOptions.NumberHandling](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.numberhandling?view=net-8.0) indicates that _gets or sets an object that specifies how number types should be handled when serializing or deserializing_.

In this ASP.NET Core sample

```csharp
public async Task<List<Product>> ReadProductsWithIntAsStringFromWeb()
{

    var json = await Utilities.ReadJsonAsync(
        "https://raw.githubusercontent.com/karenpayneoregon/jsonfiles/main/products.json");
    ProductsStringAsInt = json;

    return JsonSerializer.Deserialize<List<Product>>(json, JsonHelpers.WebOptions)!;

}
```
JsonHelpers.WebOptions uses JsonSerializerDefaults.Web with the default to string quoted numbers as numeric.


![Shows sonSerializerDefaults.Web defaults](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3bdllmizvgh2rsxpdapd.png) 

Another method is to set an attribute for desktop or web.

```csharp
public class Product
{
    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
    public int Id { get; set; }
    public string Name { get; set; }
}
```

## Save decimal as two decimal places

Given the following created using Bogus Nuget package.

![screen shot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zep2gqks289y81y4l9b.png)

We want

```json
[
  {
    "ProductId": 12,
    "ProductName": "Awesome Fresh Salad",
    "UnitPrice": "2.99",
    "UnitsInStock": 5
  },
  {
    "ProductId": 19,
    "ProductName": "Awesome Wooden Pizza",
    "UnitPrice": "7.28",
    "UnitsInStock": 3
  },
  {
    "ProductId": 15,
    "ProductName": "Ergonomic Concrete Gloves",
    "UnitPrice": "2.38",
    "UnitsInStock": 2
  }
]
```

This is done using a custom converter as follows.

```csharp
// Author https://colinmackay.scot/tag/system-text-json/
public class FixedDecimalJsonConverter : JsonConverter<decimal>
{
    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string? stringValue = reader.GetString();
        return string.IsNullOrWhiteSpace(stringValue)
            ? default
            : decimal.Parse(stringValue, CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
    {
        string numberAsString = value.ToString("F2", CultureInfo.InvariantCulture);
        writer.WriteStringValue(numberAsString);
    }
}
```

Code which in this case gets a list and saves to a json file which produces the output above.

```csharp
List<ProductItem> results = products
    .Select<Product, ProductItem>(container => container).ToList();


if (results.Any())
{
    // process checked
    File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
    {
        WriteIndented = true,
        Converters = { new FixedDecimalJsonConverter() }
    }));
}
```

Since the property UnitPrice is stored as a string we use the same technique already shown by setting NumberHandling = JsonNumberHandling.AllowReadingFromString.

```csharp
// process checked
File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
{
    WriteIndented = true,
    Converters = { new FixedDecimalJsonConverter() }
}));


var jsonOptions = new JsonSerializerOptions()
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};
var json = File.ReadAllText("Products.json");
List<Product>? productsFromFile = JsonSerializer.Deserialize<List<Product>>(json, jsonOptions);
```

## Ignore property

There may be times when a property should not be included in serialization or deserialization.

Use [JsonIgnore](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/ignore-properties) attribute, here BirthDate will be ignored.

```csharp
public class PersonIgnoreProperty : Person1
{
    [JsonIgnore]
    public DateOnly BirthDate { get; set; }
}
``` 

Which can be controlled with [JsonIgnoreCondition Enum](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonignorecondition?view=net-8.0)

```csharp
public class PersonIgnoreProperty : Person1
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public DateOnly BirthDate { get; set; }
}
```
## Immutable types

By default, System.Text.Json uses the default public parameterless constructor. However, you can tell it to use a parameterized constructor, which makes it possible to deserialize an immutable class or struct.

The following demonstrates deserializing a struct where the constructor is decelerated with [JsonConstructor](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconstructorattribute?view=net-8.0) attribute.

```csharp
public readonly struct PersonStruct
{
    public int Id { get;  }

    public string FirstName { get; }

    public string LastName { get; }

    [JsonConstructor]
    public PersonStruct(int id, string firstName, string lastName) =>
        (Id, FirstName, LastName) = (id, firstName, lastName);

    /// <summary>
    /// Used for demonstration purposes 
    /// </summary>
    public override string ToString() => $"{Id, -4}{FirstName,-12}{LastName}";

}
```

Deserializing from static json.

```csharp
public static void Immutable()
{
    var json =
        """
        [
           {
             "Id": 1,
             "FirstName": "Mary",
             "LastName": "Jones"
           },
           {
             "Id": 2,
             "FirstName": "John",
             "LastName": "Burger"
           },
           {
             "Id": 3,
             "FirstName": "Anne",
             "LastName": "Adams"
           }
        ]
        """;

    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
    List<PersonStruct>? peopleReadOnly = JsonSerializer.Deserialize<List<PersonStruct>>(json, options);
    peopleReadOnly.ForEach(peep => Console.WriteLine(peep));
}
```

![Results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ktllqhqbcw26jiur358s.png)

## Ignore null property values

You can ignore properties on serialization and deserialization using [JsonIgnoreCondition Enum](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonignorecondition?view=net-8.0).

Suppose json data has an primary key which should be ignored when populating a database table using EF Core or that a gender property is not needed at all. The following first shows not ignoring properties Id and Gender while the second ignores Id and Gender.

**Models**

```csharp
public class PersonWithGender
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}


public class PersonWithGender1
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public Gender Gender { get; set; }
    public override string ToString() => $"{Id,-4}{FirstName,-12} {LastName}";
}
```

Serializing data.

```csharp
public static void IgnoreNullValues()
{
    PersonWithGender person1 = new() { FirstName = "Karen", LastName = "Payne", Gender = Gender.Female};
    var data1 = JsonSerializer.Serialize(person1, JsonHelpers.EnumJsonSerializerOptions);
    PersonWithGender1 person2 = new() { FirstName = "Karen", LastName = "Payne" };
    var data2 = JsonSerializer.Serialize(person2, JsonHelpers.WithWriteIndentOptions);
}
```

Results

```json
{
  "Id": 0,
  "FirstName": "Karen",
  "LastName": "Payne",
  "Gender": "Female"
}


{
  "FirstName": "Karen",
  "LastName": "Payne"
}
```
# Serializing to a dictionary

In this sample data is read from a Microsoft NorthWind database table Employees to a dictionary with the key as first and last name and the value as the primary key. Dapper is used for the read operation.

**Important** Before running this code create the database and populate with populate.sql in the script folder of the project ReadOddJsonApp.

```csharp
internal class DapperOperations
{
    private IDbConnection db = new SqlConnection(ConnectionString());

    public void GetDictionary()
    {
        const string statement =
            """
                SELECT EmployeeID as Id, 
                     FirstName + ' ' + LastName AS FullName,
                     LastName
                FROM dbo.Employees ORDER BY LastName;
                """;

        Dictionary<string, int> employeeDictionary = db.Query(statement).ToDictionary(
            row => (string)row.FullName,
            row => (int)row.Id);


        Console.WriteLine(JsonSerializer.Serialize(employeeDictionary, JsonHelpers.WithWriteIndentOptions));
    }
}
```


![Serialize to dictionary data from a database table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tg4tzj2q26yezxnwtwe5.png)

## Reuse JsonSerializerOptions instances

Microsoft's docs [indicate](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-8-0#reuse-jsonserializeroptions-instances): If you use JsonSerializerOptions repeatedly with the same options, don't create a new JsonSerializerOptions instance each time you use it. Reuse the same instance for every call. 

In much of the code provided here violates the above as they are standalone code samples, best to follow above for applications.

## Summary

This article provides information to handle unusual json formats and normal formatting as a resources with code samples located in a GitHub repository.

## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/json-samples).

## Resource

- [System.Text.Json Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-8.0)
- [How to write custom converters for JSON serialization](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-8-0)
- [What’s new in System.Text.Json in .NET 8](https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/)