C# System.Text.Json
Karen Payne
Posted on February 19, 2024
Introduction
When working with json using strong typed classes and perfect json using System.Text.Json 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 and deserialize 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.
JsonSerializerOptions
JsonSerializerOptions is a class that provides a way to specify various serialization and deserialization behaviors.
For ASP.NET Core
services.AddControllers()
.AddJsonOptions(options => { ... });
Example
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
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
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.
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
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
And are receiving the following 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.
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}");
}
}
Working with Enum
Its common place to use an Enum to represent options for a property, for this there is the JsonStringEnumConverter class which converts enumeration values to and from strings.
Example using the following model.
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.
public static JsonSerializerOptions EnumJsonSerializerOptions = new()
{
Converters = { new JsonStringEnumConverter() },
WriteIndented = true
};
Usage
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
Note, if deserialization is missing the options a runtime exception is thrown.
If options are not defined for serialization the numeric values are provided, not the actual Enum member.
With ASP.NET Core and Razor Pages using the same model we can serialize and deserialize as done with desktop.
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
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.
[
{
"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
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.WriteIndented = true;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
Then alter the last method.
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.
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.
[
{
"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 as shown below.
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)
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.
Read string values as int
There may be cases were a json file is provided with your model expects an int.
[
{
"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.
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.
var jsonOptions = new JsonSerializerOptions()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
JsonSerializerOptions.NumberHandling 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
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.
Another method is to set an attribute for desktop or web.
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.
We want
[
{
"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.
// 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.
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.
// 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 attribute, here BirthDate will be ignored.
public class PersonIgnoreProperty : Person1
{
[JsonIgnore]
public DateOnly BirthDate { get; set; }
}
Which can be controlled with JsonIgnoreCondition Enum
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 attribute.
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.
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));
}
Ignore null property values
You can ignore properties on serialization and deserialization using JsonIgnoreCondition Enum.
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
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.
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
{
"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.
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));
}
}
Reuse JsonSerializerOptions instances
Microsoft's docs indicate: 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.
Resource
Posted on February 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.